Compare commits

...

39 commits

Author SHA1 Message Date
Hesabix 82d39dbb42 add debug to system managment 2025-08-04 22:44:32 +00:00
Hesabix 11caf42da8 bug fix in cheque and add support person transactions sort by 2025-08-04 15:02:55 +00:00
Hesabix 63b6654cc8 bug fix in commodity custome code 2025-08-04 13:51:04 +00:00
Hesabix 532ca041f6 add chat system 2025-08-04 13:31:07 +00:00
Hesabix 6a4254050d Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-08-03 12:38:19 +00:00
Hesabix a7636fbc42 add chat system 2025-08-03 12:38:15 +00:00
Gloomy 82f872eb10 update for Moadian plugin 2025-08-03 11:50:58 +00:00
Gloomy 140da029a1 update for Moadian Plugin 2025-08-03 11:15:03 +00:00
Gloomy 300d802ee8 update for Moadian Plugin 2025-08-03 06:12:05 +00:00
Gloomy 8e8ea18ec9 update for Moadian Plugin 2025-08-02 19:31:29 +00:00
Gloomy d3e936c59f update for Moadian plugin 2025-07-28 14:00:21 +00:00
Gloomy 163ec1ea2e update for hooks 2025-07-28 13:56:52 +00:00
Gloomy be782e14bd update for Hooks 2025-07-27 14:27:17 +00:00
Hesabix 195e6c0693 last bug fix in tonight 2025-07-26 12:46:50 -07:00
Hesabix 88bc35d85d more more bug fix in ticket tools 2025-07-26 12:34:04 -07:00
Hesabix 82623c0df8 more bug fix 2025-07-26 12:28:33 -07:00
Hesabix ad638e960f bugfix 2025-07-26 12:19:44 -07:00
Hesabix 1b4b9f85f2 bug fix 2025-07-26 11:58:03 -07:00
Hesabix 29cc20207f add ticket tool for AGI 2025-07-26 11:49:32 -07:00
Hesabix aaeb3cf31e progress and some bug fix in commodity,ai 2025-07-25 17:12:20 +00:00
Hesabix 91cf5d4eb6 add type selection to accounting table 2025-07-25 11:34:36 +00:00
Hesabix cc1515345b progress in salary and add export pdf/excell to that 2025-07-25 09:54:43 +00:00
Hesabix 8bc857c2f8 increase decimals to 3 digits 2025-07-24 22:13:15 +00:00
Hesabix 3c2bef6685 bug fix in accounting search 2025-07-24 21:58:50 +00:00
Hesabix bf6ca0f8b6 bug fix in presell invoice in find sell price 2025-07-24 21:45:19 +00:00
Hesabix bad8dc0f73 more progress in agi and support external tools 2025-07-24 19:19:53 +00:00
Hesabix 474f1274c0 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-07-24 12:55:48 +00:00
Hesabix a095dd530f progress in AGI 2025-07-24 12:55:44 +00:00
Gloomy 6720cc1774 Upate for Moadian plugin 2025-07-24 11:38:36 +00:00
Hesabix 3047c62f5d bug fix in save settings in zero day 2025-07-24 01:17:40 +00:00
Hesabix 4b286c481e bug fix for doctrine new version config change 2025-07-24 00:11:03 +00:00
Hesabix 92e6ecaee1 Update README.md 2025-07-24 03:14:18 +03:30
Gloomy 21aaad7ef1 Upate for Moadian plugin 2025-07-23 18:34:27 +00:00
Hesabix 68ef03e863 bug fix in dto 2025-07-23 16:43:30 +00:00
Hesabix 23c6775f60 bug fix in plugins sync 2025-07-23 15:32:30 +00:00
Hesabix 9113a15194 Merge branch 'master' of https://source.hesabix.ir/morrning/hesabixCore 2025-07-23 14:24:17 +00:00
Hesabix a638f22e3c update plugins sync and database default file for init 2025-07-23 14:21:39 +00:00
Gloomy 0be44cd46a Refactor tax settings save endpoint to use DTO and validation 2025-07-23 12:44:10 +00:00
Hesabix aa02ea0925 Merge pull request 'master' (#1) from Gloomy/hesabixCore:master into master
Reviewed-on: morrning/hesabixCore#1
2025-07-23 01:00:28 +03:30
90 changed files with 13374 additions and 3281 deletions

10
.env.local.php Normal file
View file

@ -0,0 +1,10 @@
<?php
return [
'APP_ENV' => 'prod',
'APP_SECRET' => 'f56179673fa562596e7fc565778a60f1',
'MESSENGER_TRANSPORT_DSN' => 'doctrine://default?auto_setup=0',
'MAILER_DSN' => 'null://null',
'CORS_ALLOW_ORIGIN' => '*',
'LOCK_DSN' => 'flock',
'DATABASE_URL' => 'mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4',
];

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

@ -1,8 +1,3 @@
# توقف فعالیت در گیت‌هاب به دلیل نگرانی‌های اخلاقی
ما به دلیل استفاده مایکروسافت از هوش مصنوعی در تولید سلاح‌های نظامی و آموزش مدل‌های هوش مصنوعی با داده‌های غیرنظامیان، تصمیم گرفتیم تمام فعالیت‌های خود را در پلتفرم گیت‌هاب متوقف کنیم. این تصمیم به منظور پایبندی به اصول اخلاقی و مسئولیت اجتماعی اتخاذ شده است.
برای دسترسی به سورس‌کدها و مشارکت در پروژه‌های ما، لطفاً به وب‌سایت رسمی ما به آدرس [source.hesabix.ir](https://source.hesabix.ir) مراجعه کنید.
با تشکر از حمایت و همراهی شما.
# حسابیکس - نرم‌افزار حسابداری متن‌باز
<img src="https://hesabix.ir/favicon/favicon.svg" alt="Hesabix Logo" width="100" height="100" />

View file

@ -3,8 +3,8 @@
-- https://www.phpmyadmin.net/
--
-- Host: localhost:3306
-- Generation Time: Mar 21, 2025 at 08:34 PM
-- Server version: 8.0.41-0ubuntu0.24.04.1
-- Generation Time: Jul 23, 2025 at 02:10 PM
-- Server version: 8.0.42-0ubuntu0.24.04.2
-- PHP Version: 8.3.6
SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
@ -18,7 +18,7 @@ SET time_zone = "+00:00";
/*!40101 SET NAMES utf8mb4 */;
--
-- Database: `hesabix`
-- Database: `hesabix_next_hesabix_ir`
--
-- --------------------------------------------------------
@ -45,6 +45,58 @@ CREATE TABLE `accounting_package_order` (
-- --------------------------------------------------------
--
-- Table structure for table `account_to_sheba_inquiry`
--
CREATE TABLE `account_to_sheba_inquiry` (
`id` int NOT NULL,
`cache_key` varchar(50) NOT NULL,
`sheba_data` json NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `aiconversation`
--
CREATE TABLE `aiconversation` (
`id` int NOT NULL,
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`category` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` int NOT NULL,
`updated_at` int NOT NULL,
`is_active` tinyint(1) DEFAULT NULL,
`deleted` tinyint(1) DEFAULT NULL,
`user_id` int NOT NULL,
`business_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `aimessage`
--
CREATE TABLE `aimessage` (
`id` int NOT NULL,
`role` varchar(20) NOT NULL,
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`created_at` int NOT NULL,
`input_tokens` int DEFAULT NULL,
`output_tokens` int DEFAULT NULL,
`input_cost` double DEFAULT NULL,
`output_cost` double DEFAULT NULL,
`total_cost` double DEFAULT NULL,
`model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`agent_source` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`conversation_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `apitoken`
--
@ -54,7 +106,8 @@ CREATE TABLE `apitoken` (
`bid_id` int DEFAULT NULL,
`submitter_id` int NOT NULL,
`token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`date_expire` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL
`date_expire` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`is_for_ai` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -104,6 +157,22 @@ CREATE TABLE `archive_orders` (
-- --------------------------------------------------------
--
-- Table structure for table `back_built_module`
--
CREATE TABLE `back_built_module` (
`id` int NOT NULL,
`date_submit` varchar(40) NOT NULL,
`code` longtext,
`locked` tinyint(1) DEFAULT NULL,
`public` tinyint(1) DEFAULT NULL,
`type` varchar(120) NOT NULL,
`submitter_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `bank_account`
--
@ -191,6 +260,19 @@ CREATE TABLE `business_money` (
-- --------------------------------------------------------
--
-- Table structure for table `card_to_sheba_inquiry`
--
CREATE TABLE `card_to_sheba_inquiry` (
`id` int NOT NULL,
`card_number` varchar(16) NOT NULL,
`sheba_data` json NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `cashdesk`
--
@ -243,7 +325,10 @@ CREATE TABLE `cheque` (
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`locked` tinyint(1) DEFAULT NULL,
`date` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`rejected` tinyint(1) DEFAULT NULL
`rejected` tinyint(1) DEFAULT NULL,
`transfered` tinyint(1) DEFAULT NULL,
`transfer_date` varchar(25) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`money_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -430,19 +515,22 @@ CREATE TABLE `hesabdari_doc` (
`date_submit` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`date` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`code` bigint NOT NULL,
`code` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
`des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`amount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`amount` decimal(30,0) DEFAULT NULL,
`mdate` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`shortlink` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`wallet_transaction_id` int DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`temp_status` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '(DC2Type:array)',
`temp_status` json DEFAULT NULL,
`invoice_label_id` int DEFAULT NULL,
`project_id` int DEFAULT NULL,
`salesman_id` int DEFAULT NULL
`salesman_id` int DEFAULT NULL,
`tax_percent` double DEFAULT NULL,
`discount_type` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_percent` decimal(10,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -474,16 +562,18 @@ CREATE TABLE `hesabdari_row` (
`bid_id` int NOT NULL,
`year_id` int NOT NULL,
`commodity_id` int DEFAULT NULL,
`commdity_count` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`commdity_count` decimal(20,4) DEFAULT NULL,
`salary_id` int DEFAULT NULL,
`cashdesk_id` int DEFAULT NULL,
`referral` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`temp_data` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '(DC2Type:array)',
`temp_data` json DEFAULT NULL,
`cheque_id` int DEFAULT NULL,
`discount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_type` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,
`discount_percent` decimal(10,2) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -702,9 +792,9 @@ CREATE TABLE `messenger_messages` (
`body` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`headers` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`queue_name` varchar(190) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`created_at` datetime NOT NULL COMMENT '(DC2Type:datetime_immutable)',
`available_at` datetime NOT NULL COMMENT '(DC2Type:datetime_immutable)',
`delivered_at` datetime DEFAULT NULL COMMENT '(DC2Type:datetime_immutable)'
`created_at` datetime NOT NULL,
`available_at` datetime NOT NULL,
`delivered_at` datetime DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -851,7 +941,13 @@ CREATE TABLE `permission` (
`plug_accpro_rfsell` tinyint(1) DEFAULT NULL,
`plug_accpro_accounting` tinyint(1) DEFAULT NULL,
`plug_accpro_close_year` tinyint(1) DEFAULT NULL,
`plug_repservice` tinyint(1) DEFAULT NULL
`plug_repservice` tinyint(1) DEFAULT NULL,
`plug_accpro_presell` tinyint(1) DEFAULT NULL,
`plug_hrm_docs` tinyint(1) DEFAULT NULL,
`plug_ghesta_manager` tinyint(1) DEFAULT NULL,
`plug_tax_settings` tinyint(1) DEFAULT NULL,
`inquiry` tinyint(1) DEFAULT NULL,
`ai` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
-- --------------------------------------------------------
@ -1015,6 +1111,117 @@ INSERT INTO `plugin_prodect` (`id`, `name`, `code`, `timestamp`, `timelabel`, `p
-- --------------------------------------------------------
--
-- Table structure for table `plugin_taxsettings_keys`
--
CREATE TABLE `plugin_taxsettings_keys` (
`id` int NOT NULL,
`business_id` int NOT NULL,
`user_id` int NOT NULL,
`private_key` longtext,
`tax_memory_id` varchar(64) DEFAULT NULL,
`economic_code` varchar(64) DEFAULT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plugin_tax_invoice`
--
CREATE TABLE `plugin_tax_invoice` (
`id` int NOT NULL,
`invoice_code` varchar(255) NOT NULL,
`tax_system_invoice_number` varchar(255) DEFAULT NULL,
`tax_system_reference_number` varchar(255) DEFAULT NULL,
`status` varchar(255) NOT NULL,
`response_data` longtext,
`error_message` longtext,
`created_at` datetime NOT NULL,
`sent_at` datetime DEFAULT NULL,
`confirmed_at` datetime DEFAULT NULL,
`amount` decimal(30,0) NOT NULL,
`customer_name` varchar(255) DEFAULT NULL,
`customer_id` varchar(255) DEFAULT NULL,
`invoice_type` varchar(50) DEFAULT NULL,
`business_id` int NOT NULL,
`user_id` int NOT NULL,
`invoice_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_ghesta_doc`
--
CREATE TABLE `plug_ghesta_doc` (
`id` int NOT NULL,
`date_submit` varchar(25) NOT NULL,
`count` bigint NOT NULL,
`profit_percent` bigint NOT NULL,
`profit_amount` varchar(255) DEFAULT NULL,
`profit_type` varchar(30) DEFAULT NULL,
`days_pay` double DEFAULT NULL,
`bid_id` int NOT NULL,
`submitter_id` int DEFAULT NULL,
`person_id` int NOT NULL,
`main_doc_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_ghesta_item`
--
CREATE TABLE `plug_ghesta_item` (
`id` int NOT NULL,
`date` varchar(25) NOT NULL,
`amount` varchar(120) NOT NULL,
`num` int NOT NULL,
`doc_id` int NOT NULL,
`hesabdari_doc_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_hrm_doc`
--
CREATE TABLE `plug_hrm_doc` (
`id` int NOT NULL,
`description` varchar(255) NOT NULL,
`date` varchar(10) NOT NULL,
`create_date` int NOT NULL,
`business_id` int NOT NULL,
`creator_id` int NOT NULL,
`hesabdari_doc_id` int DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_hrm_doc_item`
--
CREATE TABLE `plug_hrm_doc_item` (
`id` int NOT NULL,
`base_salary` int NOT NULL,
`overtime` int NOT NULL,
`shift` int NOT NULL,
`night` int NOT NULL,
`description` varchar(255) DEFAULT NULL,
`doc_id` int NOT NULL,
`person_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `plug_noghre_order`
--
@ -1092,6 +1299,20 @@ INSERT INTO `plug_repservice_order_state` (`id`, `label`, `code`) VALUES
-- --------------------------------------------------------
--
-- Table structure for table `postal_code_inquiry`
--
CREATE TABLE `postal_code_inquiry` (
`id` int NOT NULL,
`postal_code` varchar(10) NOT NULL,
`address_data` json NOT NULL,
`created_at` datetime NOT NULL,
`updated_at` datetime NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- --------------------------------------------------------
--
-- Table structure for table `pre_invoice_doc`
--
@ -1113,7 +1334,13 @@ CREATE TABLE `pre_invoice_doc` (
`plugin` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`ref_data` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shortlink` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL
`status` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tax_percent` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`total_discount` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`total_discount_percent` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`shipping_cost` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`show_percent_discount` tinyint(1) DEFAULT NULL,
`show_total_percent_discount` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1125,19 +1352,16 @@ CREATE TABLE `pre_invoice_doc` (
CREATE TABLE `pre_invoice_item` (
`id` int NOT NULL,
`commodity_id` int NOT NULL,
`person_id` int DEFAULT NULL,
`bank_id` int DEFAULT NULL,
`cashdesk_id` int DEFAULT NULL,
`salary_id` int DEFAULT NULL,
`bid_id` int NOT NULL,
`year_id` int NOT NULL,
`ref_id_id` int NOT NULL,
`commodity_count` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bs` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`bd` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`des` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL
`tax` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount_percent` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`discount_amount` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`show_percent_discount` tinyint(1) DEFAULT NULL,
`doc_id` int NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1254,7 +1478,11 @@ CREATE TABLE `print_options` (
`repservice_paper` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`fastsell_invoice` tinyint(1) DEFAULT NULL,
`fastsell_pdf` tinyint(1) DEFAULT NULL,
`fastsell_cashdesk_ticket` tinyint(1) DEFAULT NULL
`fastsell_cashdesk_ticket` tinyint(1) DEFAULT NULL,
`left_footer` longtext COLLATE utf8mb4_unicode_ci,
`right_footer` longtext COLLATE utf8mb4_unicode_ci,
`sell_invoice_index` tinyint(1) DEFAULT NULL,
`sell_business_stamp` tinyint(1) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- --------------------------------------------------------
@ -1292,7 +1520,7 @@ CREATE TABLE `registry` (
`id` int NOT NULL,
`root` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL,
`value_of_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL
`value_of_key` longtext COLLATE utf8mb4_unicode_ci
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
--
@ -1603,6 +1831,28 @@ ALTER TABLE `accounting_package_order`
ADD KEY `IDX_CAA1774D4D9866B8` (`bid_id`),
ADD KEY `IDX_CAA1774D919E5513` (`submitter_id`);
--
-- Indexes for table `account_to_sheba_inquiry`
--
ALTER TABLE `account_to_sheba_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_3B99BD82763247D7` (`cache_key`);
--
-- Indexes for table `aiconversation`
--
ALTER TABLE `aiconversation`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_B85427A8A76ED395` (`user_id`),
ADD KEY `IDX_B85427A8A89DB457` (`business_id`);
--
-- Indexes for table `aimessage`
--
ALTER TABLE `aimessage`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_4964D5D89AC0396` (`conversation_id`);
--
-- Indexes for table `apitoken`
--
@ -1627,6 +1877,13 @@ ALTER TABLE `archive_orders`
ADD KEY `IDX_182AE9FB4D9866B8` (`bid_id`),
ADD KEY `IDX_182AE9FB919E5513` (`submitter_id`);
--
-- Indexes for table `back_built_module`
--
ALTER TABLE `back_built_module`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_DB9B83EB919E5513` (`submitter_id`);
--
-- Indexes for table `bank_account`
--
@ -1652,6 +1909,13 @@ ALTER TABLE `business_money`
ADD KEY `IDX_C93EF45BA89DB457` (`business_id`),
ADD KEY `IDX_C93EF45BBF29332C` (`money_id`);
--
-- Indexes for table `card_to_sheba_inquiry`
--
ALTER TABLE `card_to_sheba_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_CE18D9D4E4AF4C20` (`card_number`);
--
-- Indexes for table `cashdesk`
--
@ -1675,7 +1939,8 @@ ALTER TABLE `cheque`
ADD KEY `IDX_A0BBFDE9919E5513` (`submitter_id`),
ADD KEY `IDX_A0BBFDE911C8FB41` (`bank_id`),
ADD KEY `IDX_A0BBFDE9217BBB47` (`person_id`),
ADD KEY `IDX_A0BBFDE921B741A9` (`ref_id`);
ADD KEY `IDX_A0BBFDE921B741A9` (`ref_id`),
ADD KEY `IDX_A0BBFDE9BF29332C` (`money_id`);
--
-- Indexes for table `commodity`
@ -1914,6 +2179,56 @@ ALTER TABLE `plugin`
ALTER TABLE `plugin_prodect`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `plugin_taxsettings_keys`
--
ALTER TABLE `plugin_taxsettings_keys`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_165525F4A89DB457` (`business_id`),
ADD KEY `IDX_165525F4A76ED395` (`user_id`),
ADD KEY `IDX_165525F42989F1FD` (`invoice_id`);
--
-- Indexes for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_2874D6C94D9866B8` (`bid_id`),
ADD KEY `IDX_2874D6C9919E5513` (`submitter_id`),
ADD KEY `IDX_2874D6C9217BBB47` (`person_id`),
ADD KEY `IDX_2874D6C9164AF0AA` (`main_doc_id`);
--
-- Indexes for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_B7D2CF60895648BC` (`doc_id`),
ADD KEY `IDX_B7D2CF6074826F51` (`hesabdari_doc_id`);
--
-- Indexes for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_D44A2689A89DB457` (`business_id`),
ADD KEY `IDX_D44A268961220EA6` (`creator_id`),
ADD KEY `IDX_D44A268974826F51` (`hesabdari_doc_id`);
--
-- Indexes for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_E3C87F09895648BC` (`doc_id`),
ADD KEY `IDX_E3C87F09217BBB47` (`person_id`);
--
-- Indexes for table `plug_noghre_order`
--
@ -1944,6 +2259,13 @@ ALTER TABLE `plug_repservice_order`
ALTER TABLE `plug_repservice_order_state`
ADD PRIMARY KEY (`id`);
--
-- Indexes for table `postal_code_inquiry`
--
ALTER TABLE `postal_code_inquiry`
ADD PRIMARY KEY (`id`),
ADD UNIQUE KEY `UNIQ_E5578D40EA98E376` (`postal_code`);
--
-- Indexes for table `pre_invoice_doc`
--
@ -1963,13 +2285,7 @@ ALTER TABLE `pre_invoice_doc`
ALTER TABLE `pre_invoice_item`
ADD PRIMARY KEY (`id`),
ADD KEY `IDX_DD881165B4ACC212` (`commodity_id`),
ADD KEY `IDX_DD881165217BBB47` (`person_id`),
ADD KEY `IDX_DD88116511C8FB41` (`bank_id`),
ADD KEY `IDX_DD881165BA216AA5` (`cashdesk_id`),
ADD KEY `IDX_DD881165B0FDF16E` (`salary_id`),
ADD KEY `IDX_DD8811654D9866B8` (`bid_id`),
ADD KEY `IDX_DD88116540C1FEA7` (`year_id`),
ADD KEY `IDX_DD881165C8FFB95` (`ref_id_id`);
ADD KEY `IDX_DD881165895648BC` (`doc_id`);
--
-- Indexes for table `price_list`
@ -2163,6 +2479,24 @@ ALTER TABLE `year`
ALTER TABLE `accounting_package_order`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=16;
--
-- AUTO_INCREMENT for table `account_to_sheba_inquiry`
--
ALTER TABLE `account_to_sheba_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `aiconversation`
--
ALTER TABLE `aiconversation`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `aimessage`
--
ALTER TABLE `aimessage`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `apitoken`
--
@ -2181,6 +2515,12 @@ ALTER TABLE `archive_file`
ALTER TABLE `archive_orders`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `back_built_module`
--
ALTER TABLE `back_built_module`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `bank_account`
--
@ -2193,6 +2533,12 @@ ALTER TABLE `bank_account`
ALTER TABLE `business`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=2;
--
-- AUTO_INCREMENT for table `card_to_sheba_inquiry`
--
ALTER TABLE `card_to_sheba_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `cashdesk`
--
@ -2367,6 +2713,42 @@ ALTER TABLE `plugin`
ALTER TABLE `plugin_prodect`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- AUTO_INCREMENT for table `plugin_taxsettings_keys`
--
ALTER TABLE `plugin_taxsettings_keys`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `plug_noghre_order`
--
@ -2385,6 +2767,12 @@ ALTER TABLE `plug_repservice_order`
ALTER TABLE `plug_repservice_order_state`
MODIFY `id` int NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7;
--
-- AUTO_INCREMENT for table `postal_code_inquiry`
--
ALTER TABLE `postal_code_inquiry`
MODIFY `id` int NOT NULL AUTO_INCREMENT;
--
-- AUTO_INCREMENT for table `pre_invoice_doc`
--
@ -2552,6 +2940,19 @@ ALTER TABLE `accounting_package_order`
ADD CONSTRAINT `FK_CAA1774D4D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_CAA1774D919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `aiconversation`
--
ALTER TABLE `aiconversation`
ADD CONSTRAINT `FK_B85427A8A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_B85427A8A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `aimessage`
--
ALTER TABLE `aimessage`
ADD CONSTRAINT `FK_4964D5D89AC0396` FOREIGN KEY (`conversation_id`) REFERENCES `aiconversation` (`id`);
--
-- Constraints for table `apitoken`
--
@ -2573,6 +2974,12 @@ ALTER TABLE `archive_orders`
ADD CONSTRAINT `FK_182AE9FB4D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_182AE9FB919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `back_built_module`
--
ALTER TABLE `back_built_module`
ADD CONSTRAINT `FK_DB9B83EB919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `bank_account`
--
@ -2610,7 +3017,8 @@ ALTER TABLE `cheque`
ADD CONSTRAINT `FK_A0BBFDE9217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_A0BBFDE921B741A9` FOREIGN KEY (`ref_id`) REFERENCES `hesabdari_table` (`id`),
ADD CONSTRAINT `FK_A0BBFDE94D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_A0BBFDE9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
ADD CONSTRAINT `FK_A0BBFDE9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_A0BBFDE9BF29332C` FOREIGN KEY (`money_id`) REFERENCES `money` (`id`);
--
-- Constraints for table `commodity`
@ -2669,8 +3077,8 @@ ALTER TABLE `hesabdari_doc`
-- Constraints for table `hesabdari_doc_hesabdari_doc`
--
ALTER TABLE `hesabdari_doc_hesabdari_doc`
ADD CONSTRAINT `FK_BE675746E2A225E5` FOREIGN KEY (`hesabdari_doc_source`) REFERENCES `hesabdari_doc` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `FK_BE675746FB47756A` FOREIGN KEY (`hesabdari_doc_target`) REFERENCES `hesabdari_doc` (`id`) ON DELETE CASCADE;
ADD CONSTRAINT `FK_BE675746E2A225E5` FOREIGN KEY (`hesabdari_doc_source`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_BE675746FB47756A` FOREIGN KEY (`hesabdari_doc_target`) REFERENCES `hesabdari_doc` (`id`);
--
-- Constraints for table `hesabdari_row`
@ -2781,6 +3189,45 @@ ALTER TABLE `plugin`
ADD CONSTRAINT `FK_E96E27944D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_E96E2794919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `plugin_tax_invoice`
--
ALTER TABLE `plugin_tax_invoice`
ADD CONSTRAINT `FK_165525F42989F1FD` FOREIGN KEY (`invoice_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_165525F4A76ED395` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_165525F4A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `plug_ghesta_doc`
--
ALTER TABLE `plug_ghesta_doc`
ADD CONSTRAINT `FK_2874D6C9164AF0AA` FOREIGN KEY (`main_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_2874D6C9217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_2874D6C94D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_2874D6C9919E5513` FOREIGN KEY (`submitter_id`) REFERENCES `user` (`id`);
--
-- Constraints for table `plug_ghesta_item`
--
ALTER TABLE `plug_ghesta_item`
ADD CONSTRAINT `FK_B7D2CF6074826F51` FOREIGN KEY (`hesabdari_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_B7D2CF60895648BC` FOREIGN KEY (`doc_id`) REFERENCES `plug_ghesta_doc` (`id`);
--
-- Constraints for table `plug_hrm_doc`
--
ALTER TABLE `plug_hrm_doc`
ADD CONSTRAINT `FK_D44A268961220EA6` FOREIGN KEY (`creator_id`) REFERENCES `user` (`id`),
ADD CONSTRAINT `FK_D44A268974826F51` FOREIGN KEY (`hesabdari_doc_id`) REFERENCES `hesabdari_doc` (`id`),
ADD CONSTRAINT `FK_D44A2689A89DB457` FOREIGN KEY (`business_id`) REFERENCES `business` (`id`);
--
-- Constraints for table `plug_hrm_doc_item`
--
ALTER TABLE `plug_hrm_doc_item`
ADD CONSTRAINT `FK_E3C87F09217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_E3C87F09895648BC` FOREIGN KEY (`doc_id`) REFERENCES `plug_hrm_doc` (`id`);
--
-- Constraints for table `plug_noghre_order`
--
@ -2819,14 +3266,8 @@ ALTER TABLE `pre_invoice_doc`
-- Constraints for table `pre_invoice_item`
--
ALTER TABLE `pre_invoice_item`
ADD CONSTRAINT `FK_DD88116511C8FB41` FOREIGN KEY (`bank_id`) REFERENCES `bank_account` (`id`),
ADD CONSTRAINT `FK_DD881165217BBB47` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`),
ADD CONSTRAINT `FK_DD88116540C1FEA7` FOREIGN KEY (`year_id`) REFERENCES `year` (`id`),
ADD CONSTRAINT `FK_DD8811654D9866B8` FOREIGN KEY (`bid_id`) REFERENCES `business` (`id`),
ADD CONSTRAINT `FK_DD881165B0FDF16E` FOREIGN KEY (`salary_id`) REFERENCES `salary` (`id`),
ADD CONSTRAINT `FK_DD881165B4ACC212` FOREIGN KEY (`commodity_id`) REFERENCES `commodity` (`id`),
ADD CONSTRAINT `FK_DD881165BA216AA5` FOREIGN KEY (`cashdesk_id`) REFERENCES `cashdesk` (`id`),
ADD CONSTRAINT `FK_DD881165C8FFB95` FOREIGN KEY (`ref_id_id`) REFERENCES `hesabdari_table` (`id`);
ADD CONSTRAINT `FK_DD881165895648BC` FOREIGN KEY (`doc_id`) REFERENCES `pre_invoice_doc` (`id`),
ADD CONSTRAINT `FK_DD881165B4ACC212` FOREIGN KEY (`commodity_id`) REFERENCES `commodity` (`id`);
--
-- Constraints for table `price_list`

View file

@ -8,7 +8,7 @@ doctrine:
naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
auto_mapping: true
controller_resolver:
auto_mapping: true
auto_mapping: false
mappings:
App:
is_bundle: false

View file

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

View file

@ -48,6 +48,7 @@ security:
# Note: Only the *first* access control that matches will be used
access_control:
# - { path: ^/admin, roles: ROLE_ADMIN }
- { path: ^/api/wordpress/plugin/stats, roles: PUBLIC_ACCESS }
- { path: ^/api/acc/*, roles: ROLE_USER }
- { path: ^/hooks/*, roles: ROLE_USER }
- { path: ^/api/app/*, roles: ROLE_USER }

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:
@ -97,6 +101,62 @@ services:
tags: ['twig.extension']
App\Cog\PersonService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Service\AGI\Promps\AccountingDocPromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
App\Service\AGI\Promps\BasePromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$access: '@App\Service\Access'
App\Service\AGI\Promps\PromptService:
arguments:
$entityManager: '@doctrine.orm.entity_manager'
$personPromptService: '@App\Service\AGI\Promps\PersonPromptService'
$basePromptService: '@App\Service\AGI\Promps\BasePromptService'
$inventoryPromptService: '@App\Service\AGI\Promps\InventoryPromptService'
$bankPromptService: '@App\Service\AGI\Promps\BankPromptService'
$accountingDocPromptService: '@App\Service\AGI\Promps\AccountingDocPromptService'
App\Cog\AccountingDocService:
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

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

View file

@ -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,29 @@
<?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 Version20241201000001 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add customCode column to commodity table';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE commodity ADD customCode TINYINT(1) DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE commodity DROP customCode');
}
}

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

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\AccountingDocService as CogAccountingDocService;
class AccountingDocService
{
private EntityManagerInterface $em;
private CogAccountingDocService $cogAccountingDocService;
public function __construct(EntityManagerInterface $em, CogAccountingDocService $cogAccountingDocService)
{
$this->em = $em;
$this->cogAccountingDocService = $cogAccountingDocService;
}
/**
* جست‌وجوی ردیف‌های اسناد حسابداری برای ابزار هوش مصنوعی
*/
public function searchRowsAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogAccountingDocService->searchRows($params, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در جست‌وجوی ردیف‌های اسناد: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Cog\CommodityService as CogCommodityService;
class CommodityService
{
private EntityManagerInterface $em;
private CogCommodityService $cogCommodityService;
public function __construct(EntityManagerInterface $em, CogCommodityService $cogCommodityService)
{
$this->em = $em;
$this->cogCommodityService = $cogCommodityService;
}
/**
* افزودن یا ویرایش کالا برای ابزار هوش مصنوعی
*/
public function addOrUpdateCommodityAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogCommodityService->addOrUpdateCommodity($params, $acc, $code ?? ($params['code'] ?? 0));
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش کالا: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace App\AiTool;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Person;
use App\Service\Explore;
class PersonService
{
private EntityManagerInterface $em;
private \App\Cog\PersonService $cogPersonService;
public function __construct(EntityManagerInterface $em, \App\Cog\PersonService $cogPersonService)
{
$this->em = $em;
$this->cogPersonService = $cogPersonService;
}
/**
* دریافت اطلاعات یک شخص بر اساس کد و اطلاعات دسترسی
*/
public function getPersonInfoByCode($code, $acc): array
{
if (!$code) {
return [
'error' => 'کد شخص الزامی است'
];
}
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
// فقط کد را به سرویس Cog پاس بده
return $this->cogPersonService->getPersonInfo($code, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage()
];
}
}
/**
* دریافت لیست اشخاص با فیلتر و صفحه‌بندی برای ابزار هوش مصنوعی
*/
public function getPersonsListAi(array $params, $acc = null): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogPersonService->getPersonsList($params, $acc);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست اشخاص: ' . $e->getMessage()
];
}
}
/**
* افزودن یا ویرایش شخص برای ابزار هوش مصنوعی
*/
public function addOrUpdatePersonAi(array $params, $acc = null, $code = 0): array
{
$acc = $acc ?? ($params['acc'] ?? null);
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
try {
return $this->cogPersonService->addOrUpdatePerson($params, $acc, $code ?? ($params['code'] ?? 0));
} catch (\Exception $e) {
return [
'error' => 'خطا در افزودن/ویرایش شخص: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,270 @@
<?php
namespace App\AiTool;
use App\Cog\TicketService as CogTicketService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
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()
];
}
}
/**
* Get the available tools for ticket management
* @return array
*/
public function getTools(): array
{
$tools = [];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'get_user_tickets',
'description' => 'Retrieve a list of tickets for the current user',
'parameters' => [
'type' => 'object',
'properties' => [
'user' => [
'type' => 'object',
'description' => 'The user object'
]
],
'required' => ['user']
]
]
];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'create_or_update_ticket',
'description' => 'Create a new ticket or update an existing one',
'parameters' => [
'type' => 'object',
'properties' => [
'params' => [
'type' => 'object',
'description' => 'The ticket parameters',
'properties' => [
'subject' => [
'type' => 'string',
'description' => 'The subject/title of the ticket'
],
'message' => [
'type' => 'string',
'description' => 'The ticket message or description'
],
'priority' => [
'type' => 'string',
'description' => 'Ticket priority level',
'enum' => ['low', 'medium', 'high']
],
'department' => [
'type' => 'string',
'description' => 'The department this ticket belongs to',
'enum' => ['technical', 'financial', 'general']
]
],
'required' => ['subject', 'message']
],
'files' => [
'type' => 'array',
'description' => 'Array of file attachments',
'items' => [
'type' => 'object',
'description' => 'File object'
]
],
'user' => [
'type' => 'object',
'description' => 'The user object'
],
'id' => [
'type' => 'string',
'description' => 'Ticket ID (required for updates, omit for new tickets)'
]
],
'required' => ['params', 'files', 'user']
]
]
];
$tools[] = [
'type' => 'function',
'function' => [
'name' => 'get_ticket_details',
'description' => 'Get detailed information about a specific ticket including its conversation history',
'parameters' => [
'type' => 'object',
'properties' => [
'id' => [
'type' => 'string',
'description' => 'The unique identifier of the ticket'
],
'user' => [
'type' => 'object',
'description' => 'The user object'
]
],
'required' => ['id', 'user']
]
]
];
return $tools;
}
/**
* دریافت لیست تیکت‌های کاربر
*/
public function getUserTickets(UserInterface $user): array
{
try {
return $this->cogTicketService->getUserTickets($user);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت لیست تیکت‌ها: ' . $e->getMessage()
];
}
}
/**
* ایجاد یا به‌روزرسانی تیکت
*/
public function createOrUpdateTicket(array $params, array $files, UserInterface $user, string $id = ''): array
{
try {
return $this->cogTicketService->createOrUpdateTicket($params, $files, $user, $id);
} catch (\Exception $e) {
return [
'error' => 'خطا در ایجاد/به‌روزرسانی تیکت: ' . $e->getMessage()
];
}
}
/**
* دریافت جزئیات تیکت و پاسخ‌های آن
*/
public function getTicketDetails(string $id, UserInterface $user): array
{
if (!$id) {
return [
'error' => 'شناسه تیکت الزامی است'
];
}
try {
return $this->cogTicketService->getTicketDetails($id, $user);
} catch (\Exception $e) {
return [
'error' => 'خطا در دریافت جزئیات تیکت: ' . $e->getMessage()
];
}
}
}

View file

@ -0,0 +1,116 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
class AccountingDocService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* جست‌وجوی ردیف‌های اسناد حسابداری بر اساس نوع و شناسه
* @param array $params
* @param array $acc
* @return array
*/
public function searchRows(array $params, array $acc): array
{
$em = $this->entityManager;
$data = [];
if (!isset($params['type'])) {
return ['error' => 'نوع (type) الزامی است'];
}
$roll = '';
if ($params['type'] == 'person')
$roll = 'person';
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'sell_receive')
$roll = 'sell';
elseif ($params['type'] == 'bank')
$roll = 'banks';
elseif ($params['type'] == 'buy_send')
$roll = 'buy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$roll = $params['type'];
// اینجا فرض می‌کنیم acc معتبر است و قبلاً بررسی شده
if ($params['type'] == 'person') {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$person)
return ['error' => 'شخص یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'person' => $person,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'bank') {
$bank = $em->getRepository(\App\Entity\BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$bank)
return ['error' => 'بانک یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'bank' => $bank,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'cashdesk') {
$cashdesk = $em->getRepository(\App\Entity\Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$cashdesk)
return ['error' => 'صندوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'salary') {
$salary = $em->getRepository(\App\Entity\Salary::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$salary)
return ['error' => 'حقوق یافت نشد'];
$data = $em->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'salary' => $salary,
], [
'id' => 'DESC'
]);
} else {
return ['error' => 'نوع پشتیبانی نمی‌شود'];
}
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDoc()->getDateSubmit(),
'date' => $item->getDoc()->getDate(),
'type' => $item->getDoc()->getType(),
'ref' => $item->getRef()->getName(),
'des' => $item->getDes(),
'bs' => $item->getBs(),
'bd' => $item->getBd(),
'code' => $item->getDoc()->getCode(),
'submitter' => $item->getDoc()->getSubmitter()->getFullName()
];
$dataTemp[] = $temp;
}
return $dataTemp;
}
}

View file

@ -0,0 +1,146 @@
<?php
namespace App\Cog;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Commodity;
use App\Entity\CommodityUnit;
use App\Entity\CommodityCat;
use App\Entity\PriceList;
use App\Entity\PriceListDetail;
class CommodityService
{
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* افزودن یا ویرایش کالا/خدمات
* @param array $params
* @param array $acc
* @param int|string $code
* @return array
*/
public function addOrUpdateCommodity(array $params, array $acc, $code = 0): array
{
$em = $this->entityManager;
if (!isset($params['name']) || trim($params['name']) === '')
return ['result' => -1, 'error' => 'نام کالا الزامی است'];
if ($code == 0) {
// افزودن کالای جدید
$data = $em->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
'bid' => $acc['bid']
]);
if (!$data) {
$data = new Commodity();
// بررسی کد سفارشی
if (isset($params['customCode']) && $params['customCode'] === true && isset($params['code'])) {
// بررسی تکراری نبودن کد سفارشی
$existingCommodity = $em->getRepository(Commodity::class)->findOneBy([
'code' => $params['code'],
'bid' => $acc['bid']
]);
if ($existingCommodity) {
return ['result' => 2, 'error' => 'کد کالا تکراری است'];
}
$data->setCode($params['code']);
$data->setCustomCode(true);
} else {
// کد اتوماتیک
$data->setCode((new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'Commodity'));
$data->setCustomCode(false);
}
}
} else {
// ویرایش کالای موجود
$data = $em->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$data)
return ['result' => -2, 'error' => 'کالا یافت نشد'];
// بررسی کد سفارشی در زمان ویرایش
if (isset($params['customCode']) && $params['customCode'] === true && isset($params['code'])) {
// بررسی تکراری نبودن کد سفارشی (به جز خود کالا)
$existingCommodity = $em->getRepository(Commodity::class)->findOneBy([
'code' => $params['code'],
'bid' => $acc['bid']
]);
if ($existingCommodity && $existingCommodity->getId() !== $data->getId()) {
return ['result' => 2, 'error' => 'کد کالا تکراری است'];
}
$data->setCode($params['code']);
$data->setCustomCode(true);
} elseif (isset($params['customCode']) && $params['customCode'] === false) {
// تغییر به کد اتوماتیک
$data->setCode((new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'Commodity'));
$data->setCustomCode(false);
}
}
$unit = null;
if (!isset($params['unit']))
$unit = $em->getRepository(CommodityUnit::class)->findAll()[0];
else
$unit = $em->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]);
if (!$unit)
return ['result' => -3, 'error' => 'واحد کالا یافت نشد'];
$data->setUnit($unit);
$data->setBid($acc['bid']);
$data->setName($params['name']);
$data->setKhadamat($params['khadamat'] ?? false);
$data->setWithoutTax($params['withoutTax'] ?? false);
if (isset($params['des'])) $data->setDes($params['des']);
if (isset($params['priceSell'])) $data->setPriceSell($params['priceSell']);
if (isset($params['priceBuy'])) $data->setPriceBuy($params['priceBuy']);
if (isset($params['commodityCountCheck'])) $data->setCommodityCountCheck($params['commodityCountCheck']);
if (isset($params['barcodes'])) $data->setBarcodes($params['barcodes']);
if (isset($params['taxCode'])) $data->setTaxCode($params['taxCode']);
if (isset($params['taxType'])) $data->setTaxType($params['taxType']);
if (isset($params['taxUnit'])) $data->setTaxUnit($params['taxUnit']);
if (isset($params['minOrderCount'])) $data->setMinOrderCount($params['minOrderCount']);
if (isset($params['speedAccess'])) $data->setSpeedAccess($params['speedAccess']);
if (isset($params['dayLoading'])) $data->setDayLoading($params['dayLoading']);
if (isset($params['orderPoint'])) $data->setOrderPoint($params['orderPoint']);
// دسته‌بندی
if (isset($params['cat']) && $params['cat'] != '') {
$cat = is_array($params['cat']) ? $em->getRepository(CommodityCat::class)->find($params['cat']['id']) : $em->getRepository(CommodityCat::class)->find($params['cat']);
if ($cat && $cat->getBid() == $acc['bid']) {
$data->setCat($cat);
}
}
$em->persist($data);
// قیمت‌ها
if (isset($params['prices'])) {
foreach ($params['prices'] as $item) {
$priceList = $em->getRepository(PriceList::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $item['list']['id']
]);
if ($priceList) {
$detail = $em->getRepository(PriceListDetail::class)->findOneBy([
'list' => $priceList,
'commodity' => $data
]);
if (!$detail) $detail = new PriceListDetail();
$detail->setList($priceList);
$detail->setCommodity($data);
$detail->setPriceSell($item['priceSell']);
$detail->setPriceBuy(0);
$detail->setMoney($acc['money']);
$em->persist($detail);
}
}
}
$em->flush();
return ['Success' => true, 'result' => 1, 'code' => $data->getId()];
}
}

View file

@ -7,7 +7,6 @@ use App\Entity\Person;
use App\Entity\PersonType;
use App\Entity\HesabdariRow;
use App\Service\Explore;
use App\Service\Access;
/**
* سرویس مدیریت اشخاص
@ -17,15 +16,13 @@ use App\Service\Access;
class PersonService
{
private EntityManagerInterface $entityManager;
private Access $access;
/**
* سازنده سرویس
*/
public function __construct(EntityManagerInterface $entityManager, Access $access)
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->access = $access;
}
/**
@ -72,7 +69,281 @@ class PersonService
$response['bs'] = $bs;
$response['bd'] = $bd;
$response['balance'] = $bs - $bd;
return $response;
}
/**
* دریافت لیست اشخاص با فیلتر، جست‌وجو و صفحه‌بندی
*
* @param array $params پارامترهای جست‌وجو و فیلتر
* @param array $acc اطلاعات دسترسی
* @return array
*/
public function getPersonsList(array $params, array $acc): array
{
$page = $params['page'] ?? 1;
$itemsPerPage = $params['itemsPerPage'] ?? 10;
$search = $params['search'] ?? '';
$types = $params['types'] ?? null;
$transactionFilters = $params['transactionFilters'] ?? null;
$sortBy = $params['sortBy'] ?? null;
$queryBuilder = $this->entityManager->getRepository(Person::class)
->createQueryBuilder('p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
if (!empty($search) || $search === '0') {
$search = trim($search);
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search')
->setParameter('search', "%$search%");
}
if ($types && !empty($types)) {
$queryBuilder->leftJoin('p.type', 't')
->andWhere('t.code IN (:types)')
->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();
$response = [];
foreach ($persons as $person) {
$rows = $this->entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0;
$bd = 0;
foreach ($rows as $row) {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs();
$bd += (float) $row->getBd();
}
}
$balance = $bs - $bd;
$include = true;
if ($transactionFilters && !empty($transactionFilters)) {
$include = false;
if (in_array('debtors', $transactionFilters) && $balance < 0) {
$include = true;
}
if (in_array('creditors', $transactionFilters) && $balance > 0) {
$include = true;
}
if (in_array('zero', $transactionFilters) && $balance == 0) {
$include = true;
}
}
if ($include) {
$result = Explore::ExplorePerson($person, $this->entityManager->getRepository(PersonType::class)->findAll());
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $balance;
$response[] = $result;
}
}
// اگر سورت روی فیلدهای محاسبه‌شده است، اینجا سورت کن
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' => $response,
'total' => $filteredTotal,
'unfilteredTotal' => $totalItems,
];
}
/**
* افزودن یا ویرایش شخص
* @param array $params
* @param array $acc
* @param int|string $code
* @return array
*/
public function addOrUpdatePerson(array $params, array $acc, $code = 0): array
{
$em = $this->entityManager;
if (!isset($params['nikename']) || trim($params['nikename']) === '')
return ['result' => -1, 'error' => 'نام مستعار الزامی است'];
if ($code == 0) {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'nikename' => $params['nikename'],
'bid' => $acc['bid']
]);
if (!$person) {
$person = new \App\Entity\Person();
$maxAttempts = 10;
$newCode = null;
for ($i = 0; $i < $maxAttempts; $i++) {
$newCode = $params['code'] ?? $code;
if (!$newCode || $newCode == 0) {
$newCode = (new \App\Service\Provider($em))->getAccountingCode($acc['bid'], 'person');
}
$exist = $em->getRepository(\App\Entity\Person::class)->findOneBy(['code' => $newCode]);
if (!$exist) break;
}
if ($newCode === null) return ['result' => -2, 'error' => 'کد جدید تولید نشد'];
$person->setCode($newCode);
}
} else {
$person = $em->getRepository(\App\Entity\Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$person) return ['result' => -3, 'error' => 'شخص یافت نشد'];
}
$person->setBid($acc['bid']);
$person->setNikename($params['nikename']);
if (isset($params['name'])) $person->setName($params['name']);
if (isset($params['birthday'])) $person->setBirthday($params['birthday']);
if (isset($params['tel'])) $person->setTel($params['tel']);
if (isset($params['speedAccess'])) $person->setSpeedAccess($params['speedAccess']);
if (isset($params['address'])) $person->setAddress($params['address']);
if (isset($params['des'])) $person->setDes($params['des']);
if (isset($params['mobile'])) $person->setMobile($params['mobile']);
if (isset($params['mobile2'])) $person->setMobile2($params['mobile2']);
if (isset($params['fax'])) $person->setFax($params['fax']);
if (isset($params['website'])) $person->setWebsite($params['website']);
if (isset($params['email'])) $person->setEmail($params['email']);
if (isset($params['postalcode'])) $person->setPostalcode($params['postalcode']);
if (isset($params['shahr'])) $person->setShahr($params['shahr']);
if (isset($params['ostan'])) $person->setOstan($params['ostan']);
if (isset($params['keshvar'])) $person->setKeshvar($params['keshvar']);
if (isset($params['sabt'])) $person->setSabt($params['sabt']);
if (isset($params['codeeghtesadi'])) $person->setCodeeghtesadi($params['codeeghtesadi']);
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
if (isset($params['company'])) $person->setCompany($params['company']);
if (isset($params['tags'])) $person->setTags($params['tags']);
if (array_key_exists('prelabel', $params)) {
if ($params['prelabel'] != '') {
$prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]);
if ($prelabel) $person->setPrelabel($prelabel);
} elseif ($params['prelabel'] == null) {
$person->setPrelabel(null);
}
}
// کارت‌ها
if (isset($params['accounts'])) {
foreach ($params['accounts'] as $item) {
$card = $em->getRepository(\App\Entity\PersonCard::class)->findOneBy([
'bid' => $acc['bid'],
'person' => $person,
'bank' => $item['bank']
]);
if (!$card) $card = new \App\Entity\PersonCard();
$card->setPerson($person);
$card->setBid($acc['bid']);
$card->setShabaNum($item['shabaNum']);
$card->setCardNum($item['cardNum']);
$card->setAccountNum($item['accountNum']);
$card->setBank($item['bank']);
$em->persist($card);
}
// حذف کارت‌های حذف‌شده
$accounts = $em->getRepository(\App\Entity\PersonCard::class)->findBy([
'bid' => $acc['bid'],
'person' => $person,
]);
foreach ($accounts as $item) {
$deleted = true;
foreach ($params['accounts'] as $param) {
if ($item->getBank() == $param['bank']) $deleted = false;
}
if ($deleted) $em->remove($item);
}
}
// نوع‌ها
if (isset($params['types'])) {
$types = $em->getRepository(\App\Entity\PersonType::class)->findAll();
foreach ($params['types'] as $item) {
$typeEntity = $em->getRepository(\App\Entity\PersonType::class)->findOneBy(['code' => $item['code']]);
if ($item['checked'] == true) $person->addType($typeEntity);
elseif ($item['checked'] == false) $person->removeType($typeEntity);
}
}
$em->persist($person);
$em->flush();
return ['Success' => true, 'result' => 1, 'code' => $person->getCode()];
}
}

View file

@ -0,0 +1,193 @@
<?php
namespace App\Cog;
use App\Entity\Business;
use App\Entity\Support;
use App\Service\Explore;
use App\Service\Jdate;
use App\Service\registryMGR;
use App\Service\SMS;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\User\UserInterface;
class TicketService
{
private const ERROR_TICKET_NOT_FOUND = ['error' => 1, 'message' => 'تیکت یافت نشد.'];
private const ERROR_INVALID_PARAMS = ['error' => 999, 'message' => 'تمام موارد لازم را وارد کنید.'];
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly Explore $explore,
private readonly Jdate $jdate,
private readonly registryMGR $registryMGR,
private readonly SMS $sms,
private readonly string $uploadDirectory
) {
}
/**
* Get list of support tickets for a user
*/
public function getUserTickets(UserInterface $user): array
{
$items = $this->entityManager->getRepository(Support::class)->findBy(
['submitter' => $user, 'main' => 0],
['id' => 'DESC']
);
return array_map(function ($item) use ($user) {
return $this->explore->ExploreSupportTicket($item, $user);
}, $items);
}
/**
* Create or update a support ticket
*/
public function createOrUpdateTicket(array $params, array $files, UserInterface $user, string $id = ''): array
{
if ($id === '') {
return $this->createNewTicket($params, $files, $user);
}
return $this->replyToTicket($params, $files, $user, $id);
}
private function createNewTicket(array $params, array $files, UserInterface $user): array
{
if (!isset($params['title'], $params['body'])) {
return self::ERROR_INVALID_PARAMS;
}
$item = new Support();
$item->setBody($params['body'])
->setTitle($params['title'])
->setDateSubmit(time())
->setSubmitter($user)
->setMain(0)
->setCode($this->generateRandomString(8))
->setState('در حال پیگیری');
// چک کردن مالکیت کسب‌وکار
$this->handleBusinessOwnership($item, $params['bid'] ?? null, $user);
$this->entityManager->persist($item);
$this->entityManager->flush();
$fileName = $this->handleFileUpload($files, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
$this->entityManager->persist($item);
$this->entityManager->flush();
}
$this->sms->send([$item->getId()], $this->registryMGR->get('sms', 'ticketRec'), $this->registryMGR->get('ticket', 'managerMobile'));
return [
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
];
}
private function replyToTicket(array $params, array $files, UserInterface $user, string $id): array
{
if (!isset($params['body'])) {
return self::ERROR_INVALID_PARAMS;
}
$upper = $this->getTicket($id);
if (!$upper) {
return self::ERROR_TICKET_NOT_FOUND;
}
$item = new Support();
$item->setMain($upper->getId())
->setBody($params['body'])
->setTitle($upper->getTitle())
->setDateSubmit(time())
->setSubmitter($user)
->setState('در حال پیگیری');
$this->entityManager->persist($item);
$this->entityManager->flush();
$fileName = $this->handleFileUpload($files, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
}
$this->entityManager->persist($item);
$upper->setState('در حال پیگیری');
$this->entityManager->persist($upper);
$this->entityManager->flush();
$this->sms->send([$item->getId()], $this->registryMGR->get('sms', 'ticketRec'), $this->registryMGR->get('ticket', 'managerMobile'));
return [
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
];
}
private function handleBusinessOwnership(Support $support, ?string $businessId, UserInterface $user): void
{
if ($businessId) {
$business = $this->entityManager->getRepository(Business::class)->find($businessId);
if ($business && $business->getOwner() === $user) {
$support->setBid($business);
return;
}
}
$support->setBid(null);
}
private function getTicket(string $id): ?Support
{
return $this->entityManager->getRepository(Support::class)->find($id);
}
private function generateRandomString(int $length = 32): string
{
return substr(str_shuffle(str_repeat('23456789ABCDEFGHJKLMNPQRSTUVWXYZ', ceil($length / 32))), 1, $length);
}
private function handleFileUpload(array $files, int $ticketId): ?string
{
if (!file_exists($this->uploadDirectory)) {
mkdir($this->uploadDirectory, 0777, true);
}
if (!empty($files)) {
$file = $files[0];
$extension = $file->getClientOriginalExtension();
$fileName = $ticketId . '.' . $extension;
$file->move($this->uploadDirectory, $fileName);
return $fileName;
}
return null;
}
/**
* Get ticket details with its replies
*/
public function getTicketDetails(string $id, UserInterface $user): array
{
$ticket = $this->entityManager->getRepository(Support::class)->find($id);
if (!$ticket || $ticket->getSubmitter() !== $user) {
throw new AccessDeniedException('شما اجازه دسترسی به این تیکت را ندارید.');
}
$replies = $this->entityManager->getRepository(Support::class)->findBy(['main' => $ticket->getId()]);
$repliesArray = array_map(fn($reply) => $this->explore->ExploreSupportTicket($reply, $user), $replies);
return [
'item' => $this->explore->ExploreSupportTicket($ticket, $user),
'replays' => $repliesArray
];
}
}

View file

@ -371,64 +371,64 @@ class AdminController extends AbstractController
}
if (array_key_exists('username', $params))
$registryMGR->update('sms', 'username', $params['username']);
$registryMGR->update('sms', 'username', $params['username'] ?? '');
if (array_key_exists('password', $params))
$registryMGR->update('sms', 'password', $params['password']);
$registryMGR->update('sms', 'password', $params['password'] ?? '');
if (array_key_exists('token', $params))
$registryMGR->update('sms', 'token', $params['token']);
$registryMGR->update('sms', 'token', $params['token'] ?? '');
if (array_key_exists('walletpay', $params))
$registryMGR->update('sms', 'walletpay', $params['walletpay']);
$registryMGR->update('sms', 'walletpay', $params['walletpay'] ?? '');
if (array_key_exists('changePassword', $params))
$registryMGR->update('sms', 'changePassword', $params['changePassword']);
$registryMGR->update('sms', 'changePassword', $params['changePassword'] ?? '');
if (array_key_exists('recPassword', $params))
$registryMGR->update('sms', 'recPassword', $params['recPassword']);
$registryMGR->update('sms', 'recPassword', $params['recPassword'] ?? '');
if (array_key_exists('f2a', $params))
$registryMGR->update('sms', 'f2a', $params['f2a']);
$registryMGR->update('sms', 'f2a', $params['f2a'] ?? '');
if (array_key_exists('ticketReplay', $params))
$registryMGR->update('sms', 'ticketReplay', $params['ticketReplay']);
$registryMGR->update('sms', 'ticketReplay', $params['ticketReplay'] ?? '');
if (array_key_exists('ticketRec', $params))
$registryMGR->update('sms', 'ticketRec', $params['ticketRec']);
$registryMGR->update('sms', 'ticketRec', $params['ticketRec'] ?? '');
if (array_key_exists('fromNum', $params))
$registryMGR->update('sms', 'fromNum', $params['fromNum']);
$registryMGR->update('sms', 'fromNum', $params['fromNum'] ?? '');
if (array_key_exists('sharefaktor', $params))
$registryMGR->update('sms', 'sharefaktor', $params['sharefaktor']);
$registryMGR->update('sms', 'sharefaktor', $params['sharefaktor'] ?? '');
if (array_key_exists('plan', $params))
$registryMGR->update('sms', 'plan', $params['plan']);
$registryMGR->update('sms', 'plan', $params['plan'] ?? '');
if (array_key_exists('chequeInput', $params))
$registryMGR->update('sms', 'chequeInput', $params['chequeInput']);
$registryMGR->update('sms', 'chequeInput', $params['chequeInput'] ?? '');
if (array_key_exists('passChequeInput', $params))
$registryMGR->update('sms', 'passChequeInput', $params['passChequeInput']);
$registryMGR->update('sms', 'passChequeInput', $params['passChequeInput'] ?? '');
if (array_key_exists('rejectChequeInput', $params))
$registryMGR->update('sms', 'rejectChequeInput', $params['rejectChequeInput']);
$registryMGR->update('sms', 'rejectChequeInput', $params['rejectChequeInput'] ?? '');
if (array_key_exists('plugRepservice', $params)) {
if (array_key_exists('get', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateGet', $params['plugRepservice']['get']);
$registryMGR->update('sms', 'plugRepserviceStateGet', $params['plugRepservice']['get'] ?? '');
if (array_key_exists('repaired', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateRepaired', $params['plugRepservice']['repaired']);
$registryMGR->update('sms', 'plugRepserviceStateRepaired', $params['plugRepservice']['repaired'] ?? '');
if (array_key_exists('unrepaired', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateUnrepired', $params['plugRepservice']['unrepaired']);
$registryMGR->update('sms', 'plugRepserviceStateUnrepired', $params['plugRepservice']['unrepaired'] ?? '');
if (array_key_exists('getback', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateGetback', $params['plugRepservice']['getback']);
$registryMGR->update('sms', 'plugRepserviceStateGetback', $params['plugRepservice']['getback'] ?? '');
if (array_key_exists('creating', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateCreating', $params['plugRepservice']['creating']);
$registryMGR->update('sms', 'plugRepserviceStateCreating', $params['plugRepservice']['creating'] ?? '');
if (array_key_exists('created', $params['plugRepservice']))
$registryMGR->update('sms', 'plugRepserviceStateCreated', $params['plugRepservice']['created']);
$registryMGR->update('sms', 'plugRepserviceStateCreated', $params['plugRepservice']['created'] ?? '');
}
if (array_key_exists('plugAccpro', $params)) {
if (array_key_exists('sharefaktor', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproSharefaktor', $params['plugAccpro']['sharefaktor']);
$registryMGR->update('sms', 'plugAccproSharefaktor', $params['plugAccpro']['sharefaktor'] ?? '');
if (array_key_exists('storeroomSmsBarbari', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproStoreroomSmsBarbari', $params['plugAccpro']['storeroomSmsBarbari']);
$registryMGR->update('sms', 'plugAccproStoreroomSmsBarbari', $params['plugAccpro']['storeroomSmsBarbari'] ?? '');
if (array_key_exists('storeroomSmsOther', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproStoreroomSmsOther', $params['plugAccpro']['storeroomSmsOther']);
$registryMGR->update('sms', 'plugAccproStoreroomSmsOther', $params['plugAccpro']['storeroomSmsOther'] ?? '');
if (array_key_exists('chequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproChequeInput', $params['plugAccpro']['chequeInput']);
$registryMGR->update('sms', 'plugAccproChequeInput', $params['plugAccpro']['chequeInput'] ?? '');
if (array_key_exists('passChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproPassChequeInput', $params['plugAccpro']['passChequeInput']);
$registryMGR->update('sms', 'plugAccproPassChequeInput', $params['plugAccpro']['passChequeInput'] ?? '');
if (array_key_exists('rejectChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput']);
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput'] ?? '');
}
return $this->json(JsonResp::success());
@ -469,6 +469,7 @@ class AdminController extends AbstractController
$resp['inputTokenPrice'] = $registryMGR->get('system', key: 'inputTokenPrice');
$resp['outputTokenPrice'] = $registryMGR->get('system', key: 'outputTokenPrice');
$resp['aiPrompt'] = $registryMGR->get('system', key: 'aiPrompt');
$resp['aiDebugMode'] = $registryMGR->get('system', key: 'aiDebugMode');
return $this->json($resp);
}
@ -486,41 +487,43 @@ class AdminController extends AbstractController
$item->setSiteKeywords($params['keywords']);
$item->setDiscription($params['description']);
$item->setScripts($params['scripts']);
$registryMGR->update('system', 'zarinpalKey', $params['zarinpal']);
$registryMGR->update('system', 'zarinpalKey', $params['zarinpal'] ?? '');
$item->setFooterScripts($params['footerScripts']);
$item->setAppSite($params['appSite']);
$item->setFooter($params['footer']);
$registryMGR->update('system', 'activeGateway', $params['activeGateway']);
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']);
$registryMGR->update('system', 'paypingKey', $params['paypingKey']);
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey']);
$registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel']);
$registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey']);
$registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress']);
$registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable']);
$registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee']);
$registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba']);
$registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee']);
$registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba']);
$registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee']);
$registryMGR->update('system', 'activeGateway', $params['activeGateway'] ?? '');
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI'] ?? '');
$registryMGR->update('system', 'paypingKey', $params['paypingKey'] ?? '');
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey'] ?? '');
$registryMGR->update('system', 'inquiryPanel', $params['inquiryPanel'] ?? '');
$registryMGR->update('system', 'inquiryZohalAPIKey', $params['inquiryZohalAPIKey'] ?? '');
$registryMGR->update('system', 'enablePostalCodeToAddress', $params['enablePostalCodeToAddress'] ?? '');
$registryMGR->update('system', 'inquiryPanelEnable', $params['inquiryPanelEnable'] ?? '');
$registryMGR->update('system', 'postalCodeToAddressFee', $params['postalCodeToAddressFee'] ?? '');
$registryMGR->update('system', 'enableCardToSheba', $params['enableCardToSheba'] ?? '');
$registryMGR->update('system', 'cardToShebaFee', $params['cardToShebaFee'] ?? '');
$registryMGR->update('system', 'enableAccountToSheba', $params['enableAccountToSheba'] ?? '');
$registryMGR->update('system', 'accountToShebaFee', $params['accountToShebaFee'] ?? '');
// ذخیره تنظیمات جادوگر هوش مصنوعی
if (array_key_exists('aiEnabled', $params))
$registryMGR->update('system', 'aiEnabled', $params['aiEnabled']);
$registryMGR->update('system', 'aiEnabled', $params['aiEnabled'] ?? '');
if (array_key_exists('aiAgentSource', $params))
$registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource']);
$registryMGR->update('system', 'aiAgentSource', $params['aiAgentSource'] ?? '');
if (array_key_exists('aiModel', $params))
$registryMGR->update('system', 'aiModel', $params['aiModel']);
$registryMGR->update('system', 'aiModel', $params['aiModel'] ?? '');
if (array_key_exists('aiApiKey', $params))
$registryMGR->update('system', 'aiApiKey', $params['aiApiKey']);
$registryMGR->update('system', 'aiApiKey', $params['aiApiKey'] ?? '');
if (array_key_exists('localModelAddress', $params))
$registryMGR->update('system', 'localModelAddress', $params['localModelAddress']);
$registryMGR->update('system', 'localModelAddress', $params['localModelAddress'] ?? '');
if (array_key_exists('inputTokenPrice', $params))
$registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice']);
$registryMGR->update('system', 'inputTokenPrice', $params['inputTokenPrice'] ?? '');
if (array_key_exists('outputTokenPrice', $params))
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice']);
$registryMGR->update('system', 'outputTokenPrice', $params['outputTokenPrice'] ?? '');
if (array_key_exists('aiPrompt', $params))
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt']);
$registryMGR->update('system', 'aiPrompt', $params['aiPrompt'] ?? '');
if (array_key_exists('aiDebugMode', $params))
$registryMGR->update('system', 'aiDebugMode', $params['aiDebugMode'] ?? '');
$entityManager->persist($item);
$entityManager->flush();

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

@ -267,6 +267,7 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
if ($item->isKhadamat()) {
$temp['count'] = 0;
@ -334,6 +335,7 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
if ($item->isKhadamat()) {
$temp['count'] = 0;
@ -429,6 +431,7 @@ class CommodityController extends AbstractController
$temp['taxCode'] = $item->getTaxCode();
$temp['taxType'] = $item->getTaxType();
$temp['taxUnit'] = $item->getTaxUnit();
$temp['customCode'] = $item->isCustomCode();
//calculate count
if ($item->isKhadamat()) {
$temp['count'] = 0;
@ -528,7 +531,8 @@ class CommodityController extends AbstractController
$temp[] = $item->getMinOrderCount();
$temp[] = $item->getDes();
$temp[] = $item->getUnit()->getName();
$temp[] = $item->getCat()->getName();
$cat = $item->getCat();
$temp[] = $cat ? $cat->getName() : '';
$array[] = $temp;
}
$filePath = $provider->createExcellFromArray($array, [
@ -604,20 +608,28 @@ class CommodityController extends AbstractController
return $this->json(['id' => $pid]);
}
#[Route('/api/commodity/info/{code}', name: 'app_commodity_info')]
public function app_commodity_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
#[Route('/api/commodity/info/{id}', name: 'app_commodity_info')]
public function app_commodity_info($id, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
throw $this->createAccessDeniedException();
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
'code' => $id
]);
if (!$data) {
return $this->json(['error' => 'کالا یافت نشد'], 404);
}
$res = Explore::ExploreCommodity($data);
$res['cat'] = '';
if ($data->getCat())
$res['cat'] = $data->getCat()->getId();
$cat = $data->getCat();
if ($cat !== null) {
$res['cat'] = $cat->getId();
}
$res['customCode'] = $data->isCustomCode();
$count = 0;
//calculate count
if ($data->isKhadamat()) {
@ -663,7 +675,7 @@ class CommodityController extends AbstractController
}
}
$res['Success'] = true;
return $this->json($res);
}
@ -679,11 +691,14 @@ class CommodityController extends AbstractController
}
if (!array_key_exists('items', $paramsAll))
return $this->json($extractor->paramsNotSend());
$results = [];
$createdItems = [];
foreach ($paramsAll['items'] as $params) {
if (!array_key_exists('name', $params))
return $this->json(['result' => -1]);
if (count_chars(trim($params['name'])) == 0)
return $this->json(['result' => 3]);
$isNew = false;
if ($code == 0) {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
@ -693,6 +708,7 @@ class CommodityController extends AbstractController
if (!$data) {
$data = new Commodity();
$data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'Commodity'));
$isNew = true;
}
} else {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
@ -779,6 +795,13 @@ class CommodityController extends AbstractController
}
}
}
if (array_key_exists('Tag', $params)) {
$tagValue = $params['Tag'];
if (is_string($tagValue)) {
$tagValue = json_decode($tagValue, true);
}
$data->setTags($tagValue);
}
$entityManager->persist($data);
//save prices list
@ -807,14 +830,45 @@ class CommodityController extends AbstractController
}
$entityManager->flush();
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
$createdItems[] = [
'id' => $data->getId(),
'name' => $data->getName(),
'code' => $data->getCode(),
'unit' => $data->getUnit() ? $data->getUnit()->getName() : null,
'khadamat' => $data->isKhadamat(),
'withoutTax' => $data->isWithoutTax(),
'des' => $data->getDes(),
'priceSell' => $data->getPriceSell(),
'priceBuy' => $data->getPriceBuy(),
'commodityCountCheck' => $data->isCommodityCountCheck(),
'barcodes' => $data->getBarcodes(),
'taxCode' => $data->getTaxCode(),
'taxType' => $data->getTaxType(),
'taxUnit' => $data->getTaxUnit(),
'minOrderCount' => $data->getMinOrderCount(),
'speedAccess' => $data->isSpeedAccess(),
'dayLoading' => $data->getDayLoading(),
'orderPoint' => $data->getOrderPoint(),
'cat' => $data->getCat() ? $data->getCat()->getId() : null,
'tags' => $data->getTags(),
];
}
if (isset($paramsAll['reqType']) && $paramsAll['reqType'] === 'woocommercePlugin') {
return $this->json([
'Success' => 1,
'result' => 1,
'createdItems' => $createdItems
]);
} else {
return $this->json([
'Success' => 1,
'result' => 1
]);
}
return $this->json([
'Success' => true,
'result' => 1,
]);
}
#[Route('/api/commodity/mod/{code}', name: 'app_commodity_mod')]
public function app_commodity_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
#[Route('/api/commodity/mod/{id}', name: 'app_commodity_mod')]
public function app_commodity_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $id = 0): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
@ -823,138 +877,13 @@ class CommodityController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('name', $params))
return $this->json(['result' => -1]);
if (count_chars(trim($params['name'])) == 0)
return $this->json(['result' => 3]);
if ($code == 0) {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'name' => $params['name'],
'bid' => $acc['bid']
]);
//check exist before
if (!$data) {
$data = new Commodity();
$data->setCode($provider->getAccountingCode($request->headers->get('activeBid'), 'Commodity'));
}
} else {
$data = $entityManager->getRepository(Commodity::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$data)
throw $this->createNotFoundException();
$commodityService = new \App\Cog\CommodityService($entityManager);
$result = $commodityService->addOrUpdateCommodity($params, $acc, $id);
if (isset($result['error'])) {
return $this->json($result, 400);
}
if (!array_key_exists('unit', $params))
$unit = $entityManager->getRepository(CommodityUnit::class)->findAll()[0];
else
$unit = $entityManager->getRepository(CommodityUnit::class)->findOneBy(['name' => $params['unit']]);
if (!$unit)
throw $this->createNotFoundException('unit not fount!');
$data->setUnit($unit);
$data->setBid($acc['bid']);
$data->setname($params['name']);
if ($params['khadamat'] == 'true')
$data->setKhadamat(true);
else
$data->setKhadamat(false);
if (!array_key_exists('withoutTax', $params))
$data->setWithoutTax(false);
else {
if ($params['withoutTax'] == 'true')
$data->setWithoutTax(true);
else
$data->setWithoutTax(false);
}
if (array_key_exists('des', $params))
$data->setDes($params['des']);
if (array_key_exists('priceSell', $params))
$data->setPriceSell($params['priceSell']);
if (array_key_exists('priceBuy', $params))
$data->setPriceBuy($params['priceBuy']);
if (array_key_exists('commodityCountCheck', $params)) {
$data->setCommodityCountCheck($params['commodityCountCheck']);
}
if (array_key_exists('barcodes', $params)) {
$data->setBarcodes($params['barcodes']);
}
if (array_key_exists('taxCode', $params)) {
$data->setTaxCode($params['taxCode']);
}
if (array_key_exists('taxType', $params)) {
$data->setTaxType($params['taxType']);
}
if (array_key_exists('taxUnit', $params)) {
$data->setTaxUnit($params['taxUnit']);
}
if (array_key_exists('minOrderCount', $params)) {
$data->setMinOrderCount($params['minOrderCount']);
}
if (array_key_exists('speedAccess', $params)) {
$data->setSpeedAccess($params['speedAccess']);
}
if (array_key_exists('dayLoading', $params)) {
$data->setDayLoading($params['dayLoading']);
}
if (array_key_exists('orderPoint', $params)) {
$data->setOrderPoint($params['orderPoint']);
}
//set cat
if (array_key_exists('cat', $params)) {
if ($params['cat'] != '') {
if (is_int($params['cat']))
$cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']);
else
$cat = $entityManager->getRepository(CommodityCat::class)->find($params['cat']['id']);
if ($cat) {
if ($cat->getBid() == $acc['bid']) {
$data->setCat($cat);
}
}
}
}
$entityManager->persist($data);
//save prices list
if (array_key_exists('prices', $params)) {
foreach ($params['prices'] as $item) {
$priceList = $entityManager->getRepository(PriceList::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $item['list']['id']
]);
if ($priceList) {
$detail = $entityManager->getRepository(PriceListDetail::class)->findOneBy([
'list' => $priceList,
'commodity' => $data
]);
if (!$detail) {
$detail = new PriceListDetail;
}
$detail->setList($priceList);
$detail->setCommodity($data);
$detail->setPriceSell($item['priceSell']);
$detail->setPriceBuy(0);
$detail->setMoney($acc['money']);
$entityManager->persist($detail);
}
}
}
$entityManager->flush();
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
return $this->json([
'Success' => true,
'result' => 1,
'code' => $data->getId()
]);
return $this->json($result);
}
#[Route('/api/commodity/units', name: 'app_commodity_units')]

View file

@ -188,7 +188,7 @@ class HesabdariController extends AbstractController
HesabdariTableRepository $hesabdariTableRepository,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('acc');
$acc = $access->hasRole('accounting');
if (!$acc) {
throw $this->createAccessDeniedException();
}
@ -848,99 +848,15 @@ class HesabdariController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('type', $params))
$this->createNotFoundException();
$roll = '';
if ($params['type'] == 'person')
$roll = 'person';
if ($params['type'] == 'person_receive' || $params['type'] == 'person_send')
$roll = 'person';
elseif ($params['type'] == 'sell_receive')
$roll = 'sell';
elseif ($params['type'] == 'bank')
$roll = 'banks';
elseif ($params['type'] == 'buy_send')
$roll = 'buy';
elseif ($params['type'] == 'transfer')
$roll = 'bankTransfer';
elseif ($params['type'] == 'all')
$roll = 'accounting';
else
$roll = $params['type'];
$acc = $access->hasRole($roll);
$acc = $access->hasRole($params['type'] ?? 'accounting');
if (!$acc)
throw $this->createAccessDeniedException();
if ($params['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$person)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'person' => $person,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$bank)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $bank,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$cashdesk)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk,
], [
'id' => 'DESC'
]);
} elseif ($params['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['id'],
]);
if (!$salary)
throw $this->createNotFoundException();
$data = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $salary,
], [
'id' => 'DESC'
]);
$service = new \App\Cog\AccountingDocService($entityManager);
$result = $service->searchRows($params, $acc);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$dataTemp = [];
foreach ($data as $item) {
$temp = [
'id' => $item->getId(),
'dateSubmit' => $item->getDoc()->getDateSubmit(),
'date' => $item->getDoc()->getDate(),
'type' => $item->getDoc()->getType(),
'ref' => $item->getRef()->getName(),
'des' => $item->getDes(),
'bs' => $item->getBs(),
'bd' => $item->getBd(),
'code' => $item->getDoc()->getCode(),
'submitter' => $item->getDoc()->getSubmitter()->getFullName()
];
$dataTemp[] = $temp;
}
return $this->json($dataTemp);
return $this->json($result);
}
#[Route('/api/accounting/table/get', name: 'app_accounting_table_get')]
@ -966,12 +882,14 @@ class HesabdariController extends AbstractController
$temp[$node->getCode()] = [
'text' => $node->getName(),
'id' => $node->getCode() ?? $node->getId(),
'type' => $node->getType(),
'children' => $this->getFilteredChildsLabel($entityManager, $node, $business),
];
} else {
$temp[$node->getCode()] = [
'text' => $node->getName(),
'id' => $node->getCode() ?? $node->getId(),
'type' => $node->getType(),
];
}
$temp[$node->getCode()]['is_public'] = $nodeBid === null;
@ -1117,6 +1035,13 @@ class HesabdariController extends AbstractController
return $this->json(['result' => 0, 'message' => 'نام ردیف حساب و آیدی والد الزامی است'], 400);
}
// بررسی نوع تفضیل حساب
$allowedTypes = ['calc', 'person', 'commodity', 'bank', 'salary', 'cashdesk'];
$accountType = $params['accountType'] ?? 'calc';
if (!in_array($accountType, $allowedTypes)) {
return $this->json(['result' => 0, 'message' => 'نوع تفضیل حساب نامعتبر است'], 400);
}
$parentNode = $entityManager->getRepository(HesabdariTable::class)->findOneBy(['code' => $params['parentId']]);
if (!$parentNode) {
return $this->json(['result' => 0, 'message' => 'ردیف حساب والد پیدا نشد'], 404);
@ -1142,18 +1067,19 @@ class HesabdariController extends AbstractController
$newNode->setCode($uniqueCode);
$newNode->setBid($acc['bid']);
$newNode->setUpper($parentNode);
$newNode->setType('calc');
$newNode->setType($accountType);
$entityManager->persist($newNode);
$entityManager->flush();
$log->insert('حسابداری', 'ردیف حساب جدید با کد ' . $newNode->getCode() . ' اضافه شد.', $this->getUser(), $acc['bid']);
$log->insert('حسابداری', 'ردیف حساب جدید با کد ' . $newNode->getCode() . ' و نوع ' . $accountType . ' اضافه شد.', $this->getUser(), $acc['bid']);
return $this->json([
'result' => 1,
'node' => [
'id' => $newNode->getCode(),
'text' => $newNode->getName(),
'type' => $newNode->getType(),
'children' => [],
'is_public' => $newNode->getBid() ? false : true,
]
@ -1194,6 +1120,7 @@ class HesabdariController extends AbstractController
'node' => [
'id' => $node->getCode(),
'text' => $node->getName(),
'type' => $node->getType(),
'children' => $this->getChildsLabel($entityManager, $node),
'is_public' => $node->getBid() ? false : true,
]

View file

@ -20,7 +20,7 @@ use App\Entity\Permission;
use App\Entity\BankAccount;
use App\Entity\CommodityCat;
use App\Entity\HesabdariDoc;
use App\Cog\PersonService;
use App\Entity\HesabdariRow;
use App\Entity\CommodityUnit;
use Doctrine\ORM\EntityManagerInterface;
@ -71,6 +71,68 @@ class HookController extends AbstractController
]);
}
#[Route('/hooks/modify/person', name: 'hook_modify_person')]
public function hook_modify_person(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$personService = new \App\Cog\PersonService($entityManager);
$result = $personService->addOrUpdatePerson($params, $acc, $code);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$log->insert('اشخاص', 'شخص با نام مستعار ' . $params['nikename'] . ' افزوده/ویرایش شد.', $this->getUser(), $acc['bid']);
$person = $personService->getPersonInfo($result['code'], $acc);
$result['person'] = $person;
return $this->json($result);
}
#[Route('/hooks/modify/commodity', name: 'app_modify_commodity')]
public function app_modify_commodity(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $id = 0): JsonResponse
{
$acc = $access->hasRole('commodity');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$commodityService = new \App\Cog\CommodityService($entityManager);
$result = $commodityService->addOrUpdateCommodity($params, $acc, $id);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$log->insert('کالا و خدمات', 'کالا / خدمات با نام ' . $params['name'] . ' افزوده/ویرایش شد.', $this->getUser(), $request->headers->get('activeBid'));
return $this->json($result);
}
#[Route('/hooks/info/person', name: 'hook_info_person')]
public function hook_info_person($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PersonService $personService): JsonResponse
{
$acc = $access->hasRole('person');
if (!$acc)
throw $this->createAccessDeniedException();
$response = $personService->getPersonInfo($code, $acc);
$response['Success'] = true;
return $this->json($response);
}
#[Route('hooks/setting/getCurrency', name: 'api_hooks_getcurrency')]
public function api_hooks_getcurrency(Access $access, Log $log, Request $request, EntityManagerInterface $entityManager): JsonResponse
{
@ -342,4 +404,46 @@ class HookController extends AbstractController
'Result' => $response
]);
}
#[Route('api/wordpress/plugin/stats', name: 'api_wordpress_plugin_stats', methods: ['GET'])]
public function api_wordpress_plugin_stats(): JsonResponse
{
return $this->json([
'Success' => true,
'ErrorCode' => 0,
'ErrorMessage' => '',
'Result' => [
'version' => '1.0.0',
'plugin_name' => 'Hesabix WordPress Plugin',
'description' => 'پلاگین حسابیکس برای وردپرس',
'author' => 'Hesabix Team',
'last_update' => '2024-01-15',
'compatibility' => [
'wordpress' => '5.0+',
'php' => '7.4+'
],
'download_url' => 'https://hesabix.ir/plugins/wordpress-plugin-latest.zip',
'changelog' => [
'1.0.0' => [
'date' => '2024-01-15',
'changes' => [
'انتشار نسخه اولیه',
'اتصال به API حسابیکس',
'مدیریت فاکتورها',
'همگام‌سازی داده‌ها'
]
]
],
'statistics' => [
'total_installations' => 1250,
'active_installations' => 1180,
'total_downloads' => 3500,
'average_rating' => 4.8,
'support_tickets' => 45
]
]
]);
}
}

View file

@ -132,8 +132,13 @@ class PersonsController extends AbstractController
$person->setName($params['name']);
if (array_key_exists('birthday', $params))
$person->setBirthday($params['birthday']);
if (array_key_exists('tel', $params))
$person->setTel($params['tel']);
if (array_key_exists('tel', $params)) {
if (is_string($params['tel']) && strlen(preg_replace('/[^0-9]/', '', $params['tel'])) > 11) {
$person->setTel('');
} else {
$person->setTel($params['tel']);
}
}
if (array_key_exists('speedAccess', $params))
$person->setSpeedAccess($params['speedAccess']);
if (array_key_exists('address', $params))
@ -238,155 +243,36 @@ class PersonsController extends AbstractController
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('nikename', $params))
return $this->json(['result' => -1]);
if (count_chars(trim($params['nikename'])) == 0)
return $this->json(['result' => 3]);
if ($code == 0) {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'nikename' => $params['nikename'],
'bid' => $acc['bid']
]);
//check exist before
if (!$person) {
$person = new Person();
$maxAttempts = 10; // حداکثر تعداد تلاش برای تولید کد جدید
$code = null;
for ($i = 0; $i < $maxAttempts; $i++) {
$code = $provider->getAccountingCode($acc['bid'], 'person');
$exist = $entityManager->getRepository(Person::class)->findOneBy([
'code' => $code
]);
if (!$exist) {
break;
}
}
if ($code === null) {
throw new \Exception('نمی‌توان کد جدیدی برای شخص تولید کرد');
}
$person->setCode($code);
}
} else {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $code
]);
if (!$person)
throw $this->createNotFoundException();
$personService = new \App\Cog\PersonService($entityManager);
$result = $personService->addOrUpdatePerson($params, $acc, $code);
if (isset($result['error'])) {
return $this->json($result, 400);
}
$person->setBid($acc['bid']);
$person->setNikename($params['nikename']);
if (array_key_exists('name', $params))
$person->setName($params['name']);
if (array_key_exists('birthday', $params))
$person->setBirthday($params['birthday']);
if (array_key_exists('tel', $params))
$person->setTel($params['tel']);
if (array_key_exists('speedAccess', $params))
$person->setSpeedAccess($params['speedAccess']);
if (array_key_exists('address', $params))
$person->setAddress($params['address']);
if (array_key_exists('des', $params))
$person->setDes($params['des']);
if (array_key_exists('mobile', $params))
$person->setMobile($params['mobile']);
if (array_key_exists('mobile2', $params))
$person->setMobile2($params['mobile2']);
if (array_key_exists('fax', $params))
$person->setFax($params['fax']);
if (array_key_exists('website', $params))
$person->setWebsite($params['website']);
if (array_key_exists('email', $params))
$person->setEmail($params['email']);
if (array_key_exists('postalcode', $params))
$person->setPostalcode($params['postalcode']);
if (array_key_exists('shahr', $params))
$person->setShahr($params['shahr']);
if (array_key_exists('ostan', $params))
$person->setOstan($params['ostan']);
if (array_key_exists('keshvar', $params))
$person->setKeshvar($params['keshvar']);
if (array_key_exists('sabt', $params))
$person->setSabt($params['sabt']);
if (array_key_exists('codeeghtesadi', $params))
$person->setCodeeghtesadi($params['codeeghtesadi']);
if (array_key_exists('shenasemeli', $params))
$person->setShenasemeli($params['shenasemeli']);
if (array_key_exists('company', $params))
$person->setCompany($params['company']);
if (array_key_exists('prelabel', $params)) {
if ($params['prelabel'] != '') {
$prelabel = $entityManager->getRepository(PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]);
if ($prelabel) {
$person->setPrelabel($prelabel);
}
}
elseif ($params['prelabel'] == null) {
$person->setPrelabel(null);
}
}
//inset cards
if (array_key_exists('accounts', $params)) {
foreach ($params['accounts'] as $item) {
$card = $entityManager->getRepository(PersonCard::class)->findOneBy([
'bid' => $acc['bid'],
'person' => $person,
'bank' => $item['bank']
]);
if (!$card)
$card = new PersonCard();
$card->setPerson($person);
$card->setBid($acc['bid']);
$card->setShabaNum($item['shabaNum']);
$card->setCardNum($item['cardNum']);
$card->setAccountNum($item['accountNum']);
$card->setBank($item['bank']);
$entityManager->persist($card);
}
}
//remove not sended accounts
$accounts = $entityManager->getRepository(PersonCard::class)->findBy([
'bid' => $acc['bid'],
'person' => $person,
]);
foreach ($accounts as $item) {
$deleted = true;
foreach ($params['accounts'] as $param) {
if ($item->getBank() == $param['bank']) {
$deleted = false;
}
}
if ($deleted) {
$entityManager->remove($item);
}
}
$entityManager->persist($person);
//insert new types
$types = $entityManager->getRepository(PersonType::class)->findAll();
foreach ($params['types'] as $item) {
if ($item['checked'] == true)
$person->addType($entityManager->getRepository(PersonType::class)->findOneBy([
'code' => $item['code']
]));
elseif ($item['checked'] == false) {
$person->removeType($entityManager->getRepository(PersonType::class)->findOneBy([
'code' => $item['code']
]));
}
}
$entityManager->flush();
$log->insert('اشخاص', 'شخص با نام مستعار ' . $params['nikename'] . ' افزوده/ویرایش شد.', $this->getUser(), $acc['bid']);
return $this->json([
'Success' => true,
'result' => 1,
]);
return $this->json($result);
}
#[Route('/api/person/list', name: 'app_persons_list', methods: ['POST'])]
public function app_persons_list(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('person');
if (!$acc) {
var_dump($acc);
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
// استفاده از سرویس جدید
$personService = new \App\Cog\PersonService($entityManager);
$result = $personService->getPersonsList($params, $acc);
return new JsonResponse($result);
}
#[Route('/api/person/list/search', name: 'app_persons_list_search')]
@ -478,115 +364,6 @@ class PersonsController extends AbstractController
return $this->json($response);
}
#[Route('/api/person/list', name: 'app_persons_list', methods: ['POST'])]
public function app_persons_list(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('person');
if (!$acc) {
var_dump($acc);
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$page = $params['page'] ?? 1;
$itemsPerPage = $params['itemsPerPage'] ?? 10;
$search = $params['search'] ?? '';
$types = $params['types'] ?? null;
$transactionFilters = $params['transactionFilters'] ?? null;
// کوئری اصلی برای گرفتن همه اشخاص
$queryBuilder = $entityManager->getRepository(\App\Entity\Person::class)
->createQueryBuilder('p')
->where('p.bid = :bid')
->setParameter('bid', $acc['bid']);
// جست‌وجو (بهبود داده‌شده)
if (!empty($search) || $search === '0') { // برای اطمینان از کار با "0" یا خالی
$search = trim($search); // حذف فضای خالی اضافی
$queryBuilder->andWhere('p.nikename LIKE :search OR p.name LIKE :search OR p.code LIKE :search OR p.mobile LIKE :search')
->setParameter('search', "%$search%");
}
// فیلتر نوع اشخاص
if ($types && !empty($types)) {
$queryBuilder->leftJoin('p.type', 't')
->andWhere('t.code IN (:types)')
->setParameter('types', $types);
}
// تعداد کل (قبل از فیلتر تراکنش‌ها)
$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 = $entityManager->getRepository(\App\Entity\HesabdariRow::class)->findBy([
'person' => $person,
'bid' => $acc['bid']
]);
$bs = 0; // بستانکار
$bd = 0; // بدهکار
foreach ($rows as $row) {
$doc = $row->getDoc();
if ($doc && $doc->getMoney() && $doc->getYear() &&
$doc->getMoney()->getId() == $acc['money']->getId() &&
$doc->getYear()->getId() == $acc['year']->getId()) {
$bs += (float) $row->getBs(); // بستانکار
$bd += (float) $row->getBd(); // بدهکار
}
}
$balance = $bs - $bd; // تراز = بستانکار - بدهکار
// اعمال فیلتر transactionFilters
$include = true;
if ($transactionFilters && !empty($transactionFilters)) {
$include = false;
if (in_array('debtors', $transactionFilters) && $balance < 0) { // بدهکارها (تراز منفی)
$include = true;
}
if (in_array('creditors', $transactionFilters) && $balance > 0) { // بستانکارها (تراز مثبت)
$include = true;
}
if (in_array('zero', $transactionFilters) && $balance == 0) { // تسویه‌شده‌ها
$include = true;
}
}
if ($include) {
$result = Explore::ExplorePerson($person, $entityManager->getRepository(PersonType::class)->findAll());
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $balance;
$response[] = $result;
}
}
// تعداد آیتم‌های فیلترشده
$filteredTotal = count($response);
return new JsonResponse([
'items' => array_slice($response, 0, $itemsPerPage), // فقط تعداد درخواستی
'total' => $filteredTotal, // تعداد کل فیلترشده
'unfilteredTotal' => $totalItems, // تعداد کل بدون فیلتر (اختیاری)
]);
}
#[Route('/api/person/list/debtors/{amount}', name: 'app_persons_list_debtors')]
public function app_persons_list_debtors(string $amount, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{

View file

@ -429,4 +429,65 @@ class PluginController extends AbstractController
return $this->json($result);
}
#[Route('/api/admin/plugins/sync', name: 'api_admin_plugins_sync', methods: ["POST"])]
public function api_admin_plugins_sync(EntityManagerInterface $entityManager): JsonResponse
{
$pluginData = [
[
'name' => 'بسته حسابداری پیشرفته',
'code' => 'accpro',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'accpro.png',
'defaultOn' => null,
],
[
'name' => 'افزونه مدیریت تعمیرگاه(تعمیرکاران)',
'code' => 'repservice',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => 'repservice.jpg',
'defaultOn' => null,
],
[
'name' => 'افزونه فروش اقساطی',
'code' => 'ghesta',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '95000',
'icon' => 'ghesta.png',
'defaultOn' => null,
],
[
'name' => 'سامانه مودیان',
'code' => 'taxsettings',
'timestamp' => '32104000',
'timelabel' => 'یک سال',
'price' => '200000',
'icon' => ' taxplugin.jpg',
'defaultOn' => null,
],
];
$repo = $entityManager->getRepository(PluginProdect::class);
foreach ($pluginData as $data) {
$exists = $repo->findOneBy(['code' => $data['code']]);
if (!$exists) {
$plugin = new PluginProdect();
$plugin->setName($data['name'])
->setCode($data['code'])
->setTimestamp($data['timestamp'])
->setTimelabel($data['timelabel'])
->setPrice($data['price'])
->setIcon($data['icon'])
->setDefaultOn($data['defaultOn']);
$entityManager->persist($plugin);
}
}
$entityManager->flush();
return $this->json(['status' => 'done']);
}
}

View file

@ -1,5 +1,10 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
namespace App\Controller\Plugins;
use App\Service\Access;
@ -14,8 +19,9 @@ use Symfony\Component\Routing\Annotation\Route;
use App\Entity\PluginTaxsettingsKey;
use App\Entity\HesabdariDoc;
use App\Entity\PluginTaxInvoice;
use App\Dto\TaxSettingsDto;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use DateTime;
use DateInterval;
class TaxSettingsController extends AbstractController
{
@ -51,7 +57,7 @@ class TaxSettingsController extends AbstractController
}
#[Route('/api/plugins/tax/settings/save', name: 'plugin_tax_settings_save', methods: ['POST'])]
public function plugin_tax_settings_save(Request $request, registryMGR $registryMGR, Access $access, Log $log, EntityManagerInterface $em): JsonResponse
public function plugin_tax_settings_save(Request $request, registryMGR $registryMGR, Access $access, Log $log, EntityManagerInterface $em, ValidatorInterface $validator): JsonResponse
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
@ -59,6 +65,24 @@ class TaxSettingsController extends AbstractController
}
$params = $request->getPayload()->all();
$dto = new TaxSettingsDto();
$dto->taxMemoryId = $params['taxMemoryId'] ?? '';
$dto->economicCode = $params['economicCode'] ?? '';
$dto->privateKey = $params['privateKey'] ?? '';
$errors = $validator->validate($dto);
if (count($errors) > 0) {
$messages = [];
foreach ($errors as $error) {
$messages[$error->getPropertyPath()] = $error->getMessage();
}
return $this->json([
'success' => false,
'errors' => $messages,
'message' => 'اطلاعات وارد شده معتبر نیست.'
], 422);
}
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
@ -69,12 +93,12 @@ class TaxSettingsController extends AbstractController
$entity = new PluginTaxsettingsKey();
$entity->setBusinessId($businessId);
$entity->setUserId($userId);
$entity->setCreatedAt(new DateTime());
$entity->setCreatedAt(new \DateTime());
}
$entity->setPrivateKey($params['privateKey'] ?? '');
$entity->setTaxMemoryId($params['taxMemoryId'] ?? null);
$entity->setEconomicCode($params['economicCode'] ?? null);
$entity->setUpdatedAt(new DateTime());
$entity->setPrivateKey($dto->privateKey);
$entity->setTaxMemoryId($dto->taxMemoryId);
$entity->setEconomicCode($dto->economicCode);
$entity->setUpdatedAt(new \DateTime());
$em->persist($entity);
$em->flush();
@ -1138,6 +1162,42 @@ class TaxSettingsController extends AbstractController
$totalTax += $item['tax'];
}
$buyerNationalId = null;
$buyerEconomicCode = null;
$buyerPostalCode = null;
$buyerPerson = null;
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();
break;
}
}
if ($buyerPerson) {
$buyerNationalId = $buyerPerson->getShenasemeli();
$buyerEconomicCode = $buyerPerson->getCodeeghtesadi();
$buyerPostalCode = $buyerPerson->getPostalCode();
if (empty($buyerNationalId) || trim($buyerNationalId) === '') {
$buyerNationalId = null;
}
if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') {
$buyerEconomicCode = null;
}
if (empty($buyerPostalCode) || trim($buyerPostalCode) === '' || count_chars($buyerPostalCode) != 10) {
$buyerPostalCode = null;
}
}
$personType = 1;
if (count_chars($buyerNationalId ) == 11) {
$personType = 2;
}
$dateTime = new DateTime();
$header = (new \SnappMarketPro\Moadian\Dto\InvoiceHeaderDto())
->setTaxid($moadian->generateTaxId($dateTime, $internalId))
@ -1149,11 +1209,11 @@ class TaxSettingsController extends AbstractController
->setInp(1)
->setIns(1)
->setTins($taxId)
->setTob(1)
->setBid(null)
->setTinb(null)
->setTob($personType)
->setBid($buyerNationalId)
->setTinb($buyerEconomicCode)
->setSbc(null)
->setBpc(null)
->setBpc($buyerPostalCode)
->setBbc(null)
->setFt(null)
->setBpn(null)
@ -1464,6 +1524,143 @@ class TaxSettingsController extends AbstractController
}
}
#[Route('/api/plugins/tax/invoice/validate-buyer-info/{id}', name: 'plugin_tax_invoice_validate_buyer_info', methods: ['POST'])]
public function validateBuyerInfo(int $id, Access $access, Log $log, EntityManagerInterface $em): JsonResponse
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
}
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$user = $this->getUser();
$userId = $user instanceof \App\Entity\User ? $user->getId() : null;
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$taxSettings = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
if (!$taxSettings || !$taxSettings->getPrivateKey() || !$taxSettings->getTaxMemoryId()) {
return $this->json([
'success' => false,
'message' => 'تنظیمات مالیاتی تکمیل نشده است. لطفاً ابتدا تنظیمات را تکمیل کنید.'
]);
}
$taxInvoiceRepo = $em->getRepository(PluginTaxInvoice::class);
$taxInvoice = $taxInvoiceRepo->findOneBy([
'id' => $id,
'business' => $businessId
]);
if (!$taxInvoice) {
return $this->json([
'success' => false,
'message' => 'فاکتور مالیاتی یافت نشد.'
]);
}
$invoice = $taxInvoice->getInvoice();
if (!$invoice) {
return $this->json([
'success' => false,
'message' => 'فاکتور یافت نشد.'
]);
}
if ($invoice->getBid()->getId() != $businessId) {
return $this->json([
'success' => false,
'message' => 'فاکتور متعلق به این کسب و کار نیست.'
]);
}
$buyerInfo = $this->validateBuyerEconomicInfo($invoice);
if (!$buyerInfo['is_valid']) {
return $this->json([
'success' => false,
'message' => 'اطلاعات اقتصادی خریدار ناقص است.',
'buyer_info' => $buyerInfo,
'can_proceed' => false
]);
}
return $this->json([
'success' => true,
'message' => 'اطلاعات اقتصادی خریدار کامل است.',
'buyer_info' => $buyerInfo,
'can_proceed' => true
]);
}
/**
* بررسی اطلاعات اقتصادی خریدار
*/
private function validateBuyerEconomicInfo($invoice): array
{
$buyerPerson = null;
$buyerNationalId = null;
$buyerEconomicCode = null;
$missingFields = [];
// دریافت شخص خریدار از ردیف‌های فاکتور
foreach ($invoice->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$buyerPerson = $row->getPerson();
break;
}
}
if (!$buyerPerson) {
return [
'is_valid' => false,
'message' => 'خریدار در فاکتور مشخص نشده است.',
'buyer_name' => null,
'national_id' => null,
'economic_code' => null,
'missing_fields' => ['buyer_not_found']
];
}
$buyerNationalId = $buyerPerson->getShenasemeli();
$buyerEconomicCode = $buyerPerson->getCodeeghtesadi();
// بررسی شناسه ملی
if (empty($buyerNationalId) || trim($buyerNationalId) === '') {
$missingFields[] = 'national_id';
}
// بررسی کد اقتصادی
if (empty($buyerEconomicCode) || trim($buyerEconomicCode) === '') {
$missingFields[] = 'economic_code';
}
$result = [
'is_valid' => empty($missingFields),
'buyer_name' => $buyerPerson->getNikename(),
'buyer_id' => $buyerPerson->getId(),
'buyer_code' => $buyerPerson->getCode(),
'national_id' => $buyerNationalId,
'economic_code' => $buyerEconomicCode,
'missing_fields' => $missingFields
];
if (!empty($missingFields)) {
$missingFieldsText = [];
if (in_array('national_id', $missingFields)) {
$missingFieldsText[] = 'شناسه ملی';
}
if (in_array('economic_code', $missingFields)) {
$missingFieldsText[] = 'کد اقتصادی';
}
$result['message'] = 'اطلاعات اقتصادی خریدار ناقص است. فیلدهای زیر تکمیل نشده‌اند: ' . implode('، ', $missingFieldsText);
} else {
$result['message'] = 'اطلاعات اقتصادی خریدار کامل است.';
}
return $result;
}
}

View file

@ -14,6 +14,9 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
class SalaryController extends AbstractController
{
@ -66,7 +69,21 @@ class SalaryController extends AbstractController
'bid' => $acc['bid'],
'code' => $code
]);
return $this->json(Explore::ExploreSalary($data));
$result = Explore::ExploreSalary($data);
// محاسبه بدهکار و بستانکار و تراز
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$result['bs'] = $bs;
$result['bd'] = $bd;
$result['balance'] = $bd - $bs;
return $this->json($result);
}
#[Route('/api/salary/mod/{code}', name: 'app_salary_mod')]
@ -267,4 +284,135 @@ class SalaryController extends AbstractController
'total' => count($transactions)
]);
}
/**
* خروجی اکسل کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/excel', name: 'app_salary_card_list_excel')]
public function app_salary_card_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|Response
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
} else {
$transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
}
}
}
}
}
$spreadsheet = new Spreadsheet();
$activeWorksheet = $spreadsheet->getActiveSheet();
$arrayEntity = [
[
'شماره تراکنش',
'تاریخ',
'توضیحات',
'تفضیل',
'بستانکار',
'بدهکار',
'سال مالی',
]
];
foreach ($transactions as $transaction) {
$arrayEntity[] = [
$transaction->getId(),
$transaction->getDoc()->getDate(),
$transaction->getDes(),
$transaction->getRef() ? $transaction->getRef()->getName() : '',
$transaction->getBs(),
$transaction->getBd(),
$transaction->getYear() ? $transaction->getYear()->getlabel() : '',
];
}
$activeWorksheet->fromArray($arrayEntity, null, 'A1');
$activeWorksheet->setRightToLeft(true);
$writer = new Xlsx($spreadsheet);
$filePath = __DIR__ . '/../../var/' . uniqid('salary_card_', true) . '.xlsx';
$writer->save($filePath);
return new BinaryFileResponse($filePath);
}
/**
* خروجی PDF کارت حساب تنخواه گردان
*/
#[Route('/api/salary/card/list/print', name: 'app_salary_card_list_print')]
public function app_salary_card_list_print(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('code', $params))
throw $this->createNotFoundException();
$salary = $entityManager->getRepository(Salary::class)->findOneBy(['bid' => $acc['bid'], 'code' => $params['code']]);
if (!$salary)
throw $this->createNotFoundException();
if (!array_key_exists('items', $params)) {
$transactions = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
} else {
$transactions = [];
if (is_array($params['items'])) {
foreach ($params['items'] as $param) {
$id = is_array($param) ? ($param['id'] ?? null) : $param;
if ($id !== null) {
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid'],
'salary' => $salary,
'year' => $acc['year'],
]);
if ($row) {
$transactions[] = $row;
}
}
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/salary_card.html.twig', [
'page_title' => 'کارت حساب تنخواه گردان ' . $salary->getName(),
'bid' => $acc['bid'],
'items' => $transactions,
'salary' => $salary
])
);
return $this->json(['id' => $pid]);
}
}

View file

@ -1236,6 +1236,7 @@ class SellController extends AbstractController
}
return $this->json([
'Success' => 1,
'result' => 1,
'message' => 'فاکتور با موفقیت ثبت شد',
'data' => [

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;
@ -248,139 +249,28 @@ class SupportController extends AbstractController
}
#[Route('/api/support/list', name: 'app_support_list')]
public function app_support_list(Jdate $jdate, EntityManagerInterface $entityManager, Explore $explore): JsonResponse
public function app_support_list(TicketService $ticketService): JsonResponse
{
$items = $entityManager->getRepository(Support::class)->findBy(
['submitter' => $this->getUser(), 'main' => 0],
['id' => 'DESC']
);
// استفاده از Explore برای تبدیل اشیاء به آرایه
$serializedItems = array_map(function ($item) use ($explore, $jdate) {
return $explore->ExploreSupportTicket($item, $this->getUser());
}, $items);
return $this->json($serializedItems);
return $this->json($ticketService->getUserTickets($this->getUser()));
}
#[Route('/api/support/mod/{id}', name: 'app_support_mod')]
public function app_support_mod(
registryMGR $registryMGR,
SMS $SMS,
TicketService $ticketService,
Request $request,
EntityManagerInterface $entityManager,
string $id = ''
): JsonResponse {
$params = $request->getPayload()->all();
$uploadDirectory = $this->getParameter('SupportFilesDir');
if (!file_exists($uploadDirectory)) {
mkdir($uploadDirectory, 0777, true);
}
if ($id === '') {
if (!isset($params['title'], $params['body'])) {
return $this->json(self::ERROR_INVALID_PARAMS);
}
$item = new Support();
$item->setBody($params['body'])
->setTitle($params['title'])
->setDateSubmit(time())
->setSubmitter($this->getUser())
->setMain(0)
->setCode($this->randomString(8))
->setState('در حال پیگیری');
// چک کردن مالکیت کسب‌وکار
$bid = $params['bid'] ?? null;
if ($bid) {
$business = $entityManager->getRepository(Business::class)->find($bid);
if ($business && $business->getOwner() === $this->getUser()) {
$item->setBid($business); // فقط در صورتی که کاربر مالک باشد
} else {
$item->setBid(null); // اگر مالک نباشد، bid خالی می‌ماند
}
} else {
$item->setBid(null); // اگر bid ارسال نشده باشد
}
$entityManager->persist($item);
$entityManager->flush();
$fileName = $this->handleFileUpload($request, $uploadDirectory, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
}
$entityManager->persist($item);
$entityManager->flush();
$SMS->send([$item->getId()], $registryMGR->get('sms', 'ticketRec'), $registryMGR->get('ticket', 'managerMobile'));
return $this->json([
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
]);
}
if (!isset($params['body'])) {
return $this->json(self::ERROR_INVALID_PARAMS);
}
$upper = $this->getTicket($entityManager, $id);
if (!$upper) {
return $this->json(self::ERROR_TICKET_NOT_FOUND);
}
$item = new Support();
$item->setMain($upper->getId())
->setBody($params['body'])
->setTitle($upper->getTitle())
->setDateSubmit(time())
->setSubmitter($this->getUser())
->setState('در حال پیگیری');
$entityManager->persist($item);
$entityManager->flush();
$fileName = $this->handleFileUpload($request, $uploadDirectory, $item->getId());
if ($fileName) {
$item->setFileName($fileName);
}
$entityManager->persist($item);
$upper->setState('در حال پیگیری');
$entityManager->persist($upper);
$entityManager->flush();
$SMS->send([$item->getId()], $registryMGR->get('sms', 'ticketRec'), $registryMGR->get('ticket', 'managerMobile'));
return $this->json([
'error' => 0,
'message' => 'ok',
'url' => $item->getId(),
'files' => $fileName
]);
$files = $request->files->get('files') ?? [];
return $this->json($ticketService->createOrUpdateTicket($params, $files, $this->getUser(), $id));
}
#[Route('/api/support/view/{id}', name: 'app_support_view')]
public function app_support_view(EntityManagerInterface $entityManager, string $id): JsonResponse
public function app_support_view(TicketService $ticketService, string $id): JsonResponse
{
$item = $this->getTicket($entityManager, $id, true);
if (!$item) {
throw $this->createAccessDeniedException();
}
$replays = $entityManager->getRepository(Support::class)->findBy(['main' => $item->getId()]);
$replaysArray = array_map(fn($replay) => Explore::ExploreSupportTicket($replay, $this->getUser()), $replays);
return $this->json([
'item' => Explore::ExploreSupportTicket($item, $this->getUser()),
'replays' => $replaysArray
]);
return $this->json($ticketService->getTicketDetails($id, $this->getUser()));
}
#[Route('/api/support/download/file/{id}', name: 'app_support_download_file')]

View file

@ -236,7 +236,7 @@ final class DatabaseController extends AbstractController
private function updateLastBackupInfo(string $type, string $filename): void
{
$key = $type === 'ftp' ? 'last_ftp_backup' : 'last_backup';
$this->registryMGR->update('system_settings', $key, $filename);
$this->registryMGR->update('system_settings', $key, $filename ?? '');
}
private function createFtpDirectory($ftp, $dir): void

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

@ -82,12 +82,12 @@ final class RegistrySettingsController extends AbstractController
$registryMGR->update($rootSystem, 'can_register', $data['canRegister'] ? '1' : '0');
$registryMGR->update($rootSystem, 'can_free_accounting', $data['canFreeAccounting'] ? '1' : '0');
$registryMGR->update($rootSystem, 'sms_price', (string) $data['smsPrice']);
$registryMGR->update($rootSystem, 'cloud_price_per_gb', (string) $data['cloudPricePerGb']);
$registryMGR->update($rootSystem, 'unlimited_price', (string) $data['unlimitedPrice']);
$registryMGR->update($rootSystem, 'accounting_doc_price', (string) $data['accountingDocPrice']);
$registryMGR->update($rootSystem, 'gift_credit', (string) $data['giftCredit']); // ذخیره فیلد جدید
$registryMGR->update($rootSystem, 'unlimited_duration', json_encode($data['unlimitedDuration']));
$registryMGR->update($rootSystem, 'sms_price', (string) ($data['smsPrice'] ?? 0));
$registryMGR->update($rootSystem, 'cloud_price_per_gb', (string) ($data['cloudPricePerGb'] ?? 0));
$registryMGR->update($rootSystem, 'unlimited_price', (string) ($data['unlimitedPrice'] ?? 0));
$registryMGR->update($rootSystem, 'accounting_doc_price', (string) ($data['accountingDocPrice'] ?? 0));
$registryMGR->update($rootSystem, 'gift_credit', (string) ($data['giftCredit'] ?? 0)); // ذخیره فیلد جدید
$registryMGR->update($rootSystem, 'unlimited_duration', json_encode($data['unlimitedDuration'] ?? []));
$registryMGR->update($rootSystem, 'sms_alert_enabled', $data['smsAlertEnabled'] ? '1' : '0');
$registryMGR->update($rootTicket, 'managerMobile', $data['smsAlertMobile'] ?? '');
$registryMGR->update('system', 'sponsers', $data['sponsorMessage'] ?? '');

View file

@ -29,10 +29,12 @@ class wizardController extends AbstractController
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Log $log
Log $log,
\App\Service\registryMGR $registryMGR
): JsonResponse
{
try {
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json([
@ -45,7 +47,8 @@ class wizardController extends AbstractController
}
// بررسی دسترسی هوش مصنوعی
if (!$acc['ai']) {
$acc = $access->hasRole('ai');
if (!$acc) {
return $this->json([
'success' => false,
'error' => 'شما دسترسی استفاده از هوش مصنوعی را ندارید',
@ -71,6 +74,53 @@ class wizardController extends AbstractController
$options = $params['options'] ?? [];
$conversationId = $params['conversationId'] ?? null;
// بررسی امنیتی conversationId
if ($conversationId) {
$conversation = $entityManager->getRepository(AIConversation::class)->find($conversationId);
if ($conversation) {
// بررسی دسترسی کاربر به این گفتگو
if ($conversation->getUser()->getId() !== $acc['user']->getId() ||
$conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
$log->warning('تلاش غیرمجاز برای دسترسی به گفتگوی دیگران در wizard_talk', [
'conversationId' => $conversationId,
'requestedUser' => $acc['user']->getId(),
'requestedBusiness' => $acc['bid']->getId(),
'conversationUser' => $conversation->getUser()->getId(),
'conversationBusiness' => $conversation->getBusiness()->getId()
]);
return $this->json([
'success' => false,
'error' => 'دسترسی غیرمجاز به گفتگو',
'debug_info' => [
'conversationId' => $conversationId
]
]);
}
// بررسی حذف شدن گفتگو
if ($conversation->isDeleted()) {
$log->info('تلاش برای دسترسی به گفتگوی حذف شده در wizard_talk', [
'conversationId' => $conversationId,
'user' => $acc['user']->getId()
]);
// گفتگوی جدید ایجاد می‌شود
$conversationId = null;
}
} else {
$log->warning('تلاش برای دسترسی به گفتگوی ناموجود در wizard_talk', [
'conversationId' => $conversationId,
'user' => $acc['user']->getId(),
'business' => $acc['bid']->getId()
]);
// گفتگوی جدید ایجاد می‌شود
$conversationId = null;
}
}
// بررسی فعال بودن هوش مصنوعی
$aiStatus = $this->agiService->checkAIServiceStatus();
if (!$aiStatus['isEnabled']) {
@ -96,7 +146,11 @@ class wizardController extends AbstractController
'showChargeButton' => true,
'debug_info' => [
'balance' => $currentBalance,
'required' => $estimatedCost
'required' => $estimatedCost,
'business' => [
'id' => $business->getId(),
'name' => $business->getName(),
]
]
]);
}
@ -104,19 +158,31 @@ class wizardController extends AbstractController
// استفاده از AGIService برای مدیریت گفتگو و ارسال درخواست
$result = $this->agiService->sendRequest($message, $business, $acc['user'], $conversationId, $acc);
// دریافت وضعیت نمایش دیباگ از تنظیمات سیستم
$aiDebugMode = false;
if ($registryMGR) {
$aiDebugMode = $registryMGR->get('system', 'aiDebugMode') === '1' || $registryMGR->get('system', 'aiDebugMode') === true;
}
if ($result['success']) {
$responseContent = $result['response'] ?? $result['message'] ?? 'عملیات با موفقیت انجام شد';
$response = [
'success' => true,
'response' => $responseContent,
'conversationId' => $result['conversation_id'] ?? null,
'conversationId' => $result['conversationId'] ?? $result['conversation_id'] ?? null,
'model' => $result['model'] ?? null,
'usage' => $result['usage'] ?? null,
'cost' => $result['cost'] ?? null,
'debug_info' => $result['debug_info'] ?? null
];
// اگر دیباگ خاموش بود، debug_info و model را حذف کن
if (!$aiDebugMode) {
unset($response['debug_info']);
unset($response['model']);
}
// محاسبه هزینه در صورت وجود اطلاعات usage
if (isset($result['cost'])) {
$cost = $result['cost'];
@ -142,15 +208,27 @@ class wizardController extends AbstractController
}
}
// پاک‌سازی خروجی از اشیای Doctrine (Business, User و ...)
array_walk_recursive($response, function (&$item) {
if (is_object($item) && method_exists($item, 'getId')) {
$item = $item->getId();
}
});
return $this->json($response);
} else {
return $this->json([
$errorResponse = [
'success' => false,
'error' => $result['error'] ?? 'خطای نامشخص در سرویس هوش مصنوعی',
'debug_info' => $result['debug_info'] ?? ['fallback' => 'no debug info from service', 'result' => $result]
]);
];
array_walk_recursive($errorResponse, function (&$item) {
if (is_object($item) && method_exists($item, 'getId')) {
$item = $item->getId();
}
});
return $this->json($errorResponse);
}
try {
} catch (\Exception $e) {
return $this->json([
'success' => false,
@ -199,5 +277,138 @@ class wizardController extends AbstractController
}
}
#[Route('/api/wizard/conversations/list', name: 'wizard_conversations_list', methods: ['POST'])]
public function wizard_conversations_list(Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\Jdate $jdate): JsonResponse
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$params = json_decode($request->getContent(), true) ?? [];
$search = $params['search'] ?? '';
$category = $params['category'] ?? '';
$conversationRepo = $entityManager->getRepository(\App\Entity\AIConversation::class);
if (!empty($search)) {
$conversations = $conversationRepo->searchByTitle($acc['user'], $acc['bid'], $search);
} elseif (!empty($category)) {
$conversations = $conversationRepo->findByCategory($acc['user'], $acc['bid'], $category);
} else {
$conversations = $conversationRepo->findActiveConversations($acc['user'], $acc['bid']);
}
$result = [];
foreach ($conversations as $conversation) {
$messageRepo = $entityManager->getRepository(\App\Entity\AIMessage::class);
$lastMessage = $messageRepo->findLastMessageByConversation($conversation);
$result[] = [
'id' => $conversation->getId(),
'title' => $conversation->getTitle(),
'category' => $conversation->getCategory(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $conversation->getCreatedAt()),
'updatedAt' => $jdate->jdate('Y/m/d H:i', $conversation->getUpdatedAt()),
'messageCount' => count($conversation->getMessages()),
'lastMessage' => $lastMessage ? $lastMessage->getContent() : ''
];
}
return $this->json(['success' => true, 'items' => $result]);
}
#[Route('/api/wizard/conversations/create', name: 'wizard_conversations_create', methods: ['POST'])]
public function wizard_conversations_create(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$params = json_decode($request->getContent(), true) ?? [];
$title = $params['title'] ?? 'گفتگوی جدید';
$category = $params['category'] ?? 'عمومی';
$conversation = new \App\Entity\AIConversation();
$conversation->setUser($acc['user']);
$conversation->setBusiness($acc['bid']);
$conversation->setTitle($title);
$conversation->setCategory($category);
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json(['success' => true, 'id' => $conversation->getId(), 'title' => $conversation->getTitle(), 'category' => $conversation->getCategory(), 'createdAt' => $conversation->getCreatedAt()]);
}
#[Route('/api/wizard/conversations/{id}/delete', name: 'wizard_conversations_delete', methods: ['POST'])]
public function wizard_conversations_delete(int $id, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$conversation = $entityManager->getRepository(\App\Entity\AIConversation::class)->find($id);
if (!$conversation) {
return $this->json(['success' => false, 'error' => 'گفتگو یافت نشد']);
}
if ($conversation->getUser()->getId() !== $acc['user']->getId() || $conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
if ($conversation->isDeleted()) {
return $this->json(['success' => false, 'error' => 'این گفتگو قبلاً حذف شده است']);
}
$conversation->setDeleted(true);
$entityManager->persist($conversation);
$entityManager->flush();
return $this->json(['success' => true]);
}
#[Route('/api/wizard/conversations/{id}/messages', name: 'wizard_conversations_messages', methods: ['POST'])]
public function wizard_conversations_messages(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\Jdate $jdate): JsonResponse
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$conversation = $entityManager->getRepository(\App\Entity\AIConversation::class)->find($id);
if (!$conversation) {
return $this->json(['success' => false, 'error' => 'گفتگو یافت نشد']);
}
if ($conversation->getUser()->getId() !== $acc['user']->getId() || $conversation->getBusiness()->getId() !== $acc['bid']->getId()) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$messageRepo = $entityManager->getRepository(\App\Entity\AIMessage::class);
$messages = $messageRepo->findByConversation($conversation);
$result = [];
foreach ($messages as $message) {
$result[] = [
'id' => $message->getId(),
'role' => $message->getRole(),
'content' => $message->getContent(),
'createdAt' => $jdate->jdate('Y/m/d H:i', $message->getCreatedAt())
];
}
return $this->json(['success' => true, 'items' => $result]);
}
#[Route('/api/wizard/conversations/delete-all', name: 'wizard_conversations_delete_all', methods: ['POST'])]
public function wizard_conversations_delete_all(Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('join');
if (!$acc) {
return $this->json(['success' => false, 'error' => 'دسترسی غیرمجاز']);
}
$userId = $acc['user']->getId();
$businessId = $acc['bid']->getId();
$repo = $entityManager->getRepository(AIConversation::class);
$convs = $repo->createQueryBuilder('c')
->where('c.user = :user')
->andWhere('c.business = :business')
->andWhere('c.deleted = false')
->setParameter('user', $userId)
->setParameter('business', $businessId)
->getQuery()->getResult();
$count = 0;
foreach ($convs as $conv) {
$conv->setDeleted(true);
$entityManager->persist($conv);
$count++;
}
$entityManager->flush();
return $this->json(['success' => true, 'deleted' => $count]);
}
}

View file

@ -0,0 +1,38 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
namespace App\Dto;
use Symfony\Component\Validator\Constraints as Assert;
class TaxSettingsDto
{
#[Assert\NotBlank(message: 'شناسه حافظه مالیاتی الزامی است.')]
#[Assert\Length(
max: 50,
maxMessage: 'شناسه حافظه مالیاتی نباید بیشتر از ۵۰ کاراکتر باشد.'
)]
public string $taxMemoryId;
#[Assert\NotBlank(message: 'کد اقتصادی الزامی است.')]
#[Assert\Regex(
pattern: '/^\d{11}$/',
message: 'کد اقتصادی باید دقیقا ۱۱ رقم باشد.'
)]
public string $economicCode;
#[Assert\NotBlank(message: 'کلید خصوصی الزامی است.')]
#[Assert\Length(
min: 100,
minMessage: 'کلید خصوصی باید حداقل ۱۰۰ کاراکتر باشد.'
)]
#[Assert\Regex(
pattern: '/^-----BEGIN (RSA )?PRIVATE KEY-----[\s\S]+-----END (RSA )?PRIVATE KEY-----$/',
message: 'فرمت کلید خصوصی معتبر نیست.'
)]
public string $privateKey;
}

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,9 +33,12 @@ 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)]
private ?bool $customCode = null;
#[ORM\Column(type: 'string', length: 255, nullable: true)]
private $priceBuy;
@ -101,6 +104,9 @@ class Commodity
#[ORM\OneToMany(mappedBy: 'commodity', targetEntity: PreInvoiceItem::class, orphanRemoval: true)]
private Collection $preInvoiceItems;
#[ORM\Column(type: Types::JSON, nullable: true)]
private ?array $tags = null;
public function __construct()
{
$this->setPriceBuy(0);
@ -178,6 +184,18 @@ class Commodity
return $this;
}
public function isCustomCode(): ?bool
{
return $this->customCode;
}
public function setCustomCode(?bool $customCode): static
{
$this->customCode = $customCode;
return $this;
}
public function getPriceBuy(): ?int
{
return $this->priceBuy;
@ -525,4 +543,15 @@ class Commodity
return $this;
}
public function getTags(): ?array
{
return $this->tags;
}
public function setTags(?array $tags): static
{
$this->tags = $tags;
return $this;
}
}

View file

@ -158,6 +158,9 @@ class Person
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'person', orphanRemoval: true)]
private Collection $PlugGhestaDocs;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $tags = null;
public function __construct()
{
$this->hesabdariRows = new ArrayCollection();
@ -899,4 +902,15 @@ class Person
return $this;
}
public function getTags(): ?string
{
return $this->tags;
}
public function setTags(?string $tags): self
{
$this->tags = $tags;
return $this;
}
}

View file

@ -1,5 +1,10 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
namespace App\Entity;
use App\Repository\PluginTaxInvoiceRepository;

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

@ -1,5 +1,10 @@
<?php
/**
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
namespace App\Repository;
use App\Entity\PluginTaxInvoice;

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;
}
/**
@ -64,15 +79,13 @@ class AGIService
'service_status' => $status,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
try {
// مدیریت گفتگو و تاریخچه
$conversation = $this->manageConversation($conversationId, $business, $user, $message);
$conversationHistory = $this->getConversationHistory($conversation);
@ -80,8 +93,8 @@ class AGIService
// ذخیره پیام کاربر
$this->saveUserMessage($conversation, $message);
// ساخت پرامپ هوشمند
$prompt = $this->buildSmartPrompt($message, $business, $conversationHistory);
// فقط سوال کاربر به عنوان prompt
$userPrompt = $message;
$service = $this->getAIAgentSource();
$apiKey = $this->getAIApiKey($service);
@ -94,18 +107,21 @@ class AGIService
'service' => $service,
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
];
}
// ارسال درخواست با function calling
$result = $this->sendToAIServiceWithFunctionCalling($prompt, $apiKey, $service, $conversationHistory, $acc);
// ارسال درخواست با function calling (فقط سوال کاربر)
$result = $this->sendToAIServiceWithFunctionCalling($userPrompt, $apiKey, $service, $conversationHistory, $acc);
if (!$result['success']) {
// اگر system_prompt در debug_info وجود داشت، به خروجی خطا اضافه کن
if (isset($result['debug_info']['system_prompt'])) {
if (!isset($result['debug_info'])) $result['debug_info'] = [];
$result['debug_info']['system_prompt'] = $result['debug_info']['system_prompt'];
}
return $result;
}
@ -116,31 +132,27 @@ class AGIService
// ذخیره پاسخ هوش مصنوعی
$this->saveAIMessage($conversation, $aiResponse, $result['data'], $cost);
$debugInfo = [
'context' => 'sendRequest',
'function_calls' => $result['function_calls'] ?? [],
'tool_results' => $result['tool_results'] ?? [],
'conversation_history' => $conversationHistory
];
// اگر system_prompt در debug_info خروجی قبلی بود، به debug_info اضافه کن
if (isset($result['debug_info']['system_prompt'])) {
$debugInfo['system_prompt'] = $result['debug_info']['system_prompt'];
}
return [
'success' => true,
'response' => $aiResponse,
'conversationId' => $conversation->getId(),
'conversationId' => $conversation->getId(), // مقداردهی صحیح conversationId
'model' => $this->getAIModel(),
'usage' => $result['data']['usage'] ?? [],
'cost' => $cost,
'debug_info' => [
'context' => 'sendRequest',
'function_calls' => $result['function_calls'] ?? [],
'tool_results' => $result['tool_results'] ?? []
]
];
'debug_info' => $debugInfo
];
try {
} catch (\Exception $e) {
$this->log->error('خطا در ارسال درخواست به هوش مصنوعی: ' . $e->getMessage(), [
'context' => 'AGIService::sendRequest',
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId,
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
return [
'success' => false,
'error' => 'خطا در پردازش درخواست: ' . $e->getMessage(),
@ -149,8 +161,6 @@ class AGIService
'exception' => $e->getMessage(),
'inputs' => [
'message' => $message,
'business' => $business,
'user' => $user,
'conversationId' => $conversationId
]
]
@ -165,34 +175,28 @@ class AGIService
{
$urls = $this->getServiceUrls($service);
$model = $this->getAIModel();
// ساخت messages با تاریخچه
$messages = [];
// اضافه کردن تاریخچه گفتگو
foreach ($conversationHistory as $historyItem) {
$messages[] = [
'role' => $historyItem['role'],
'content' => $historyItem['content']
];
}
// اضافه کردن پیام فعلی
// ساخت پرامپت system با buildSmartPrompt
$systemPrompt = $this->buildSmartPrompt($prompt, $acc['bid'] ?? null, $conversationHistory);
$messages = [
[
'role' => 'system',
'content' => $systemPrompt
]
];
// تاریخچه گفتگو و پیام user دیگر اینجا اضافه نمی‌شود چون در buildSmartPrompt لحاظ شده است
$messages[] = [
'role' => 'user',
'content' => $prompt
];
// تعریف ابزارهای موجود
$tools = $this->buildToolsFromPromptServices();
$data = [
'model' => $model,
'messages' => $messages,
'tools' => $tools,
'tool_choice' => 'auto', // اجازه انتخاب ابزار به مدل
'tool_choice' => 'auto',
'max_tokens' => 12000,
'temperature' => 0.1
'temperature' => 0.9
];
$maxIterations = 5; // حداکثر تعداد تکرار برای جلوگیری از حلقه بی‌نهایت
@ -220,7 +224,13 @@ class AGIService
'context' => 'sendToAIServiceWithFunctionCalling',
'url_list' => $urls,
'data' => $data,
'iteration' => $iteration
'iteration' => $iteration,
'result' => $result,
'apiKey' => $apiKey,
'service' => $service,
'conversationHistory' => $conversationHistory,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
]
];
}
@ -235,7 +245,9 @@ class AGIService
'debug_info' => [
'context' => 'sendToAIServiceWithFunctionCalling',
'response_data' => $responseData,
'iteration' => $iteration
'iteration' => $iteration,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
]
];
}
@ -250,7 +262,11 @@ class AGIService
'success' => true,
'data' => $responseData,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
'tool_results' => $toolResults,
'debug_info' => [
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
]
];
}
@ -300,7 +316,9 @@ class AGIService
'context' => 'sendToAIServiceWithFunctionCalling',
'max_iterations' => $maxIterations,
'function_calls' => $functionCalls,
'tool_results' => $toolResults
'tool_results' => $toolResults,
'system_prompt' => $systemPrompt,
'messages_to_ai' => $messages,
]
];
}
@ -313,90 +331,94 @@ class AGIService
try {
switch ($tool) {
case 'getPersonInfo':
// استفاده مستقیم از سرویس Cog\PersonService
return $this->callGetPersonInfoFromCog($params);
$cogPersonService = new \App\Cog\PersonService($this->em);
$personService = new \App\AiTool\PersonService($this->em, $cogPersonService);
return $personService->getPersonInfoByCode($params['code'] ?? null, $params['acc'] ?? null);
case 'getPersonsList':
$cogPersonService = new \App\Cog\PersonService($this->em);
$personService = new \App\AiTool\PersonService($this->em, $cogPersonService);
return $personService->getPersonsListAi($params, $params['acc'] ?? null);
case 'addOrUpdatePerson':
$cogPersonService = new \App\Cog\PersonService($this->em);
$personService = new \App\AiTool\PersonService($this->em, $cogPersonService);
return $personService->addOrUpdatePersonAi($params, $params['acc'] ?? null, $params['code'] ?? 0);
case 'addOrUpdateCommodity':
$cogCommodityService = new \App\Cog\CommodityService($this->em);
$commodityService = new \App\AiTool\CommodityService($this->em, $cogCommodityService);
return $commodityService->addOrUpdateCommodityAi($params, $params['acc'] ?? null, $params['code'] ?? 0);
case 'searchAccountingRows':
$cogAccountingDocService = new \App\Cog\AccountingDocService($this->em);
$accountingDocService = new \App\AiTool\AccountingDocService($this->em, $cogAccountingDocService);
return $accountingDocService->searchRowsAi($params, $params['acc'] ?? null);
// ابزارهای مربوط به تیکت
case 'getTicketsList':
$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, $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, $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, $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:
return [
'error' => 'ابزار ناشناخته: ' . $tool
];
}
} catch (\Exception $e) {
$this->log->error('خطا در اجرای ابزار: ' . $e->getMessage(), [
'context' => 'AGIService::callTool',
'tool' => $tool,
'params' => $params,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در اجرای ابزار: ' . $e->getMessage()
];
}
}
/**
* اجرای ابزار getPersonInfo با استفاده از سرویس Cog\PersonService
*/
private function callGetPersonInfoFromCog(array $params)
{
$code = $params['code'] ?? null;
if (!$code) {
return [
'error' => 'کد شخص الزامی است'
];
}
try {
// دریافت اطلاعات دسترسی (acc) از context یا پارامترها
$acc = $params['acc'] ?? null;
if (!$acc) {
return [
'error' => 'اطلاعات دسترسی (acc) الزامی است'
];
}
// استفاده از سرویس Cog\PersonService
$personService = new \App\Cog\PersonService($this->em, $this->provider->getAccessService());
$result = $personService->getPersonInfo($code, $acc);
return $result;
} catch (\Exception $e) {
$this->log->error('خطا در دریافت اطلاعات شخص از Cog: ' . $e->getMessage(), [
'context' => 'AGIService::callGetPersonInfoFromCog',
'code' => $code,
'exception' => $e->getMessage()
]);
return [
'error' => 'خطا در دریافت اطلاعات شخص: ' . $e->getMessage()
];
}
}
/**
* ساخت پرامپ هوشمند
*/
private function buildSmartPrompt(string $message, ?Business $business, array $conversationHistory = []): string
{
// دریافت پرامپ‌های پایه از PromptService
$basePrompts = $this->promptService->getAllBasePrompts();
$prompt = $basePrompts;
// دریافت aiPrompt مدیر سیستم و قوانین خروجی
$aiPrompt = $this->registryMGR->get('system', 'aiPrompt');
$outputFormatPrompt = $this->promptService->getOutputFormatPrompt();
$basePrompt = "شما دستیار هوشمند حسابیکس هستید. فقط پاسخ را به صورت JSON مطابق مثال خروجی بده. اگر نیاز به ابزار داشتی، از function calling استفاده کن.";
// قوانین خروجی JSON و مثال‌ها از سرویس مدیریت پرامپت‌ها
$prompt .= $this->promptService->getOutputFormatPrompt();
$parts = [];
// اضافه کردن aiPrompt اگر تکراری نبود
if ($aiPrompt && strpos($basePrompt, trim($aiPrompt)) === false && strpos($outputFormatPrompt, trim($aiPrompt)) === false) {
$parts[] = trim($aiPrompt);
}
// اضافه کردن basePrompt اگر تکراری نبود
if (strpos($outputFormatPrompt, trim($basePrompt)) === false) {
$parts[] = trim($basePrompt);
}
// قوانین خروجی اگر تکراری نبود
$parts[] = trim($outputFormatPrompt);
// اضافه کردن اطلاعات کسب و کار
if ($business) {
$prompt .= "\n\nاطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
$parts[] = "اطلاعات کسب و کار: نام: {$business->getName()}, کد اقتصادی: {$business->getCodeeghtesadi()}.";
}
// اضافه کردن تاریخچه گفتگو
if (!empty($conversationHistory)) {
$prompt .= "\n\n📜 تاریخچه گفتگو:\n";
$historyText = "\n📜 تاریخچه گفتگو:\n";
foreach ($conversationHistory as $historyItem) {
$role = $historyItem['role'] === 'user' ? 'کاربر' : 'دستیار';
$prompt .= "{$role}: {$historyItem['content']}\n";
$historyText .= "{$role}: {$historyItem['content']}\n";
}
$prompt .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید.";
$historyText .= "\n💡 نکته: لطفاً context گفتگو را حفظ کنید و به سوالات قبلی مراجعه کنید.";
$parts[] = $historyText;
}
$prompt .= "\n\nسوال کاربر: " . $message;
return $prompt;
// سوال کاربر
$parts[] = "سوال کاربر: " . $message;
// ادغام نهایی
return implode("\n\n", array_filter($parts));
}
/**
@ -420,9 +442,19 @@ class AGIService
*/
private function makeHttpRequest(string $url, array $data, string $apiKey): array
{
$requestJson = json_encode($data, JSON_UNESCAPED_UNICODE);
$debugInfo = [
'request_url' => $url,
'request_data' => $data,
'request_headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $apiKey,
],
'api_key' => $apiKey,
'model' => $data['model'] ?? null,
'request_json' => $requestJson,
'request_time' => date('Y-m-d H:i:s'),
'request_size_bytes' => strlen($requestJson),
];
try {
@ -440,6 +472,10 @@ class AGIService
$content = $response->getContent(false); // false: throw exception on 4xx/5xx نمی‌دهد
$debugInfo['http_code'] = $statusCode;
$debugInfo['raw_response'] = $content;
$debugInfo['response_headers'] = $response->getHeaders(false);
$debugInfo['response_time'] = date('Y-m-d H:i:s');
$debugInfo['response_size_bytes'] = strlen($content);
$debugInfo['response_sample'] = mb_substr($content, 0, 500, 'UTF-8');
if ($statusCode !== 200) {
$debugInfo['http_error_message'] = $this->getHttpErrorMessage($statusCode);
@ -461,6 +497,7 @@ class AGIService
];
}
$debugInfo['response_data'] = $responseData;
return [
'success' => true,
'data' => $responseData,
@ -629,13 +666,6 @@ class AGIService
return (bool) ($this->registryMGR->get('system', 'aiEnabled') ?? false);
}
/**
* دریافت پرامپ هوش مصنوعی
*/
public function getAIPrompt(): string
{
return $this->registryMGR->get('system', 'aiPrompt') ?? 'شما یک دستیار هوشمند برای سیستم حسابداری هستید.';
}
/**
* مدیریت گفتگو - ایجاد یا بازیابی گفتگوی موجود
@ -645,8 +675,25 @@ class AGIService
if ($conversationId) {
// بازیابی گفتگوی موجود
$conversation = $this->em->getRepository(AIConversation::class)->find($conversationId);
if ($conversation && $conversation->getUser() === $user && $conversation->getBusiness() === $business) {
// به‌روزرسانی زمان آخرین تغییر
// بررسی وجود گفتگو
if (!$conversation) {
// ایجاد گفتگوی جدید به جای خطا
}
// بررسی دسترسی کاربر به گفتگو
elseif ($conversation->getUser()->getId() !== $user->getId() ||
$conversation->getBusiness()->getId() !== $business->getId()) {
// ایجاد گفتگوی جدید به جای خطا
$conversation = null;
}
// بررسی حذف شدن گفتگو
elseif ($conversation->isDeleted()) {
// ایجاد گفتگوی جدید به جای خطا
$conversation = null;
}
// اگر گفتگو معتبر است، به‌روزرسانی زمان
if ($conversation && !$conversation->isDeleted()) {
$conversation->setUpdatedAt(time());
$this->em->persist($conversation);
return $conversation;
@ -665,6 +712,8 @@ class AGIService
$conversation->setDeleted(false);
$this->em->persist($conversation);
$this->em->flush(); // اضافه شد تا id مقداردهی شود
return $conversation;
}
@ -799,7 +848,36 @@ class AGIService
{
$conversation = $this->em->getRepository(AIConversation::class)->find($conversationId);
if (!$conversation || $conversation->getUser() !== $user || $conversation->getBusiness() !== $business) {
// بررسی وجود گفتگو
if (!$conversation) {
$this->log->warning('تلاش برای دریافت پیام‌های گفتگوی ناموجود', [
'conversationId' => $conversationId,
'user' => $user ? $user->getId() : 'unknown',
'business' => $business ? $business->getId() : 'unknown'
]);
return [];
}
// بررسی دسترسی کاربر به گفتگو
if ($conversation->getUser()->getId() !== $user->getId() ||
$conversation->getBusiness()->getId() !== $business->getId()) {
$this->log->warning('تلاش غیرمجاز برای دریافت پیام‌های گفتگوی دیگران', [
'conversationId' => $conversationId,
'requestedUser' => $user ? $user->getId() : 'unknown',
'requestedBusiness' => $business ? $business->getId() : 'unknown',
'conversationUser' => $conversation->getUser()->getId(),
'conversationBusiness' => $conversation->getBusiness()->getId()
]);
return [];
}
// بررسی حذف شدن گفتگو
if ($conversation->isDeleted()) {
$this->log->info('تلاش برای دریافت پیام‌های گفتگوی حذف شده', [
'conversationId' => $conversationId,
'user' => $user ? $user->getId() : 'unknown'
]);
return [];
}
@ -833,6 +911,19 @@ class AGIService
*/
private function buildToolsFromPromptServices(): array
{
return $this->promptService->getAllTools();
// بر اساس آخرین مستندات OpenAI و OpenRouter، باید هر ابزار به صورت زیر باشد:
// [
// 'type' => 'function',
// 'function' => [
// 'name' => ..., 'description' => ..., 'parameters' => [...]
// ]
// ]
// این ساختار در PromptService رعایت شده اما اینجا یکبار دیگر چک و لاگ می‌کنیم
$tools = $this->promptService->getAllTools();
// ابزارها را در لاگ دیباگ ذخیره کن
if (method_exists($this->log, 'debug')) {
$this->log->debug('AIService tools', ['tools' => $tools]);
}
return $tools;
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
class AccountingDocPromptService
{
private $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->em = $entityManager;
}
/**
* ابزارهای بخش اسناد حسابداری برای function calling
*/
public function getTools(): array
{
$tools = [];
// ابزار جست‌وجوی ردیف‌های اسناد
$searchRowsPrompt = $this->getSearchAccountingRowsPrompt();
$searchRowsData = json_decode($searchRowsPrompt, true);
if ($searchRowsData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $searchRowsData['tool'],
'description' => $searchRowsData['description'],
'parameters' => $searchRowsData['parameters']
]
];
}
return $tools;
}
/**
* پرامپ‌های توضیحی برای مدل
*/
public function getAllAccountingDocPrompts(): string
{
return $this->getSearchAccountingRowsPrompt();
}
public function getSearchAccountingRowsPrompt(): string
{
return '{
"tool": "searchAccountingRows",
"description": "این ابزار برای جست‌وجوی تراکنش‌ها و ردیف‌های اسناد حسابداری اشخاص، حساب‌های بانکی، صندوق، حقوق و ... استفاده می‌شود. برای استفاده، ابتدا باید با ابزارهای جست‌وجوی اشخاص یا حساب‌های بانکی و ...، کد (code) یا شناسه مورد نظر را به دست آورید. سپس با ارسال نوع (type) مناسب (مانند person، bank، cashdesk، salary و ...) و کد (id)، می‌توانید لیست تراکنش‌ها یا ردیف‌های مرتبط را دریافت کنید. خروجی شامل لیست ردیف‌ها با اطلاعات کامل است.",
"endpoint": "/api/accounting/rows/search",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"type": {"type": "string", "description": "نوع جست‌وجو (person, bank, cashdesk, salary و ...)"},
"id": {"type": ["string", "integer"], "description": "کد یا شناسه مورد جست‌وجو"},
"acc": {"type": "object", "description": "اطلاعات دسترسی (الزامی برای بک‌اند)"}
},
"required": ["type", "id"]
},
"output": [
{
"id": "integer",
"dateSubmit": "string",
"date": "string",
"type": "string",
"ref": "string",
"des": "string",
"bs": "string",
"bd": "string",
"code": "string",
"submitter": "string"
}
],
"examples": {
"input": {"type":"person","id":"1001","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": [
{
"id": 9,
"dateSubmit": "1753370911",
"date": "1404/05/02",
"type": "sell",
"ref": "حساب‌های دریافتی",
"des": "فاکتور فروش",
"bs": "0",
"bd": "210000",
"code": "1000",
"submitter": "بابک"
}
]
}
}';
}
}

View file

@ -26,6 +26,13 @@ class BankPromptService
$bankAccountInfoData = json_decode($bankAccountInfoPrompt, true);
if ($bankAccountInfoData) {
// اصلاح ساختار properties
$properties = [
'code' => [
'type' => 'string',
'description' => 'Bank account code (e.g., 1001, 1002)'
]
];
$tools[] = [
'type' => 'function',
'function' => [
@ -33,8 +40,8 @@ class BankPromptService
'description' => $bankAccountInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $bankAccountInfoData['input'],
'required' => array_keys($bankAccountInfoData['input'])
'properties' => $properties,
'required' => ['code']
]
]
];

View file

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

View file

@ -13,104 +13,71 @@ class InventoryPromptService
$this->em = $entityManager;
}
/**
* دریافت تمام ابزارهای بخش کالاها برای function calling
* @return array
*/
public function getTools(): array
{
$tools = [];
// ابزار getItemInfo
$itemInfoPrompt = $this->getItemInfoPrompt();
$itemInfoData = json_decode($itemInfoPrompt, true);
if ($itemInfoData) {
$commodityPrompt = $this->getAddOrUpdateCommodityPrompt();
$commodityData = json_decode($commodityPrompt, true);
if ($commodityData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $itemInfoData['tool'],
'description' => $itemInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $itemInfoData['input'],
'required' => array_keys($itemInfoData['input'])
]
'name' => $commodityData['tool'],
'description' => $commodityData['description'],
'parameters' => $commodityData['parameters']
]
];
}
return $tools;
}
/**
* تولید تمام پرامپ‌های بخش کالاها
* @return string
*/
public function getAllInventoryPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getItemInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreateItemPrompt();
// $prompts[] = $this->getUpdateItemPrompt();
// $prompts[] = $this->getSearchItemPrompt();
// $prompts[] = $this->getItemStockPrompt();
// ترکیب تمام پرامپ‌ها
return implode("\n\n", $prompts);
return $this->getAddOrUpdateCommodityPrompt();
}
/**
* پرامپ برای دریافت اطلاعات کامل کالا
* @return string
*/
public function getItemInfoPrompt(): string
public function getAddOrUpdateCommodityPrompt(): string
{
return '{
"tool": "getItemInfo",
"description": "Get complete item information by code",
"endpoint": "/api/item/info/{code}",
"method": "GET",
"input": {
"code": "string - Item code (e.g., 1001, 1002)"
},
"output": {
"id": "integer - Item ID",
"code": "string - Item code",
"name": "string - Item name",
"description": "string - Item description",
"category": "string - Item category",
"unit": "string - Unit of measurement",
"price": "float - Item price",
"stock": "float - Current stock quantity",
"minStock": "float - Minimum stock level",
"maxStock": "float - Maximum stock level",
"supplier": "string - Supplier name",
"barcode": "string - Barcode",
"isActive": "boolean - Item active status"
},
"examples": {
"input": {"code": "1001"},
"output": {
"id": 45,
"code": "1001",
"name": "لپ‌تاپ اپل",
"description": "لپ‌تاپ اپل مک‌بوک پرو 13 اینچ",
"category": "الکترونیک",
"unit": "عدد",
"price": 45000000,
"stock": 15,
"minStock": 5,
"maxStock": 50,
"supplier": "شرکت اپل",
"barcode": "1234567890123",
"isActive": true
}
}
}';
"tool": "addOrUpdateCommodity",
"description": "برای ویرایش یک کالا ابتدا باید با ابزار جست‌وجوی کالا (در آینده) کالا را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، کالا/خدمت جدید ایجاد خواهد شد. Add a new commodity or update an existing one. If code is 0 or not set, a new commodity will be created. Otherwise, the commodity with the given code will be updated.",
"endpoint": "/api/commodity/mod/{code}",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "Commodity name (required)"},
"priceSell": {"type": "string", "description": "Sell price"},
"priceBuy": {"type": "string", "description": "Buy price"},
"des": {"type": "string", "description": "Description"},
"unit": {"type": "string", "description": "Unit name (required)"},
"code": {"type": ["integer", "string"], "description": "Commodity code (0 for new, otherwise for update)"},
"khadamat": {"type": "boolean", "description": "Is service?"},
"cat": {"type": "object", "description": "Category object (id, code, name, ...)"},
"orderPoint": {"type": "integer", "description": "Order point"},
"commodityCountCheck": {"type": "boolean", "description": "Count check flag"},
"minOrderCount": {"type": "integer", "description": "Minimum order count"},
"dayLoading": {"type": "integer", "description": "Day loading"},
"speedAccess": {"type": "boolean", "description": "Quick access flag"},
"withoutTax": {"type": "boolean", "description": "Without tax flag"},
"barcodes": {"type": "string", "description": "Barcodes"},
"prices": {"type": "array", "items": {"type": "object"}, "description": "Prices list"},
"taxCode": {"type": "string", "description": "Tax code"},
"taxType": {"type": "string", "description": "Tax type"},
"taxUnit": {"type": "string", "description": "Tax unit"},
"acc": {"type": "object", "description": "Access info (required for backend)"}
},
"required": ["name", "unit"]
},
"output": {
"Success": "boolean",
"result": "integer",
"code": "integer"
},
"examples": {
"input": {"name":"میخ","priceSell":"6500","priceBuy":"5500","des":"","unit":"عدد","code":0,"khadamat":false,"cat":{"id":4,"code":4,"name":"بدون دسته‌بندی","checked":false,"root":null,"upper":"3"},"orderPoint":0,"commodityCountCheck":false,"minOrderCount":1,"dayLoading":0,"speedAccess":false,"withoutTax":false,"barcodes":"","prices":[],"taxCode":"","taxType":"","taxUnit":"","acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1,"code":2}
}
}';
}
}

View file

@ -26,6 +26,13 @@ class PersonPromptService
$personInfoData = json_decode($personInfoPrompt, true);
if ($personInfoData) {
// اصلاح ساختار properties
$properties = [
'code' => [
'type' => 'string',
'description' => 'Person code (e.g., 1001, 1002)'
]
];
$tools[] = [
'type' => 'function',
'function' => [
@ -33,13 +40,40 @@ class PersonPromptService
'description' => $personInfoData['description'],
'parameters' => [
'type' => 'object',
'properties' => $personInfoData['input'],
'required' => array_keys($personInfoData['input'])
'properties' => $properties,
'required' => ['code']
]
]
];
}
// ابزار getPersonsList
$personListPrompt = $this->getPersonsListPrompt();
$personListData = json_decode($personListPrompt, true);
if ($personListData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $personListData['tool'],
'description' => $personListData['description'],
'parameters' => $personListData['parameters']
]
];
}
// ابزار addOrUpdatePerson
$addOrUpdatePrompt = $this->getAddOrUpdatePersonPrompt();
$addOrUpdateData = json_decode($addOrUpdatePrompt, true);
if ($addOrUpdateData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $addOrUpdateData['tool'],
'description' => $addOrUpdateData['description'],
'parameters' => $addOrUpdateData['parameters']
]
];
}
return $tools;
}
@ -50,17 +84,9 @@ class PersonPromptService
public function getAllPersonPrompts(): string
{
$prompts = [];
// اضافه کردن تمام پرامپ‌های موجود
$prompts[] = $this->getPersonInfoPrompt();
// در آینده پرامپ‌های دیگر اضافه خواهند شد
// $prompts[] = $this->getCreatePersonPrompt();
// $prompts[] = $this->getUpdatePersonPrompt();
// $prompts[] = $this->getSearchPersonPrompt();
// $prompts[] = $this->getDeletePersonPrompt();
// ترکیب تمام پرامپ‌ها
$prompts[] = $this->getPersonsListPrompt();
$prompts[] = $this->getAddOrUpdatePersonPrompt();
return implode("\n\n", $prompts);
}
@ -182,4 +208,164 @@ class PersonPromptService
}';
}
public function getPersonsListPrompt(): string
{
return '{
"tool": "getPersonsList",
"description": "Search and list persons with filters, pagination, and types. The parameters types (person type: e.g., customer, marketer, etc.) and transactionFilters (debtors, creditors, zero) are optional and help to narrow down the search. Normally, you do not need to send these parameters unless you want a more precise search.",
"endpoint": "/api/person/list",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"page": {"type": "integer", "description": "Page number"},
"itemsPerPage": {"type": "integer", "description": "Number of items per page"},
"search": {"type": "string", "description": "Search text (name, code, mobile, etc.)"},
"types": {"type": "array", "items": {"type": "string"}, "description": "Person types (e.g., customer, marketer, etc.) - optional"},
"transactionFilters": {"type": "array", "items": {"type": "string"}, "description": "Transaction filters (debtors, creditors, zero) - optional"},
"sortBy": {"type": ["string", "null"], "description": "Sort field (optional)"},
"acc": {"type": "object", "description": "Access info (required for backend)"}
},
"required": ["page", "itemsPerPage", "search"]
},
"output": {
"items": [
{
"id": "integer",
"code": "string",
"nikename": "string",
"name": "string",
"tel": "string",
"mobile": "string",
"mobile2": "string",
"des": "string",
"company": "string",
"shenasemeli": "string",
"sabt": "string",
"shahr": "string",
"keshvar": "string",
"ostan": "string",
"postalcode": "string",
"codeeghtesadi": "string",
"email": "string",
"website": "string",
"fax": "string",
"birthday": "string|null",
"speedAccess": "boolean",
"address": "string",
"prelabel": "string|null",
"accounts": "array",
"types": "array",
"bs": "float",
"bd": "float",
"balance": "float"
}
],
"total": "integer",
"unfilteredTotal": "integer"
},
"examples": {
"input": {"page":1,"itemsPerPage":10,"search":"بابک","types":["customer","marketer","emplyee","supplier","colleague","salesman"],"transactionFilters":["debtors","creditors"],"sortBy":null,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {
"items": [
{
"id": 2,
"code": "1000",
"nikename": "بابک علی زاده",
"name": "",
"tel": "",
"mobile": "",
"mobile2": "",
"des": "",
"company": "",
"shenasemeli": "",
"sabt": "",
"shahr": "",
"keshvar": "",
"ostan": "",
"postalcode": "6761656589",
"codeeghtesadi": "",
"email": "",
"website": "",
"fax": "",
"birthday": null,
"speedAccess": false,
"address": "",
"prelabel": "آقای",
"accounts": [
{
"bank": "صادرات",
"shabaNum": "125210000000032563214444",
"cardNum": "6037998121456321",
"accountNum": "123456"
}
],
"types": [
{"label": "مشتری", "code": "customer", "checked": false},
{"label": "بازاریاب", "code": "marketer", "checked": false},
{"label": "کارمند", "code": "emplyee", "checked": true},
{"label": "تامین‌کننده", "code": "supplier", "checked": true},
{"label": "همکار", "code": "colleague", "checked": true},
{"label": "فروشنده", "code": "salesman", "checked": true}
],
"bs": 0,
"bd": 0,
"balance": 0
}
],
"total": 1,
"unfilteredTotal": 4
}
}
}';
}
public function getAddOrUpdatePersonPrompt(): string
{
return '{
"tool": "addOrUpdatePerson",
"description": "برای ویرایش یک شخص ابتدا باید با ابزار جست‌وجوی شخص (getPersonsList) شخص مورد نظر را پیدا کنید. اگر چند نتیجه یافت شد، باید از کاربر بپرسید کدام را می‌خواهد ویرایش کند و کد (code) آن را دریافت کنید. سپس با ارسال کد و اطلاعات جدید به این ابزار، ویرایش انجام می‌شود. اگر code برابر 0 یا ارسال نشود، شخص جدید ایجاد خواهد شد. Add a new person or update an existing person. If code is 0 or not set, a new person will be created. Otherwise, the person with the given code will be updated.",
"endpoint": "/api/person/mod/{code}",
"method": "POST",
"parameters": {
"type": "object",
"properties": {
"nikename": {"type": "string", "description": "Person nickname (required)"},
"name": {"type": "string", "description": "Person name"},
"des": {"type": "string", "description": "Description"},
"tel": {"type": "string", "description": "Telephone number"},
"mobile": {"type": "string", "description": "Mobile number"},
"mobile2": {"type": "string", "description": "Secondary mobile"},
"address": {"type": "string", "description": "Address"},
"company": {"type": "string", "description": "Company name"},
"shenasemeli": {"type": "string", "description": "National ID"},
"codeeghtesadi": {"type": "string", "description": "Economic code"},
"sabt": {"type": "string", "description": "Registration number"},
"keshvar": {"type": "string", "description": "Country"},
"ostan": {"type": "string", "description": "Province"},
"shahr": {"type": "string", "description": "City"},
"postalcode": {"type": "string", "description": "Postal code"},
"email": {"type": "string", "description": "Email address"},
"website": {"type": "string", "description": "Website"},
"fax": {"type": "string", "description": "Fax number"},
"code": {"type": ["integer", "string"], "description": "Person code (0 for new, otherwise for update)"},
"types": {"type": "array", "items": {"type": "object"}, "description": "Person types (array of {label, code, checked})"},
"accounts": {"type": "array", "items": {"type": "object"}, "description": "Bank accounts (array of {bank, accountNum, cardNum, shabaNum})"},
"prelabel": {"type": "string", "description": "Pre label (e.g., آقای, دکتر, etc.)"},
"speedAccess": {"type": "boolean", "description": "Quick access flag"},
"birthday": {"type": "string", "description": "Birthday"},
"acc": {"type": "object", "description": "Access info (required for backend)"}
},
"required": ["nikename"]
},
"output": {
"Success": "boolean",
"result": "integer"
},
"examples": {
"input": {"nikename":"بهتاش","name":"بهتاش عابدینی","des":"توضیحات","tel":"","mobile":"09183282405","mobile2":"","address":"","company":"آذرخش","shenasemeli":"123848","codeeghtesadi":"4864864","sabt":"468468","keshvar":"ایران","ostan":"کرمانشاه","shahr":"اسلام آباد غرب","postalcode":"6761656589","email":"","website":"","fax":"","code":0,"types":[{"label":"مشتری","code":"customer","checked":false},{"label":"بازاریاب","code":"marketer","checked":false},{"label":"کارمند","code":"emplyee","checked":false},{"label":"تامین‌کننده","code":"supplier","checked":false},{"label":"همکار","code":"colleague","checked":false},{"label":"فروشنده","code":"salesman","checked":false}],"accounts":[{"bank":"ملی","accountNum":"123456","cardNum":"12888787","shabaNum":"8484220000051515150"}],"prelabel":"دکتر","speedAccess":false,"acc":{"bid":2,"user":2,"year":2,"access":true,"money":1,"ai":true}},
"output": {"Success":true,"result":1}
}
}';
}
}

View file

@ -5,6 +5,8 @@ namespace App\Service\AGI\Promps;
use Doctrine\ORM\EntityManagerInterface;
use App\Service\AGI\Promps\InventoryPromptService;
use App\Service\AGI\Promps\BankPromptService;
use App\Service\AGI\Promps\AccountingDocPromptService;
use App\Service\AGI\Promps\TicketService;
class PromptService
{
@ -13,19 +15,25 @@ class PromptService
private $basePromptService;
private $inventoryPromptService;
private $bankPromptService;
private $accountingDocPromptService;
private $ticketPromptService;
public function __construct(
EntityManagerInterface $entityManager,
PersonPromptService $personPromptService,
BasePromptService $basePromptService,
InventoryPromptService $inventoryPromptService,
BankPromptService $bankPromptService
BankPromptService $bankPromptService,
AccountingDocPromptService $accountingDocPromptService,
TicketService $ticketPromptService
) {
$this->em = $entityManager;
$this->personPromptService = $personPromptService;
$this->basePromptService = $basePromptService;
$this->inventoryPromptService = $inventoryPromptService;
$this->bankPromptService = $bankPromptService;
$this->accountingDocPromptService = $accountingDocPromptService;
$this->ticketPromptService = $ticketPromptService;
}
/**
@ -48,9 +56,13 @@ class PromptService
$bankTools = $this->bankPromptService->getTools();
$tools = array_merge($tools, $bankTools);
// در آینده ابزارهای بخش‌های دیگر اضافه خواهند شد
// $accountingTools = $this->accountingPromptService->getTools();
// $tools = array_merge($tools, $accountingTools);
// ابزارهای بخش اسناد حسابداری
$accountingTools = $this->accountingDocPromptService->getTools();
$tools = array_merge($tools, $accountingTools);
// ابزارهای بخش تیکت‌ها
$ticketTools = $this->ticketPromptService->getTools();
$tools = array_merge($tools, $ticketTools);
return $tools;
}
@ -74,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();
@ -107,7 +121,13 @@ class PromptService
// پرامپ‌های بخش بانک‌ها
$prompts['bank'] = $this->bankPromptService->getAllBankPrompts();
// پرامپ‌های بخش اسناد حسابداری
$prompts['accounting'] = $this->accountingDocPromptService->getAllAccountingDocPrompts();
// پرامپ‌های بخش تیکت‌ها
$prompts['ticket'] = $this->ticketPromptService->getAllTicketPrompts();
// در آینده بخش‌های دیگر اضافه خواهند شد
// $prompts['accounting'] = $this->accountingPromptService->getAllAccountingPrompts();
// $prompts['reports'] = $this->reportsPromptService->getAllReportsPrompts();

View file

@ -0,0 +1,293 @@
<?php
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
{
$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']
]
];
}
// ابزار getTicketInfo
$ticketInfoPrompt = $this->getTicketInfoPrompt();
$ticketInfoData = json_decode($ticketInfoPrompt, true);
if ($ticketInfoData) {
$tools[] = [
'type' => 'function',
'function' => [
'name' => $ticketInfoData['tool'],
'description' => $ticketInfoData['description'],
'parameters' => $ticketInfoData['parameters']
]
];
}
// ابزار 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 getAllTicketPrompts(): string
{
$prompts = [];
$prompts[] = $this->getTicketsListPrompt();
$prompts[] = $this->getTicketInfoPrompt();
$prompts[] = $this->getAddOrUpdateTicketPrompt();
$prompts[] = $this->getReplyToTicketPrompt();
return implode("\n\n", $prompts);
}
/**
* پرامپ برای دریافت لیست تیکت‌ها
*/
public function getTicketsListPrompt(): string
{
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
}
],
"total": 1,
"unfilteredTotal": 5
}
}
}';
}
/**
* پرامپ برای دریافت اطلاعات تیکت
*/
public function getTicketInfoPrompt(): string
{
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
}
]
}
}
}';
}
/**
* پرامپ برای افزودن یا ویرایش تیکت
*/
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":"تیکت با موفقیت ایجاد شد"}
}
}';
}
/**
* پرامپ برای پاسخ به تیکت
*/
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

@ -236,6 +236,7 @@ class Explore
'taxCode' => $item->getTaxCode(),
'taxType' => $item->getTaxType(),
'taxUnit' => $item->getTaxUnit(),
'customCode' => $item->isCustomCode(),
'unitData' => [
'name' => $item->getUnit()->getName(),
'floatNumber' => $item->getUnit()->getFloatNumber(),
@ -245,8 +246,9 @@ class Explore
if ($des) {
$result['des'] = $des;
}
if ($item->getCat()) {
$result['cat'] = $item->getCat()->getName();
$cat = $item->getCat();
if ($cat !== null) {
$result['cat'] = $cat->getName();
}
return $result;
}
@ -344,6 +346,7 @@ class Explore
'speedAccess' => $person->isSpeedAccess(),
'address' => $person->getAddress(),
'prelabel' => null,
'tags' => $person->getTags(),
];
if ($person->getPrelabel()) {
$res['prelabel'] = $person->getPrelabel()->getLabel();

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;

View file

@ -0,0 +1,105 @@
{% extends "pdf/base.html.twig" %}
{% block body %}
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">مشخصات تنخواه گردان</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="">
<p>
<b>نام:</b>
{{ salary.name }}
</p>
</td>
<td class="center">
<p>
<b>کد حسابداری:</b>
{{ salary.code }}
</p>
</td>
<td class="center" colspan="3">
<p>
<b>شرح:</b>
{{ salary.des }}
</p>
</td>
</tr>
</tbody>
</table>
</div>
<div style="width:100%;margin-top:5px;text-align:center;">
<table style="width:100%;">
<tbody>
<tr style="text-align: center; background-color: grey; text-color: white">
<td style="width: 35px;">ردیف</td>
<td class="center item">شماره سند</td>
<td class="center item">تاریخ</td>
<td class="center item">توضیحات</td>
<td class="center item">تفضیل</td>
<td class="center item">بدهکار</td>
<td class="center item">بستانکار</td>
<td class="center item">شرح سند</td>
</tr>
{% set sumBs = 0 %}
{% set sumBd = 0 %}
{% for item in items %}
{% set sumBs = sumBs + item.bs %}
{% set sumBd = sumBd + item.bd %}
<tr class="stimol">
<td class="center item">{{ loop.index }}</td>
<td class="center item">{{ item.doc.code }}</td>
<td class="center item">{{ item.doc.date }}</td>
<td class="center item">{{ item.des }}</td>
<td class="center item">{{ item.ref ? item.ref.name : '' }}</td>
<td class="center item">{{ item.bd | number_format }}</td>
<td class="center item">{{ item.bs | number_format }}</td>
<td class="center item">{{ item.doc.des }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="width:100%; border:1px solid black;border-radius: 8px;margin-top:5px;text-align:center;">
<div class="tg-wrap" style="width:100%;border-radius: 8px 8px 0px 0px;text-align:center;background-color:gray">
<b style="color:white;">وضعیت حساب</b>
</div>
<table style="width:100%;">
<tbody>
<tr style="text-align:center;">
<td class="center">
<p>
<b>جمع بستانکار:</b>
{{ sumBs | number_format }}
</p>
</td>
<td class="center">
<p>
<b>جمع بدهکار:</b>
{{ sumBd | number_format }}
</p>
</td>
<td class="center">
<p>
<b>تراز حساب:</b>
<span>{{ (sumBs - sumBd) | abs |number_format }}</span>
</p>
</td>
<td class="center">
<p>
<b>وضعیت:</b>
{% if sumBs > sumBd%}
بستانکار
{% elseif sumBs == sumBd %}
تسویه شده
{% else %}
بدهکار
{% endif %}
</p>
</td>
</tr>
</tbody>
</table>
</div>
{% endblock %}

View file

@ -897,6 +897,9 @@ setup_web_ui() {
fi
log_message "INFO" "Web UI setup completed"
# رفع مشکل دسترسی esbuild (اضافه شده توسط AI)
find "$webui_path/node_modules" -type f -path '*/bin/esbuild' -exec chmod +x {} \;
}
# Function to set Apache ownership

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

@ -17,6 +17,9 @@
<span class="node-label">
{{ node.text }}
<span class="account-code">({{ node.id }})</span>
<span v-if="node.type && node.type !== 'calc'" class="account-type-badge">
{{ getAccountTypeLabel(node.type) }}
</span>
</span>
</template>
<template #after-input="{ node }">
@ -36,7 +39,7 @@
</Tree>
<!-- دیالوگ اضافه کردن زیرمجموعه -->
<v-dialog v-model="addDialog" max-width="400" persistent>
<v-dialog v-model="addDialog" max-width="500" persistent>
<v-card>
<v-toolbar flat color="success" dark>
<v-toolbar-title>اضافه کردن زیرمجموعه</v-toolbar-title>
@ -62,6 +65,31 @@
<v-card-text class="pt-4">
<v-text-field v-model="newNodeText" label="نام ردیف حساب جدید"
:rules="[v => !!v.trim() || 'این فیلد نمی‌تواند خالی باشد']" :disabled="dialogLoading"></v-text-field>
<v-select
v-model="selectedAccountType"
:items="accountTypes"
item-title="label"
item-value="value"
label="نوع تفضیل حساب"
:disabled="dialogLoading"
class="mt-4"
>
<template v-slot:item="{ props, item }">
<v-list-item v-bind="props">
<template v-slot:prepend>
<v-icon :icon="item.raw.icon" class="mr-2"></v-icon>
</template>
<v-list-item-subtitle>{{ item.raw.description }}</v-list-item-subtitle>
</v-list-item>
</template>
<template v-slot:selection="{ item }">
<div class="d-flex align-center">
<v-icon :icon="item.raw.icon" class="mr-2"></v-icon>
<span>{{ item.raw.label }}</span>
</div>
</template>
</v-select>
</v-card-text>
</v-card>
</v-dialog>
@ -187,6 +215,46 @@ export default {
const newNodeText = ref("");
const editNodeText = ref("");
const selectedNode = ref(null);
const selectedAccountType = ref("calc");
const accountTypes = ref([
{
value: "calc",
label: "اسناد جاری",
description: "برای ثبت اسناد حسابداری معمولی",
icon: "mdi-calculator"
},
{
value: "person",
label: "اشخاص",
description: "برای ثبت اطلاعات مشتریان، تامین‌کنندگان و کارمندان",
icon: "mdi-account-multiple"
},
{
value: "commodity",
label: "موجودی کالا",
description: "برای ثبت موجودی کالاها و محصولات",
icon: "mdi-package-variant"
},
{
value: "bank",
label: "حساب‌های بانکی",
description: "برای ثبت حساب‌های بانکی و تراکنشات مالی",
icon: "mdi-bank"
},
{
value: "salary",
label: "تنخواه گردان",
description: "برای ثبت تنخواه‌های گردان و هزینه‌های جاری",
icon: "mdi-cash-multiple"
},
{
value: "cashdesk",
label: "صندوق",
description: "برای ثبت صندوق‌های نقدی",
icon: "mdi-cash-register"
}
]);
const checkAccproPlugin = async () => {
try {
@ -209,7 +277,9 @@ export default {
const node = data[key];
treeData[key] = {
...node,
text: `(${node.id}) ${node.text}`
text: `(${node.id}) ${node.text}`,
originalText: node.text, // ذخیره نام اصلی برای استفاده در ویرایش
type: node.type || 'calc' // اضافه کردن نوع تفضیل حساب
};
});
@ -232,6 +302,7 @@ export default {
const openAddDialog = (node) => {
selectedNode.value = node;
newNodeText.value = "";
selectedAccountType.value = "calc";
addDialog.value = true;
};
@ -242,10 +313,16 @@ export default {
const response = await axios.post("/api/accounting/table/add", {
text: newNodeText.value,
parentId: selectedNode.value.id,
accountType: selectedAccountType.value,
});
if (response.data.result === 1) {
const newNode = response.data.node;
tree.value[newNode.id] = newNode;
tree.value[newNode.id] = {
...newNode,
text: `(${newNode.id}) ${newNode.text}`,
originalText: newNode.text,
type: newNode.type || 'calc',
};
if (!tree.value[selectedNode.value.id].children) {
tree.value[selectedNode.value.id].children = [];
}
@ -265,7 +342,8 @@ export default {
const openEditDialog = (node) => {
selectedNode.value = node;
editNodeText.value = node.text;
// استفاده از نام اصلی ردیف حساب
editNodeText.value = node.originalText || node.text.replace(/^\(\d+\)\s*/, '');
editDialog.value = true;
};
@ -281,6 +359,9 @@ export default {
tree.value[selectedNode.value.id] = {
...tree.value[selectedNode.value.id],
...response.data.node,
text: `(${response.data.node.id}) ${response.data.node.text}`,
originalText: response.data.node.text,
type: response.data.node.type || tree.value[selectedNode.value.id].type || 'calc',
};
editDialog.value = false;
showMessage("موفقیت", "ردیف حساب با موفقیت ویرایش شد!");
@ -328,6 +409,11 @@ export default {
}
};
const getAccountTypeLabel = (type) => {
const accountType = accountTypes.value.find(t => t.value === type);
return accountType ? accountType.label : type;
};
checkAccproPlugin();
loadData();
@ -346,12 +432,15 @@ export default {
newNodeText,
editNodeText,
selectedNode,
selectedAccountType,
accountTypes,
openAddDialog,
addNode,
openEditDialog,
saveEditNode,
openDeleteDialog,
confirmDeleteNode,
getAccountTypeLabel,
};
},
};
@ -393,4 +482,14 @@ export default {
font-size: 0.9em;
font-family: monospace;
}
.account-type-badge {
background-color: #1976d2;
color: white;
padding: 2px 6px;
border-radius: 12px;
font-size: 0.75em;
margin-right: 8px;
white-space: nowrap;
}
</style>

File diff suppressed because it is too large Load diff

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

@ -29,8 +29,12 @@
<v-tabs-window v-model="tabs">
<!-- اطلاعات پایه -->
<v-tabs-window-item value="0">
<v-card flat>
<v-card-text class="pa-2">
<v-card elevation="2">
<v-card-title class="text-white bg-primary">
<v-icon start icon="mdi-account" class="text-white"></v-icon>
{{ $t('pages.person.basic_info') }}
</v-card-title>
<v-card-text class="pa-4">
<v-row dense>
<v-col cols="12">
<v-switch v-model="person.speedAccess" :label="$t('pages.person.speed_access')"
@ -68,8 +72,12 @@
<!-- اطلاعات اقتصادی -->
<v-tabs-window-item value="1">
<v-card flat>
<v-card-text class="pa-2">
<v-card elevation="2">
<v-card-title class="text-white bg-primary">
<v-icon start icon="mdi-chart-line" class="text-white"></v-icon>
{{ $t('pages.person.eco_info') }}
</v-card-title>
<v-card-text class="pa-4">
<v-row dense>
<v-col cols="12" md="6">
<v-text-field v-model="person.shenasemeli" :label="$t('pages.person.national_id')" dense
@ -90,8 +98,12 @@
<!-- اطلاعات تماس -->
<v-tabs-window-item value="2">
<v-card flat>
<v-card-text class="pa-2">
<v-card elevation="2">
<v-card-title class="text-white bg-primary">
<v-icon start icon="mdi-phone" class="text-white"></v-icon>
{{ $t('pages.person.contact_info') }}
</v-card-title>
<v-card-text class="pa-4">
<v-row dense>
<v-col cols="12" md="6">
<v-text-field v-model="person.mobile" :label="$t('pages.person.mobile')" dense
@ -124,8 +136,12 @@
<!-- آدرس -->
<v-tabs-window-item value="3">
<v-card flat>
<v-card-text class="pa-2">
<v-card elevation="2">
<v-card-title class="text-white bg-primary">
<v-icon start icon="mdi-map-marker" class="text-white"></v-icon>
{{ $t('pages.person.address') }}
</v-card-title>
<v-card-text class="pa-4">
<v-row dense>
<v-col cols="12" md="6">
<v-text-field v-model="person.keshvar" :label="$t('pages.person.country')" dense
@ -158,8 +174,12 @@
<!-- حسابهای بانکی -->
<v-tabs-window-item value="4">
<v-card flat>
<v-card-text class="pa-2">
<v-card elevation="2">
<v-card-title class="text-white bg-primary">
<v-icon start icon="mdi-bank" class="text-white"></v-icon>
{{ $t('pages.person.banks_accounts') }}
</v-card-title>
<v-card-text class="pa-4">
<v-row justify="end" class="mb-2">
<v-col cols="auto">
<v-btn color="primary" @click="addNewCard" prepend-icon="mdi-plus" size="small">

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

@ -1,4 +1,9 @@
<template>
/*
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
<template>
<div class="sticky-container">
<v-toolbar color="toolbar" title="صورتحساب‌های ارسالی به سامانه مودیان مالیاتی">
<template v-slot:prepend>
@ -10,6 +15,9 @@
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn variant="flat" color="info" prepend-icon="mdi-book-open-page-variant" @click="$router.push('/acc/plugins/taxsettings/doc')" class="me-2">
راهنمای کامل
</v-btn>
<v-tooltip text="ارسال گروهی به سامانه مودیان مالیاتی" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-cloud-upload" color="orange" @click="sendBulkInvoices()"
@ -245,6 +253,7 @@ export default {
return {
loading: false,
bulkLoading: false,
checkLoading: false,
selectedInvoices: [],
invoices: [],
snackbar: {
@ -334,7 +343,7 @@ export default {
return 'خطا';
case 'FAILED':
return 'خطا';
case 'ACCEPTED':
case 'SUCCESS':
return 'تایید شده';
default:
return 'نامشخص';
@ -352,7 +361,7 @@ export default {
return 'grey';
case 'FAILED':
return 'error';
case 'ACCEPTED':
case 'SUCCESS':
return 'success';
default:
return 'primary';
@ -525,13 +534,62 @@ export default {
},
async performSend(item) {
item.sending = true;
try {
const validateResponse = await axios.post(`/api/plugins/tax/invoice/validate-buyer-info/${item.id}`);
if (!validateResponse.data.success && !validateResponse.data.can_proceed) {
if (validateResponse.data.buyer_info) {
const buyerInfo = validateResponse.data.buyer_info;
let message = `اطلاعات اقتصادی خریدار ناقص است.\n\n`;
message += `خریدار: ${buyerInfo.buyer_name || 'نامشخص'}`;
Swal.fire({
title: 'اطلاعات اقتصادی ناقص',
html: message.replace(/\n/g, '<br>'),
icon: 'warning',
confirmButtonText: 'ارسال بدون اطلاعات خریدار',
showCancelButton: true,
cancelButtonText: 'ویرایش خریدار',
showDenyButton: true,
denyButtonText: 'انصراف'
}).then((result) => {
if (result.isConfirmed) {
this.sendWithoutBuyerInfo(item);
} else if (result.dismiss === Swal.DismissReason.cancel && buyerInfo.buyer_code) {
this.$router.push(`/acc/persons/mod/${buyerInfo.buyer_code}`);
}
});
} else {
Swal.fire({
title: 'خطا در بررسی اطلاعات',
text: validateResponse.data.message || 'خطا در بررسی اطلاعات اقتصادی خریدار',
icon: 'error',
confirmButtonText: 'باشه'
});
}
return;
} else if (validateResponse.data.can_proceed) {
this.sendWithoutBuyerInfo(item);
}
} catch (error) {
Swal.fire({
title: 'خطا در ارسال فاکتور',
text: 'خطا در ارسال فاکتور: ' + (error.response?.data?.message || error.message),
icon: 'error',
confirmButtonText: 'باشه'
});
} finally {
item.sending = false;
}
},
async sendWithoutBuyerInfo(item) {
try {
const response = await axios.post(`/api/plugins/tax/invoice/send/${item.id}`);
if (response.data.success) {
Swal.fire({
title: 'ارسال موفق',
text: 'فاکتور با موفقیت به سامانه مودیان مالیاتی ارسال شد',
text: 'فاکتور بدون اطلاعات خریدار به سامانه مودیان مالیاتی ارسال شد',
icon: 'success',
confirmButtonText: 'باشه'
});
@ -551,8 +609,6 @@ export default {
icon: 'error',
confirmButtonText: 'باشه'
});
} finally {
item.sending = false;
}
},
sendBulkInvoices() {
@ -581,8 +637,65 @@ export default {
async performBulkSend(selectedItems) {
this.bulkLoading = true;
try {
const validationPromises = selectedItems.map(item =>
axios.post(`/api/plugins/tax/invoice/validate-buyer-info/${item.id}`)
);
const validationResults = await Promise.all(validationPromises);
const invalidInvoices = [];
const validInvoices = [];
validationResults.forEach((result, index) => {
if (!result.data.success) {
invalidInvoices.push({
...selectedItems[index],
buyerInfo: result.data.buyer_info
});
} else {
validInvoices.push(selectedItems[index]);
}
});
if (invalidInvoices.length > 0) {
let message = `تعداد ${invalidInvoices.length} فاکتور دارای اطلاعات اقتصادی ناقص هستند.\n\n`;
message += `خریداران دارای مشکل:\n`;
const uniqueBuyers = new Map();
invalidInvoices.forEach(invoice => {
const buyerInfo = invoice.buyerInfo;
if (buyerInfo && buyerInfo.buyer_name) {
uniqueBuyers.set(buyerInfo.buyer_name, true);
}
});
uniqueBuyers.forEach((value, buyerName) => {
message += `${buyerName}\n`;
});
Swal.fire({
title: 'اطلاعات اقتصادی ناقص',
html: message.replace(/\n/g, '<br>'),
icon: 'warning',
confirmButtonText: 'ارسال بدون اطلاعات خریدار',
showCancelButton: true,
cancelButtonText: 'مشاهده لیست اشخاص',
showDenyButton: true,
denyButtonText: 'انصراف'
}).then((result) => {
if (result.isConfirmed) {
this.sendBulkWithoutBuyerInfo(selectedItems);
} else if (result.dismiss === Swal.DismissReason.cancel) {
this.$router.push('/acc/persons/list');
}
});
this.bulkLoading = false;
return;
}
const response = await axios.post('/api/plugins/tax/invoice/send-bulk', {
ids: selectedItems.map(item => item.id)
ids: validInvoices.map(item => item.id)
});
if (response.data.success) {
@ -641,6 +754,67 @@ export default {
this.bulkLoading = false;
}
},
async sendBulkWithoutBuyerInfo(selectedItems) {
try {
const response = await axios.post('/api/plugins/tax/invoice/send-bulk', {
ids: selectedItems.map(item => item.id),
skip_buyer_validation: true
});
if (response.data.success) {
const summary = response.data.summary;
const results = response.data.results;
let successCount = 0;
let errorCount = 0;
const errorMessages = [];
results.forEach(result => {
if (result.success) {
successCount++;
} else {
errorCount++;
errorMessages.push(`${result.code}: ${result.message}`);
}
});
let message = `پردازش ${summary.total} فاکتور مالیاتی بدون اطلاعات خریدار تکمیل شد.\n\n`;
message += `✅ موفق: ${successCount} فاکتور\n`;
message += `❌ ناموفق: ${errorCount} فاکتور`;
if (errorCount > 0 && errorMessages.length > 0) {
message += `\n\اکتورهای ناموفق:\n${errorMessages.slice(0, 5).join('\n')}`;
if (errorMessages.length > 5) {
message += `\${errorMessages.length - 5} فاکتور دیگر...`;
}
}
Swal.fire({
title: successCount > 0 ? 'ارسال گروهی تکمیل شد' : 'خطا در ارسال گروهی',
html: message.replace(/\n/g, '<br>'),
icon: successCount > 0 ? 'success' : 'error',
confirmButtonText: 'باشه'
});
this.selectedInvoices = [];
this.loadData();
} else {
Swal.fire({
title: 'خطا در ارسال گروهی',
text: response.data.message || 'خطا در ارسال گروهی فاکتورها',
icon: 'error',
confirmButtonText: 'باشه'
});
}
} catch (error) {
Swal.fire({
title: 'خطا در ارسال گروهی',
text: 'خطا در ارسال گروهی فاکتورها: ' + (error.response?.data?.message || error.message),
icon: 'error',
confirmButtonText: 'باشه'
});
}
},
isItemSelectable(item) {
return item.status === 'pending' || item.status === 'error';
}

View file

@ -1,4 +1,9 @@
<template>
/*
* Developed by Mohammad Rezai
* https://pirouz.xyz 2025-07-28
*/
<template>
<div>
<v-toolbar color="toolbar" title="تنظیمات مالیاتی">
<template v-slot:prepend>

View file

@ -7,18 +7,24 @@
<div class="plugin-info">
<h1>راهنمای سامانه مودیان مالیاتی</h1>
<p class="plugin-description">
راهنمای کامل اتصال و استفاده از سامانه مودیان مالیاتی در حسابیکس
راهنمای کامل اتصال و استفاده از سامانه مودیان مالیاتی در {{ getSiteName() }}
</p>
<div class="plugin-version">
<span class="version-badge">نسخه 1.0.0</span>
<span v-if="isPluginActive('taxsettings')" class="status-badge active">فعال</span>
<RouterLink to="/acc/plugin-center/view-end/taxsettings" v-if="!isPluginActive('taxsettings')">
<span class="status-badge active text-white d-flex align-items-center">
<i class="fa fa-shopping-cart me-1"></i>
خرید
</span>
</RouterLink>
</div>
</div>
</div>
<div class="doc-content">
<div class="doc-section">
<h2>سامانه مودیان مالیاتی در نرم افزار حسابداری آنلاین حسابیکس</h2>
<h2>سامانه مودیان مالیاتی در نرم افزار حسابداری آنلاین {{ getSiteName() }}</h2>
<hr>
<p>سامانه مودیان مالیاتی، سامانهای آنلاین تحت مدیریت سازمان امور مالیاتی است که برای جمع آوری و یکپارچه سازی
اطلاعات مودیان مالیاتی و دریافت گزارشهای الکترونیکی مودیان، از طریق ایجاد یک کارپوشه ی اختصاصی در نظر گرفته
@ -32,25 +38,25 @@
نمی شوند.</p>
<div class="highlight-box">
<strong>با حسابیکس به راحتی به سامانه مودیان مالیاتی متصل شوید.</strong>
<strong>با {{ getSiteName() }} به راحتی به سامانه مودیان مالیاتی متصل شوید.</strong>
</div>
<p>در روزهای اخیر که دغدغه ی تمامی کاربران ارسال صورتحساب به سامانه مودیان مالیاتی بوده و یکی از پر تکرارترین
سوالات مطرح شده از تیم پشتیبانی حسابیکس در رابطه با این مورد بوده است. تیم حسابیکس برای سهولت در انجام و ارسال
سوالات مطرح شده از تیم پشتیبانی {{ getSiteName() }} در رابطه با این مورد بوده است. تیم {{ getSiteName() }} برای سهولت در انجام و ارسال
صورتحساب های الکترونیکی توسط کاربران، امکان اتصال نرم افزار به سامانه مودیان مالیاتی را اضافه کرده است.</p>
<p>از این پس کاربران در نرم افزار حسابداری حسابیکس این امکان را دارند که به ساده ترین حالت ممکن و بدون نیاز به
<p>از این پس کاربران در نرم افزار حسابداری {{ getSiteName() }} این امکان را دارند که به ساده ترین حالت ممکن و بدون نیاز به
داشتن یک حسابدار به سامانه مودیان مالیاتی متصل شوند.</p>
<div class="info-panel">
<p class="mb-0">در نرم افزار حسابیکس با انجام چند مرحله ی ساده می توانید صورتحساب الکترونیکی خود را به صورت رایگان به
<p class="mb-0">در نرم افزار {{ getSiteName() }} با انجام چند مرحله ی ساده می توانید صورتحساب الکترونیکی خود را به صورت رایگان به
سامانه مودیان مالیاتی ارسال نمایید.</p>
</div>
</div>
<div class="doc-section">
<h2>اتصال حسابیکس به سامانه مودیان:</h2>
<p>برای اتصال به سامانه مودیان و ارسال فاکتور در حسابیکس ، باید موارد زیر را به ترتیب انجام دهید:</p>
<h2>اتصال {{ getSiteName() }} به سامانه مودیان:</h2>
<p>برای اتصال به سامانه مودیان و ارسال فاکتور در {{ getSiteName() }} ، باید موارد زیر را به ترتیب انجام دهید:</p>
<div class="step-item">
<div class="step-number">۱</div>
@ -61,7 +67,7 @@
انتخاب کنید. در پنجره باز شده با وارد کردن اطلاعات مورد نیاز (شناسه ملی، نام فارسی، نام انگلیسی و ایمیل)
اقدام به دریافت کلیدهای لازم کنید.</p>
<div class="image-container">
<img src="/img/docs/moadian/1.jpg" alt="اتصال نرم افزار حسابدای آنلاین حسابیکس به سامانه مودیان"
<img src="/img/docs/moadian/1.jpg" alt="اتصال نرم افزار حسابدای آنلاین {{ getSiteName() }} به سامانه مودیان"
class="doc-image">
</div>
@ -70,7 +76,7 @@
<ul>
<li>پیشنهاد می شود شناسه ملی و نام فارسی را، با مراجعه به سایت سازمان امور مالیاتی کشور به آدرس <a
href="//my.tax.gov.ir" target="_blank" rel="noopener nofollow">my.tax.gov.ir</a> و ورود به کار پوشه
خود، کپی و در کادر مربوطه در حسابیکس جایگذاری نمایید.</li>
خود، کپی و در کادر مربوطه در {{ getSiteName() }} جایگذاری نمایید.</li>
<li>دقت داشته باشید که در ابتدا و انتهای فیلد نام فارسی، نباید فاصله (اسپیس) اضافی گذاشته شده باشد.</li>
<li>نام انگلیسی را می توانید بصورت فینگیلیش وارد نمایید. (نیازی به ترجمه نام کسب و کار نیست)</li>
<li>در فیلد ایمیل، ترجیحا ایمیل سازمانی معتبرخود را وارد کنید.</li>
@ -97,7 +103,7 @@
<div class="warning-panel">
<strong>توجه</strong>
<p>حتما اطلاعات نمایش داده شده در این پنجره را دانلود و در جای امن و مطمئن نگهداری کنید. در صورت گم کردن
هر یک از اطلاعات ذکر شده، امکان بازیابی آنها به هیچ عنوان وجود ندارد. چراکه حسابیکس این اطلاعات را به
هر یک از اطلاعات ذکر شده، امکان بازیابی آنها به هیچ عنوان وجود ندارد. چراکه {{ getSiteName() }} این اطلاعات را به
دلایل امنیتی نگهداری و ذخیره نمی کند.</p>
</div>
<div class="image-container">
@ -125,14 +131,14 @@
<div class="step-item">
<div class="step-number">۴</div>
<div class="step-content">
<p>به حسابیکس برگردید، شناسه یکتا حافظه مالیاتی و کداقتصادی را در فیلدهای مرتبط وارد نمایید.</p>
<p>به {{ getSiteName() }} برگردید، شناسه یکتا حافظه مالیاتی و کداقتصادی را در فیلدهای مرتبط وارد نمایید.</p>
</div>
</div>
<div class="step-item">
<div class="step-number">۵</div>
<div class="step-content">
<p>فایل دانلود شده Private Key را در کادر مرتبط در حسابیکس جایگذاری کنید. روی دکمه ذخیره کلیک کنید تا اتصال
<p>فایل دانلود شده Private Key را در کادر مرتبط در {{ getSiteName() }} جایگذاری کنید. روی دکمه ذخیره کلیک کنید تا اتصال
با موفقیت انجام شود.</p>
</div>
</div>
@ -160,7 +166,7 @@
<div class="doc-section">
<h2>دریافت کدمالیاتی کالا و خدمت:</h2>
<p>پس از اتصال به سامانه مودیان، برای ارسال صورتحساب به این سامانه باید کد و واحد مالیاتی کالا و خدمات در
حسابیکس ثبت شود.</p>
{{ getSiteName() }} ثبت شود.</p>
<p>برای ثبت کد مالیاتی باید از کدهای عمومی یا اختصاصی استفاده نمایید. راهنمای کامل پیدا کردن کد مالیاتی کالاها و
خدمات در سایت <a href="//www.intamedia.ir/" target="_blank" rel="noopener nofollow">intamedia.ir</a> در بخش
پایانه های فروشگاهی وسامانه مودیان <span class="arrow"> &gt; </span> آیین نامه ها، دستورالعمل ها و نرم
@ -171,17 +177,17 @@
rel="noopener nofollow">ntsw.ir</a> نسبت به ثبت کدهای اختصاصی برای کالا های خود اقدام نمایید.</p>
<p>برای ثبت کد خدمات ابتدا باید در سایت <a href="//portal.gs1-ir.org/Account/Login?ReturnUrl=%2F"
target="_blank" rel="noopener nofollow">portal.gs1-ir.org</a> درخواست صدور کد برای خدمات را ثبت و سپس در
حسابیکس وارد کنید.</p>
{{ getSiteName() }} وارد کنید.</p>
<div class="image-container">
<img src="/img/docs/moadian/7.jpg" alt="ارسال صورتحساب به سامانه مودیان" class="doc-image">
</div>
<p>کد مالیاتی کالا را در فایل پیدا کنید. در حسابیکس از منوی کالا و خدمات، کالا را جستجو و روی دکمه ویرایش کلیک
<p>کد مالیاتی کالا را در فایل پیدا کنید. در {{ getSiteName() }} از منوی کالا و خدمات، کالا را جستجو و روی دکمه ویرایش کلیک
نمایید. در سربرگ مالیات در کادر کدمالیاتی کد ۱۳ رقمی را وارد نمایید. امکان انتخاب واحد مالیاتی از لیست هم وجود
دارد.</p>
<div class="image-container">
<img src="/img/docs/moadian/8.jpg" alt="کدمالیاتی کد ۱۳ رقمی سامانه مودیان" class="doc-image">
</div>
<!-- <p>برای ثبت کد و واحد مالیاتی در نرم افزار حسابیکس به صورت دسته جمعی می توانید از طریق فایل اکسل نیز اطلاعات را
<!-- <p>برای ثبت کد و واحد مالیاتی در نرم افزار {{ getSiteName() }} به صورت دسته جمعی می توانید از طریق فایل اکسل نیز اطلاعات را
بارگذاری کنید. کافیست در منوی کالا و خدمات روی منو بیشتر ... بالا صفحه سمت چپ کلیک و صدور اطلاعات به اکسل را
انتخاب نمایید. در فایل اکسل دانلود شده، ستون های کد و واحد مالیاتی را تکمیل و ذخیره کنید. سپس فایل اکسل ذخیره
شده را در نرم افزار از منوی کالا و خدمات <span class="arrow"> &gt; </span> منو بیشتر ... <span class="arrow">
@ -199,7 +205,7 @@
<li>بهتر است تاریخ فاکتور در یک بازه زمانی مشخص، حداکثر ۲هفته قبل از تاریخ ارسال فاکتور به سامانه مودیان ثبت
شده باشد.</li>
<li>در حال حاضر تنها با واحد پولی ریال ایران (IRR) میتوانید فاکتورها را ثبت کنید.</li>
<li>حسابیکس مبالغ دارای اعشار را به صورت رند شده نمایش می دهد. اما مبالغ دارای اعشار در سامانه مودیان
<li>{{ getSiteName() }} مبالغ دارای اعشار را به صورت رند شده نمایش می دهد. اما مبالغ دارای اعشار در سامانه مودیان
پذیرفته نمی شود. پس در صورتیکه مبلغ درج شده در فیلد مالیات ارزش افزوده دارای اعشار باشد، ارسال صورتحساب با
خطا مواجه خواهد شد. می بایست مبلغ واحد را به صورتی تعیین کنید که پس از کسر تخفیف و سایر موارد، محاسبه درصد
مالیات ارزش افزوده آن بدون اعشار باشد. پیشنهاد می شود برای حل این مشکل مبالغی را وارد نمایید که به دو صفر
@ -219,7 +225,7 @@
<div class="image-container">
<img src="/img/docs/moadian/10.jpg" alt="ثبت مبلغ اضافات و کسورات در سامانه مودیان" class="doc-image">
</div>
<p>نرم افزار شما را به منو سامانه مودیان در حسابیکس هدایت می کند. این بخش از منوی سامانه مودیان مالیاتی<span class="arrow"> &gt;
<p>نرم افزار شما را به منو سامانه مودیان در {{ getSiteName() }} هدایت می کند. این بخش از منوی سامانه مودیان مالیاتی<span class="arrow"> &gt;
</span> زیر منوی صورتحساب ها هم در دسترس است.</p>
<h5>در این صفحه چند نوع فاکتور مشاهده می کنید:</h5>
@ -242,7 +248,7 @@
<h5>در این جدول ستون هایی مشاهده می شود که به بررسی هر کدام می پردازیم:</h5>
<ul>
<li><strong>شماره فاکتور:</strong> شماره فاکتور در حسابیکس</li>
<li><strong>شماره فاکتور:</strong> شماره فاکتور در {{ getSiteName() }}</li>
<li><strong>تاریخ فاکتور:</strong> تاریخ فاکتور</li>
<li><strong>مشتری:</strong> خریدار فاکتور</li>
<li><strong>مبلغ کل:</strong> مبلغ فاکتور</li>
@ -291,8 +297,8 @@
</div>
</div>
<div v-if="isPluginActive('taxsettings')" class="doc-footer">
<div class="action-buttons">
<div class="doc-footer">
<div v-if="isPluginActive('taxsettings')" class="action-buttons">
<router-link to="/acc/plugins/taxsettings/intro" class="btn btn-info">
<i class="fas fa-home"></i>
صفحه اصلی افزونه
@ -306,11 +312,16 @@
مدیریت فاکتورها
</router-link>
</div>
<div style="margin-top: 20px; font-size: 0.75rem; color: #888; text-align: center;">
Developed by <a href="https://pirouz.xyz" target="_blank" style="color: #667eea; text-decoration: none;">Mohammad Rezai</a> 2025
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'TaxSettingsDoc',
data() {
@ -319,25 +330,31 @@ export default {
name: 'سامانه مودیان مالیاتی',
version: '1.0.0',
description: 'راهنمای کامل اتصال و استفاده از سامانه مودیان مالیاتی'
}
}
},
computed: {
plugins() {
return this.$store?.state?.plugins || {};
},
plugins: {}
}
},
methods: {
isPluginActive(plugName) {
return this.plugins && this.plugins[plugName] !== undefined;
}
},
getSiteName() {
return localStorage.getItem('hesabix_site_name') || '{{ getSiteName() }}';
},
},
mounted() {
axios.post('/api/plugin/get/actives').then((response) => {
this.plugins = response.data;
});
if (this.$store) {
this.$store.commit('setPageTitle', 'سامانه مودیان مالیاتی - راهنما')
}
}
}
document.addEventListener('DOMContentLoaded', () => {
console.log(isPluginActive('taxsettings'))
});
</script>
<style scoped>

View file

@ -12,6 +12,12 @@
<div class="plugin-version">
<span class="version-badge">نسخه 1.0.0</span>
<span v-if="isPluginActive('taxsettings')" class="status-badge active">فعال</span>
<RouterLink to="/acc/plugin-center/view-end/taxsettings" v-if="!isPluginActive('taxsettings')">
<span class="status-badge active text-white d-flex align-items-center">
<i class="fa fa-shopping-cart me-1"></i>
خرید
</span>
</RouterLink>
</div>
</div>
</div>
@ -205,8 +211,8 @@
</div>
</div>
<div v-if="isPluginActive('taxsettings')" class="intro-footer">
<div class="action-buttons">
<div class="intro-footer">
<div v-if="isPluginActive('taxsettings')" class="action-buttons">
<router-link to="/acc/plugins/taxsettings/doc" class="btn btn-info">
<i class="fas fa-book"></i>
راهنمای کامل
@ -220,11 +226,16 @@
مدیریت فاکتورها
</router-link>
</div>
<div style="margin-top: 20px; font-size: 0.75rem; color: #888; text-align: center;">
Developed by <a href="https://pirouz.xyz" target="_blank" style="color: #667eea; text-decoration: none;">Mohammad Rezai</a> 2025
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'TaxSettingsIntro',
data() {
@ -233,20 +244,22 @@ export default {
name: 'سامانه مودیان مالیاتی',
version: '1.0.0',
description: 'ارسال خودکار فاکتورها به سامانه مودیان سازمان امور مالیاتی کشور'
}
}
},
computed: {
plugins() {
return this.$store?.state?.plugins || {};
},
plugins: {}
}
},
methods: {
isPluginActive(plugName) {
return this.plugins && this.plugins[plugName] !== undefined;
}
},
getSiteName() {
return localStorage.getItem('hesabix_site_name') || '{{ getSiteName() }}';
},
},
mounted() {
axios.post('/api/plugin/get/actives').then((response) => {
this.plugins = response.data;
});
if (this.$store) {
this.$store.commit('setPageTitle', 'سامانه مودیان مالیاتی - معرفی افزونه')
}

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

@ -52,7 +52,7 @@
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '64px' }">
<td class="text-center" style="min-width: 200px;">
<Hcommoditysearch v-model="item.name" density="compact" hide-details class="my-0"
style="font-size: 0.8rem;" return-object></Hcommoditysearch>
style="font-size: 0.8rem;" return-object @update:modelValue="onCommoditySelect(item)"></Hcommoditysearch>
</td>
<td class="text-center" style="width: 100px;">
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals"
@ -156,7 +156,7 @@
</div>
<div class="mb-2">
<Hcommoditysearch v-model="item.name" density="compact" label="نام کالا" hide-details class="my-0"
style="font-size: 0.8rem;" return-object></Hcommoditysearch>
style="font-size: 0.8rem;" return-object @update:modelValue="onCommoditySelect(item)"></Hcommoditysearch>
</div>
<div class="d-flex justify-space-between mb-2">
<div style="width: 48%;">
@ -676,6 +676,12 @@ export default {
item.discountPercent = 0;
}
this.recalculateTotals();
},
onCommoditySelect(item) {
if (item.name && item.name.priceSell) {
item.price = item.name.priceSell;
this.recalculateTotals();
}
}
}
}

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

View file

@ -1,65 +1,147 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button"
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
تراکنش های تنخواه گردان
</h3>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="col-sm-12 col-md-6 mb-1">
<div class="card push">
<div class="card-header border-bottom-0 bg-primary-dark text-light">
<h3 class="block-title"> گردش حساب <small class="text-info-light">{{ selectedObjectItem.name }}</small>
</h3>
</div>
<div class="card-body">
<small class="mb-2">تنخواه گردان</small>
<v-cob dir="rtl" :options="objectItems" label="name" v-model="selectedObjectItem"
@option:selected="updateRoute(selectedObjectItem.code)">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<hr />
<div class="fw-bold mb-2">کد حسابداری: <small class="text-primary">{{ selectedObjectItem.code }}</small>
</div>
<div class="fw-bold mb-2">نام : <small class="text-primary">{{ selectedObjectItem.name }}</small></div>
<div class="fw-bold mb-2">شرح: <small class="text-primary">{{ selectedObjectItem.des }}</small></div>
</div>
</div>
<v-toolbar color="toolbar" dense flat>
<v-btn icon @click="$router.back()" class="d-none d-md-flex">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
<v-toolbar-title class="text-primary-dark">
کارت حساب تنخواه گردان
</v-toolbar-title>
<v-spacer />
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="red">
<v-tooltip activator="parent" text="خروجی PDF" location="bottom" />
<v-icon icon="mdi-file-pdf-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">خروجی PDF</v-list-subheader>
<v-list-item class="text-dark" title="انتخاب شده‌ها" @click="print(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" title="همه" @click="print(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="green">
<v-tooltip activator="parent" text="خروجی اکسل" location="bottom" />
<v-icon icon="mdi-file-excel-box"></v-icon>
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">خروجی اکسل</v-list-subheader>
<v-list-item class="text-dark" title="انتخاب شده‌ها" @click="excelOutput(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" title="همه" @click="excelOutput(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar>
</div>
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12">
<h3>تراکنش ها:</h3>
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
<v-container fluid class="pa-4">
<v-row dense>
<v-col cols="12" md="12">
<v-autocomplete v-model="selectedSalary" :items="listSalaries" item-title="name" item-value="code"
return-object label="انتخاب تنخواه گردان" dense hide-details prepend-inner-icon="mdi-cash"
:loading="loading" @update:model-value="updateRoute"
class="rounded-lg elevation-2">
<template v-slot:no-data>
نتیجهای یافت نشد!
</template>
</v-autocomplete>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
اطلاعات تنخواه گردان
<small class="text-info-light" v-if="selectedSalary">{{ selectedSalary.name }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">کد حسابداری: <span class="text-primary">{{ selectedSalary.code || '-' }}</span></div>
<div class="text-subtitle-2">نام: <span class="text-primary">{{ selectedSalary.name || '-' }}</span></div>
<div class="text-subtitle-2">شرح: <span class="text-primary">{{ selectedSalary.des || '-' }}</span></div>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
وضعیت حساب
<small class="text-info-light" v-if="selectedSalary">{{ selectedSalary.name }}</small>
</v-toolbar-title>
</v-toolbar>
<v-card-text class="pa-2">
<div class="text-subtitle-2">
وضعیت حساب:
<span :class="{
'text-success': selectedSalary.balance > 0,
'text-danger': selectedSalary.balance < 0,
'text-dark': selectedSalary.balance == 0
}">
{{ selectedSalary.balance > 0 ? 'بستانکار' : selectedSalary.balance < 0 ? 'بدهکار' : 'تسویه' }}
</span>
</div>
</div>
<EasyDataTable table-class-name="customize-table" show-index alternating :search-value="searchValue" :headers="headers" :items="items"
theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="loading">
<template #item-operation="{ code }">
<router-link class="text-success" :to="'/acc/accounting/view/' + code">
<i class="fa fa-eye px-1"></i>
</router-link>
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
<div class="text-subtitle-2">بستانکار: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.bs) || '-' }}</span></div>
<div class="text-subtitle-2">بدهکار: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.bd) || '-' }}</span></div>
<div class="text-subtitle-2">تراز حساب: <span class="text-primary">{{ $filters.formatNumber(selectedSalary.balance) || '-' }}</span></div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12">
<v-data-table v-model="itemsSelected" :headers="headers" :items="items" :search="searchValue" :loading="loading"
show-select dense :items-per-page="25" class="elevation-2 rounded-lg" :header-props="{ class: 'custom-header' }">
<template v-slot:top>
<v-toolbar flat dense color="grey-lighten-4" class="rounded-t-lg">
<v-toolbar-title class="text-subtitle-1">تراکنشها</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field v-model="searchValue" dense hide-details prepend-inner-icon="mdi-magnify" />
</v-toolbar>
</template>
<template v-slot:item.operation="{ item }">
<v-btn variant="plain" icon size="small" :to="'/acc/accounting/view/' + item.code" color="success">
<v-icon small>mdi-eye</v-icon>
</v-btn>
</template>
<template v-slot:item.code="{ item }">
{{ $filters.formatNumber(item.code) }}
</template>
<template v-slot:item.bd="{ item }">
{{ $filters.formatNumber(item.bd) }}
</template>
<template v-slot:item.bs="{ item }">
{{ $filters.formatNumber(item.bs) }}
</template>
<template v-slot:no-data>
اطلاعاتی برای نمایش وجود ندارد
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar v-model="snackbar" :timeout="3000" color="info">{{ snackbarText }}</v-snackbar>
<v-overlay :value="loading" contained class="align-center justify-center">
<v-progress-circular indeterminate size="64" />
</v-overlay>
</template>
<script>
@ -68,67 +150,151 @@ import { ref } from "vue";
export default {
name: "card",
data: () => {
data() {
return {
searchValue: '',
objectItems: [{
name: ''
}],
selectedObjectItem: {},
listSalaries: [],
itemsSelected: [],
selectedSalary: { balance: 0, bs: 0, bd: 0 },
items: [],
loading: ref(true),
loading: ref(false),
snackbar: false,
snackbarText: '',
headers: [
{ text: "عملیات", value: "operation" },
{ text: "تاریخ", value: "date", 'sortable': true },
{ text: "شرح", value: "des" },
{ text: "تفضیل", value: "ref", 'sortable': true },
{ text: "بدهکار", value: "bd", 'sortable': true },
{ text: "بستانکار", value: "bs", 'sortable': true },
]
}
{ title: 'عملیات', key: "operation", align: "center", sortable: false },
{ title: 'شماره سند', key: "code", align: "center", sortable: true },
{ title: 'تاریخ', key: "date", align: "center", sortable: true },
{ title: 'شرح', key: "des", align: "center" },
{ title: 'تفضیل', key: "ref", align: "center", sortable: true },
{ title: 'بدهکار', key: "bd", align: "center", sortable: true },
{ title: 'بستانکار', key: "bs", align: "center", sortable: true },
],
};
},
mounted() {
this.loadData();
},
methods: {
updateRoute(id) {
this.$router.push(id);
this.loadData();
showSnackbar(text) {
this.snackbarText = text;
this.snackbar = true;
},
loadData() {
axios.post('/api/salary/list').then((response) => {
this.objectItems = response.data;
if (this.$route.params.id != '') {
this.loadObject(this.$route.params.id);
this.objectItems.forEach((item) => {
if (item.code == this.$route.params.id) {
this.selectedObjectItem = item;
}
});
} else {
this.selectedObjectItem = response.data[0];
this.loadObject(this.selectedObjectItem.code);
}
});
updateRoute() {
if (this.selectedSalary && this.selectedSalary.code) {
this.$router.push(this.selectedSalary.code);
this.loadSalary(this.selectedSalary.code);
}
},
loadObject(id) {
async loadData() {
this.loading = true;
axios.post('/api/accounting/rows/search',
{
type: 'salary',
id: id
try {
const response = await axios.post('/api/salary/list');
this.listSalaries = response.data;
const id = this.$route.params.id;
if (id) {
await this.loadSalary(id);
} else if (response.data.length > 0) {
this.selectedSalary = response.data[0];
await this.loadSalary(this.selectedSalary.code);
}
).then((response) => {
this.items = response.data;
this.items.forEach((item) => {
item.bs = this.$filters.formatNumber(item.bs)
item.bd = this.$filters.formatNumber(item.bd)
})
} catch (error) {
this.showSnackbar('خطا در بارگذاری اطلاعات');
} finally {
this.loading = false;
});
}
}
}
}
},
async loadSalary(id) {
this.loading = true;
try {
const salaryResponse = await axios.post('/api/salary/info/' + id);
this.selectedSalary = salaryResponse.data;
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'salary', id });
this.items = rowsResponse.data;
} catch (error) {
this.selectedSalary = { balance: 0, bs: 0, bd: 0 };
this.items = [];
this.showSnackbar('خطا در دریافت اطلاعات تنخواه گردان');
} finally {
this.loading = false;
}
},
async excelOutput(allItems = true) {
if (!allItems && this.itemsSelected.length === 0) {
this.showSnackbar('هیچ تراکنشی انتخاب نشده است');
return;
}
try {
const response = await axios({
method: 'post',
url: '/api/salary/card/list/excel',
data: allItems ? { code: this.selectedSalary.code } : { code: this.selectedSalary.code, items: this.itemsSelected },
responseType: 'arraybuffer',
});
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'salary-card-view.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
this.showSnackbar('خطا در دریافت خروجی اکسل');
}
},
async print(allItems = true) {
if (!this.selectedSalary) {
this.showSnackbar('هیچ تنخواه گردانی انتخاب نشده است');
return;
}
if (!allItems && this.itemsSelected.length === 0) {
this.showSnackbar('هیچ تراکنشی انتخاب نشده است');
return;
}
try {
const response = await axios.post('/api/salary/card/list/print', allItems ? { code: this.selectedSalary.code } : { code: this.selectedSalary.code, items: this.itemsSelected });
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
} catch (error) {
this.showSnackbar('خطا در دریافت خروجی PDF');
}
},
},
};
</script>
<style scoped></style>
<style scoped>
.custom-header {
background-color: #f5f5f5 !important;
font-weight: bold !important;
}
.v-data-table {
border-radius: 8px;
overflow: hidden;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.v-autocomplete {
background-color: white;
border-radius: 8px;
}
.v-toolbar {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.v-list-item {
transition: background-color 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
</style>

View file

@ -1,138 +1,299 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button" class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="fa fa-bank px-2"></i>
تنخواهگردانها
</h3>
<div class="block-options">
<router-link to="/acc/salary/mod/" class="block-options-item">
<span class="fa fa-plus fw-bolder"></span>
</router-link>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
</div>
</div>
<EasyDataTable table-class-name="customize-table" show-index alternating :search-value="searchValue" :headers="headers" :items="items"
theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="loading">
<template #item-operation="{ code }">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link"
data-bs-toggle="dropdown" id="dropdown-align-center-alt-primary" type="button">
<i class="fa-solid fa-ellipsis"></i>
</button>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end"
style="">
<router-link class="dropdown-item" :to="'/acc/salary/card/view/' + code">
<i class="fa fa-eye text-success pe-2"></i>
مشاهده
</router-link>
<router-link class="dropdown-item" :to="'/acc/salary/mod/' + code">
<i class="fa fa-edit pe-2"></i>
ویرایش
</router-link>
<button type="button" @click="deleteItem(code)" class="dropdown-item text-danger">
<i class="fa fa-trash pe-2"></i>
حذف
</button>
</div>
<v-toolbar color="toolbar" :title="$t('drawer.salary')">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer />
<v-slide-group show-arrows>
<v-slide-group-item>
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/salary/mod/" />
</template>
</v-tooltip>
</v-slide-group-item>
<v-slide-group-item>
<v-tooltip :text="$t('dialog.column_settings')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true" />
</template>
</v-tooltip>
</v-slide-group-item>
</v-slide-group>
</v-toolbar>
<v-text-field
v-model="search"
:loading="loading"
color="green"
class="mb-0 pt-0 rounded-0"
hide-details="auto"
density="compact"
:rounded="false"
:placeholder="$t('dialog.search_txt')"
clearable
>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template>
</v-tooltip>
</template>
</v-text-field>
<v-data-table
:headers="visibleHeaders"
:items="items"
:loading="loading"
:search="search"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
>
<template v-slot:item="{ item }">
<tr>
<td v-if="isColumnVisible('operation')" class="text-center">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<template #item-name="{ name, code }">
<router-link :to="'/acc/salary/card/view/' + code">
{{ name }}
</router-link>
</template>
<template #item-balance="{ balance }">
<label class="text-success" v-if="balance >= 0">{{ $filters.formatNumber(balance) }}</label>
<label class="text-danger" v-else>{{ $filters.formatNumber(-1 * balance) }} منفی</label>
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
<v-list>
<v-list-item :to="'/acc/salary/card/view/' + item.code">
<template v-slot:prepend>
<v-icon color="success" icon="mdi-eye" />
</template>
<v-list-item-title>{{ $t('dialog.view') }}</v-list-item-title>
</v-list-item>
<v-list-item :to="'/acc/salary/mod/' + item.code">
<template v-slot:prepend>
<v-icon icon="mdi-pencil" />
</template>
<v-list-item-title>{{ $t('dialog.edit') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmDelete(item.code)">
<template v-slot:prepend>
<v-icon color="error" icon="mdi-delete" />
</template>
<v-list-item-title>{{ $t('dialog.delete') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</td>
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
<td v-if="isColumnVisible('name')" class="text-center">
<router-link :to="'/acc/salary/card/view/' + item.code">
{{ item.name }}
</router-link>
</td>
<td v-if="isColumnVisible('balance')" class="text-center">
<span :class="Number(item.balance) >= 0 ? 'text-success' : 'text-error'">
{{ formatNumber(Math.abs(Number(item.balance))) }}
<span v-if="Number(item.balance) < 0">منفی</span>
</span>
</td>
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
</tr>
</template>
</v-data-table>
<v-dialog v-model="showColumnDialog" max-width="500">
<v-card>
<v-toolbar color="toolbar" :title="$t('dialog.manage_columns')">
<v-spacer></v-spacer>
<v-btn icon @click="showColumnDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col v-for="header in allHeaders" :key="header.key" cols="12" sm="6">
<v-checkbox
v-model="header.visible"
:label="header.title"
@change="updateColumnVisibility"
hide-details
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title class="text-h6">
تأیید حذف
</v-card-title>
<v-card-text>
آیا برای حذف تنخواهگردان مطمئن هستید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">خیر</v-btn>
<v-btn color="error" variant="text" @click="deleteItem">بله</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="messageDialog.show" max-width="400">
<v-card>
<v-card-title :class="messageDialog.color + ' text-h6'">
{{ messageDialog.title }}
</v-card-title>
<v-card-text class="pt-4">
{{ messageDialog.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="messageDialog.show = false">قبول</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
import { ref } from "vue";
<script setup>
import { ref, computed, onMounted } from 'vue';
import axios from 'axios';
export default {
name: "list",
data: () => {
return {
searchValue: '',
loading: ref(true),
items: [],
headers: [
{ text: "عملیات", value: "operation", width: "130" },
{ text: "کد", value: "code", width: "70px" },
{ text: "نام تنخواه‌گردان", value: "name", width: "120px" },
{ text: "موجودی()", value: "balance", width: "140px" },
{ text: "توضیحات", value: "des", width: "150px" },
]
}
},
methods: {
loadData() {
axios.post('/api/salary/list')
.then((response) => {
this.items = response.data;
this.loading = false;
})
},
deleteItem(code) {
Swal.fire({
text: 'آیا برای حذف تنخواه‌گردان مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: `خیر`,
}).then((result) => {
/* Read more about isConfirmed, isDenied below */
if (result.isConfirmed) {
axios.post('/api/salary/delete/' + code).then((response) => {
if (response.data.result == 1) {
let index = 0;
for (let z = 0; z < this.items.length; z++) {
index++;
if (this.items[z]['code'] == code) {
this.items.splice(index - 1, 1);
}
}
Swal.fire({
text: 'تنخواه‌گردان با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
else if (response.data.result == 2) {
Swal.fire({
text: 'تنخواه‌گردان به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
})
}
})
}
},
beforeMount() {
this.loadData();
const loading = ref(false);
const items = ref([]);
const search = ref('');
const showColumnDialog = ref(false);
const deleteDialog = ref({
show: false,
code: null
});
const messageDialog = ref({
show: false,
title: '',
message: '',
color: 'primary'
});
const formatNumber = (value) => {
if (!value) return '0';
return Number(value).toLocaleString('fa-IR');
};
const allHeaders = ref([
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
{ title: "کد", key: "code", align: 'center', sortable: true, width: 70, visible: true },
{ title: "نام تنخواه‌گردان", key: "name", align: 'center', sortable: true, width: 120, visible: true },
{ title: "موجودی", key: "balance", align: 'center', sortable: true, width: 140, visible: true },
{ title: "توضیحات", key: "des", align: 'center', sortable: true, width: 150, visible: true },
]);
const visibleHeaders = computed(() => {
return allHeaders.value.filter(header => header.visible);
});
const isColumnVisible = (key) => {
return allHeaders.value.find(header => header.key === key)?.visible;
};
const LOCAL_STORAGE_KEY = 'hesabix_salary_table_columns';
const loadColumnSettings = () => {
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSettings) {
const visibleColumns = JSON.parse(savedSettings);
allHeaders.value.forEach(header => {
header.visible = visibleColumns.includes(header.key);
});
}
}
};
const updateColumnVisibility = () => {
const visibleColumns = allHeaders.value
.filter(header => header.visible)
.map(header => header.key);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(visibleColumns));
};
const showMessage = (message, title = 'پیام', color = 'primary') => {
messageDialog.value = {
show: true,
title,
message,
color
};
};
const confirmDelete = (code) => {
deleteDialog.value = {
show: true,
code
};
};
const loadData = async () => {
loading.value = true;
try {
const response = await axios.post('/api/salary/list');
items.value = response.data;
} catch (error) {
console.error('Error loading data:', error);
showMessage('خطا در بارگذاری داده‌ها: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
const deleteItem = async () => {
const code = deleteDialog.value.code;
deleteDialog.value.show = false;
if (!code) return;
try {
loading.value = true;
const response = await axios.post(`/api/salary/delete/${code}`);
if (response.data.result === 1) {
items.value = items.value.filter(item => item.code !== code);
showMessage('تنخواه‌گردان با موفقیت حذف شد.', 'موفقیت', 'success');
} else if (response.data.result === 2) {
showMessage('تنخواه‌گردان به دلیل داشتن تراکنش و اسناد حسابداری مرتبط قابل حذف نیست.', 'خطا', 'error');
}
} catch (error) {
console.error('Error deleting item:', error);
showMessage('خطا در حذف آیتم: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
onMounted(() => {
loadColumnSettings();
loadData();
});
</script>
<style scoped></style>
<style>
.v-data-table {
width: 100%;
overflow-x: auto;
}
:deep(.v-data-table-header th) {
text-align: center !important;
}
:deep(.v-data-table__wrapper table td) {
text-align: center !important;
}
.text-success {
color: #4caf50 !important;
}
.text-error {
color: #ff5252 !important;
}
:deep(.v-data-table__wrapper table td a) {
text-decoration: none;
color: #1976d2;
}
:deep(.v-data-table__wrapper table td a:hover) {
text-decoration: underline;
}
</style>

View file

@ -67,7 +67,7 @@
<Hcommoditysearch v-model="item.name" density="compact" hide-details class="my-0" style="font-size: 0.8rem;" return-object @update:modelValue="handleCommodityChange(item)"></Hcommoditysearch>
</td>
<td class="text-center px-2">
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :max-decimals="2" :allow-decimal="true"></Hnumberinput>
<Hnumberinput v-model="item.count" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :max-decimals="3" :allow-decimal="true"></Hnumberinput>
</td>
<td class="text-center px-2">
<div class="d-flex align-center justify-center">
@ -145,7 +145,7 @@
</div>
<div class="d-flex justify-space-between mb-2">
<div class="flex-grow-1 mr-2">
<Hnumberinput v-model="item.count" density="compact" label="تعداد" hide-details class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;" @update:modelValue="recalculateTotals" :allow-decimal="true"></Hnumberinput>
<Hnumberinput v-model="item.count" density="compact" label="تعداد" hide-details class="my-0" style="font-size: 0.8rem; margin: 0; padding: 0;" @update:modelValue="recalculateTotals" :allow-decimal="true" :max-decimals="3"></Hnumberinput>
</div>
<div class="flex-grow-1">
<div class="d-flex align-center">

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

@ -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

@ -25,7 +25,12 @@ export default {
{ text: "دوره اعتبار(ثانیه)", value: "time" },
{ text: "برچسب زمان", value: "timeLabel" },
{ text: "ویرایش", value: "operation" },
]
],
snackbar: {
show: false,
color: '',
text: ''
}
}
},
methods: {
@ -81,6 +86,26 @@ export default {
this.loading = false;
this.dialog = false;
}
},
async syncPlugins() {
this.loading = true;
try {
await axios.post('/api/admin/plugins/sync');
this.snackbar = {
show: true,
color: 'success',
text: 'همگام‌سازی افزونه‌ها با موفقیت انجام شد.'
};
await this.loadData();
} catch (error) {
this.snackbar = {
show: true,
color: 'error',
text: 'خطا در همگام‌سازی افزونه‌ها!'
};
}
this.loading = false;
}
},
beforeMount() {
@ -92,6 +117,10 @@ export default {
<template>
<v-toolbar color="toolbar" :title="$t('dialog.plugins') + ' : (' + items.length + ')'">
<v-spacer></v-spacer>
<v-btn color="primary" :loading="loading" @click="syncPlugins" class="ml-2">
<v-icon left>mdi-sync</v-icon>
همگامسازی افزونهها
</v-btn>
</v-toolbar>
<v-container class="pa-0 ma-0">
<v-card :loading="loading ? 'red' : null" :disabled="loading">
@ -169,6 +198,9 @@ export default {
</v-form>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3500" top right>
{{ snackbar.text }}
</v-snackbar>
</template>
<style scoped></style>

View file

@ -73,6 +73,7 @@ export default defineComponent({
inputTokenPrice: 0,
outputTokenPrice: 0,
aiPrompt: '',
aiDebugMode: false,
aiAgentSources: [
{ title: 'GapGPT', value: 'gapgpt', subtitle: 'gapgpt.app' },
{ title: 'AvalAI', value: 'avalai', subtitle: 'avalai.ir' },
@ -164,6 +165,7 @@ export default defineComponent({
this.inputTokenPrice = parseFloat(data.inputTokenPrice) || 0;
this.outputTokenPrice = parseFloat(data.outputTokenPrice) || 0;
this.aiPrompt = data.aiPrompt || '';
this.aiDebugMode = data.aiDebugMode === '1' || data.aiDebugMode === true;
this.loading = false;
})
},
@ -224,7 +226,8 @@ export default defineComponent({
localModelAddress: this.localModelAddress,
inputTokenPrice: this.inputTokenPrice,
outputTokenPrice: this.outputTokenPrice,
aiPrompt: this.aiPrompt
aiPrompt: this.aiPrompt,
aiDebugMode: this.aiDebugMode
};
axios.post('/api/admin/settings/system/info/save', submitData).then((resp) => {
@ -876,6 +879,19 @@ export default defineComponent({
<v-icon size="16" class="mr-1">mdi-information</v-icon>
این متن قبل از هر سوال به هوش مصنوعی ارسال میشود تا رفتار و پاسخدهی آن را کنترل کند
</div>
<v-switch
v-model="aiDebugMode"
label="نمایش اطلاعات دیباگ در خروجی هوش مصنوعی"
color="info"
hide-details="auto"
inset
density="comfortable"
class="mt-6"
></v-switch>
<div class="text-caption text-medium-emphasis mt-1 d-flex align-center">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
اگر این گزینه فعال باشد، اطلاعات دیباگ (debug_info) در خروجی پاسخهای هوش مصنوعی نمایش داده میشود.
</div>
</v-card-text>
</v-card>
</v-col>

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 },

View file

@ -1,6 +1,129 @@
<template>
<div class="chat-container">
<!-- لودینگ بالای صفحه برای عملیات گفتگو -->
<v-progress-linear
v-if="loadingConversation"
color="primary"
indeterminate
absolute
style="top:0; left:0; right:0; z-index:2000;"
/>
<!-- دیالوگ مدیریت گفتوگوها - ساختار بهبود یافته -->
<v-dialog v-model="showConversations" max-width="420" scrollable transition="dialog-bottom-transition">
<v-card class="conversation-dialog-card">
<!-- هدر بهبود یافته -->
<div class="conversation-dialog-header">
<div class="header-content">
<div class="header-icon-wrapper">
<v-icon size="28" color="white">mdi-forum</v-icon>
</div>
<div class="header-text">
<h3 class="header-title">مدیریت گفتوگوها</h3>
<p class="header-subtitle">{{ conversations.length }} گفتوگو موجود</p>
</div>
</div>
<div class="header-actions">
<!-- دکمه حذف همه گفتوگوها با تولتیپ -->
<v-tooltip text="حذف همه گفت‌وگوها" location="bottom">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="white"
variant="text"
:loading="loadingDeleteAll"
icon
size="small"
class="header-action-btn delete-all-btn"
@click="showDeleteAllDialog = true"
>
<v-icon size="20">mdi-delete-sweep</v-icon>
</v-btn>
</template>
</v-tooltip>
<!-- دکمه بستن دیالوگ -->
<v-btn
icon
variant="text"
color="white"
size="small"
class="header-action-btn close-btn"
@click="showConversations = false"
>
<v-icon size="20">mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-list class="py-0" style="min-height: 320px; max-height: 60vh; overflow-y: auto;">
<template v-if="conversations.length">
<template v-for="(conv, idx) in conversations" :key="conv.id">
<v-list-item :active="conv.id === conversationId" @click="switchConversation(conv.id)" class="conv-list-item-v">
<template #prepend>
<v-avatar :color="getAvatarColor(conv.title)" size="36">
<span class="white--text text-h6">{{ conv.title.charAt(0) }}</span>
</v-avatar>
</template>
<v-list-item-title class="font-weight-bold">{{ conv.title }}</v-list-item-title>
<v-list-item-subtitle class="d-flex align-center">
<v-icon size="16" color="grey" class="ml-1">mdi-clock-outline</v-icon>
<span class="mr-1">{{ conv.updatedAt }}</span>
<v-divider vertical class="mx-2" style="height: 18px;"></v-divider>
<v-icon size="16" color="primary" class="ml-1">mdi-message-reply-text</v-icon>
<span>{{ conv.messageCount }}</span>
</v-list-item-subtitle>
<template #append>
<v-btn icon color="error" variant="text" @click.stop="confirmDelete(conv.id)"><v-icon>mdi-delete</v-icon></v-btn>
</template>
</v-list-item>
<v-divider v-if="idx !== conversations.length - 1" class="my-1"></v-divider>
</template>
</template>
<template v-else>
<div class="d-flex flex-column align-center justify-center py-10">
<v-icon size="64" color="grey-lighten-1">mdi-emoticon-happy-outline</v-icon>
<div class="mt-3 text-h6">هنوز گفتوگویی ایجاد نکردهاید!</div>
<div class="text-caption mt-1">برای شروع، روی دکمه زیر کلیک کنید.</div>
</div>
</template>
</v-list>
<div class="conversation-dialog-footer">
<v-btn
color="primary"
block
size="large"
class="new-conversation-btn"
@click="createConversation"
:loading="loadingConversation"
>
<v-icon start size="20">mdi-plus</v-icon>
گفتوگوی جدید
</v-btn>
</div>
<!-- دیالوگ تایید حذف -->
<v-dialog v-model="showDeleteDialog" max-width="320">
<v-card>
<v-card-title class="text-h6">حذف گفتگو</v-card-title>
<v-card-text>آیا از حذف این گفتگو مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDelete" :disabled="loadingDelete" @click="doDeleteConversation">حذف</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- دیالوگ تایید حذف همه -->
<v-dialog v-model="showDeleteAllDialog" max-width="340">
<v-card>
<v-card-title class="text-h6">حذف همه گفتوگوها</v-card-title>
<v-card-text>آیا از حذف همه گفتوگوها مطمئن هستید؟ این عمل قابل بازگشت نیست.</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey" variant="text" @click="showDeleteAllDialog = false">انصراف</v-btn>
<v-btn color="error" variant="flat" :loading="loadingDeleteAll" :disabled="loadingDeleteAll" @click="deleteAllConversations">حذف همه</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</v-card>
</v-dialog>
<!-- ناحیه پیامها -->
<div class="messages-container" ref="messagesContainer">
@ -83,6 +206,21 @@
>
<v-icon>mdi-send</v-icon>
</v-btn>
<!-- دکمه مدیریت گفتوگوها فقط آیکون با تولتیپ -->
<v-tooltip text="مدیریت گفت‌وگوها" location="top">
<template #activator="{ props }">
<v-btn
v-bind="props"
color="info"
size="small"
class="send-button"
@click="showConversations = true"
icon
>
<v-icon>mdi-forum</v-icon>
</v-btn>
</template>
</v-tooltip>
</div>
<!-- دکمههای سریع -->
@ -111,6 +249,9 @@
</v-alert>
</div>
</div>
<v-snackbar v-model="showSnackbar" :color="snackbarColor" location="bottom" timeout="3500">
{{ snackbarText }}
</v-snackbar>
</div>
</template>
@ -118,6 +259,7 @@
import { useNavigationStore } from '@/stores/navigationStore';
import axios from 'axios';
import AIChart from '@/components/widgets/AIChart.vue';
import { h } from 'vue';
export default {
name: "WizardHome",
@ -137,12 +279,23 @@ export default {
aiEnabled: false,
aiStatus: 'checking',
conversationId: null,
showConversations: false,
conversations: [],
quickSuggestions: [
'چطور می‌تونم کمکتون کنم؟',
'سوالی دارید؟',
'نیاز به راهنمایی دارید؟',
'مشکلی پیش اومده؟'
]
'چگونه می‌توانم یک گزارش مالی تهیه کنم؟',
'برای ثبت یک فاکتور جدید چه مراحلی را باید طی کنم؟',
'چطور می‌توانم کاربران جدید به سیستم اضافه کنم؟',
'چگونه می‌توانم تنظیمات کسب‌وکارم را تغییر دهم؟'
],
showDeleteDialog: false,
deleteTargetId: null,
loadingConversation: false,
loadingDelete: false,
showSnackbar: false,
snackbarText: '',
snackbarColor: 'success',
showDeleteAllDialog: false,
loadingDeleteAll: false,
}
},
methods: {
@ -350,7 +503,205 @@ export default {
}
}, 100);
});
}
},
async fetchConversations() {
const res = await axios.post('/api/wizard/conversations/list');
if (res.data.success) {
this.conversations = res.data.items;
}
},
async createConversation() {
this.loadingConversation = true;
const res = await axios.post('/api/wizard/conversations/create', { title: 'گفتگوی جدید' });
if (res.data.success) {
this.showConversations = false;
this.conversationId = res.data.id;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'سلام! گفت‌وگوی جدید ایجاد شد. پیام خود را بنویسید.' }] },
timestamp: new Date()
}
];
await this.fetchConversations();
}
this.loadingConversation = false;
},
async switchConversation(id) {
this.loadingConversation = true;
this.conversationId = id;
this.showConversations = false;
// دریافت پیامهای گفتگو
const res = await axios.post(`/api/wizard/conversations/${id}/messages`);
if (res.data.success) {
this.messages = res.data.items.map(msg => {
const role = (msg.role || '').toLowerCase();
if (role.includes('user')) {
return {
type: 'user',
text: msg.content,
timestamp: new Date(msg.createdAt)
};
} else if (role.includes('ai') || role.includes('assistant') || role.includes('system')) {
let parsed = null;
try {
parsed = msg.content;
let safety = 0;
while (typeof parsed === 'string' && safety < 5) {
parsed = JSON.parse(parsed);
safety++;
}
if (
parsed &&
parsed.data &&
Array.isArray(parsed.data) &&
typeof parsed.data[0] === 'string'
) {
let safety2 = 0;
while (typeof parsed.data[0] === 'string' && safety2 < 5) {
parsed.data[0] = JSON.parse(parsed.data[0]);
safety2++;
}
}
} catch (e) {
parsed = { type: ['text'], data: [{ type: 'text', content: msg.content }] };
}
return {
type: 'ai',
data: parsed,
timestamp: new Date(msg.createdAt)
};
} else {
// پیشفرض: پیام کاربر
return {
type: 'user',
text: msg.content,
timestamp: new Date(msg.createdAt)
};
}
});
}
this.loadingConversation = false;
},
async deleteConversation(id) {
this.loadingConversation = true;
await axios.post(`/api/wizard/conversations/${id}/delete`);
await this.fetchConversations();
if (this.conversationId === id) {
this.conversationId = null;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'گفت‌وگو حذف شد. یک گفت‌وگوی جدید شروع کنید.' }] },
timestamp: new Date()
}
];
}
this.loadingConversation = false;
},
confirmDelete(id) {
this.deleteTargetId = id;
this.showDeleteDialog = true;
},
async doDeleteConversation() {
if (this.deleteTargetId) {
this.loadingDelete = true;
try {
await this.deleteConversation(this.deleteTargetId);
this.showDeleteDialog = false;
this.deleteTargetId = null;
this.snackbarText = 'گفت‌وگو با موفقیت حذف شد.';
this.snackbarColor = 'success';
this.showSnackbar = true;
} catch (e) {
this.snackbarText = 'خطا در حذف گفت‌وگو!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
this.loadingDelete = false;
}
},
async deleteAllConversations() {
this.loadingDeleteAll = true;
try {
const res = await axios.post('/api/wizard/conversations/delete-all');
if (res.data.success) {
this.showDeleteAllDialog = false;
this.snackbarText = 'همه گفت‌وگوها با موفقیت حذف شدند.';
this.snackbarColor = 'success';
this.showSnackbar = true;
await this.fetchConversations();
this.conversationId = null;
this.messages = [
{
type: 'ai',
data: { type: ['text'], data: [{ type: 'text', content: 'همه گفت‌وگوها حذف شدند. یک گفت‌وگوی جدید شروع کنید.' }] },
timestamp: new Date()
}
];
} else {
this.snackbarText = 'خطا در حذف همه گفت‌وگوها!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
} catch (e) {
this.snackbarText = 'خطا در حذف همه گفت‌وگوها!';
this.snackbarColor = 'error';
this.showSnackbar = true;
}
this.loadingDeleteAll = false;
},
getAvatarColor(title) {
// تولید رنگ ثابت بر اساس عنوان گفتگو
const colors = ['primary', 'deep-purple', 'indigo', 'teal', 'cyan', 'pink', 'orange', 'green', 'blue', 'red'];
let hash = 0;
for (let i = 0; i < title.length; i++) hash = title.charCodeAt(i) + ((hash << 5) - hash);
return colors[Math.abs(hash) % colors.length];
},
renderLastMessagePreview(msg) {
if (!msg || typeof msg !== 'string') return {
render() { return h('span', msg || 'بدون پیام'); }
};
let parsed;
try {
parsed = JSON.parse(msg);
} catch (e) {
return {
render() { return h('span', msg); }
};
}
if (parsed && parsed.type && parsed.data && Array.isArray(parsed.data)) {
if (parsed.type.includes('text')) {
const textItem = parsed.data.find(d => d.type === 'text');
if (textItem && textItem.content) {
return {
render() { return h('span', textItem.content); }
};
}
}
if (parsed.type.includes('table')) {
const tableItem = parsed.data.find(d => d.type === 'table');
if (tableItem && tableItem.headers && tableItem.rows) {
return {
render() {
return h('v-simple-table', { class: 'conv-preview-table' }, [
h('thead', [
h('tr', tableItem.headers.map(hd => h('th', hd)))
]),
h('tbody', tableItem.rows.map(row =>
h('tr', row.map(cell => h('td', cell)))
))
]);
}
};
}
}
}
return {
render() { return h('span', msg); }
};
},
},
async mounted() {
@ -362,6 +713,7 @@ export default {
// بررسی وضعیت هوش مصنوعی
await this.checkAIStatus();
await this.fetchConversations();
this.scrollToBottom();
},
@ -395,7 +747,7 @@ export default {
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 16px;
gap: 12px;
min-height: 0;
}
@ -454,7 +806,7 @@ export default {
}
.message-text {
margin: 0 0 4px 0;
margin: 0;
line-height: 1.5;
font-size: 14px;
}
@ -601,5 +953,301 @@ export default {
.message {
max-width: 90%;
}
.quick-actions {
display: none;
}
}
.conversation-item {
cursor: pointer;
border-radius: 12px;
transition: background 0.2s;
}
.conversation-item:hover {
background: #f5f5f5;
}
.conv-dialog-card {
border-radius: 20px;
box-shadow: 0 8px 32px rgba(25, 118, 210, 0.10);
background: linear-gradient(135deg, #f8fafc 0%, #e3eafc 100%);
}
.conv-dialog-title {
background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%);
color: white;
border-top-left-radius: 20px;
border-top-right-radius: 20px;
min-height: 56px;
}
.conv-new-btn {
border-radius: 12px;
font-weight: bold;
font-size: 15px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08);
}
.conv-list {
background: transparent;
}
.conv-list-item {
margin-bottom: 6px;
border-radius: 14px;
transition: background 0.2s, box-shadow 0.2s;
box-shadow: 0 1px 4px rgba(25, 118, 210, 0.04);
border: 1px solid #e3eafc;
}
.conv-list-item:hover, .active-conv {
background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10);
border-color: #90caf9;
}
.empty-state {
opacity: 0.7;
font-size: 15px;
}
.conv-dialog-card-new {
border-radius: 24px;
box-shadow: 0 12px 40px rgba(25, 118, 210, 0.13);
background: linear-gradient(135deg, #fafdff 0%, #e3eafc 100%);
overflow: hidden;
}
.conv-dialog-header-new {
background: linear-gradient(90deg, #1976d2 0%, #42a5f5 100%);
color: white;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
min-height: 64px;
font-size: 20px;
letter-spacing: 0.5px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.08);
}
.conv-dialog-body-new {
padding: 18px 0 0 0;
min-height: 320px;
max-height: 60vh;
overflow-y: auto;
}
.conv-card-item-new {
display: flex;
align-items: center;
background: linear-gradient(90deg, #f5faff 0%, #e3f2fd 100%);
border-radius: 18px;
box-shadow: 0 2px 12px rgba(25, 118, 210, 0.07);
margin-bottom: 14px;
padding: 12px 18px 12px 8px;
transition: box-shadow 0.2s, background 0.2s;
border: 1.5px solid #e3eafc;
cursor: pointer;
position: relative;
}
.conv-card-item-new.active {
background: linear-gradient(90deg, #e3f2fd 0%, #bbdefb 100%);
border-color: #90caf9;
box-shadow: 0 6px 24px rgba(25, 118, 210, 0.13);
}
.conv-card-main {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.conv-title-row {
display: flex;
align-items: center;
gap: 10px;
font-size: 16px;
font-weight: 600;
}
.conv-title {
color: #1976d2;
font-weight: bold;
font-size: 16px;
max-width: 120px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-count {
color: #42a5f5;
font-size: 13px;
font-weight: 500;
display: flex;
align-items: center;
gap: 2px;
}
.conv-last-message {
color: #607d8b;
font-size: 13px;
margin-top: 2px;
max-width: 180px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.conv-time {
color: #90a4ae;
font-size: 12px;
margin-right: 12px;
min-width: 70px;
text-align: left;
}
.conv-delete-btn {
margin-right: 8px;
margin-left: 0;
z-index: 2;
}
.conv-dialog-actions-new {
padding: 18px 24px 18px 24px;
background: transparent;
border-bottom-left-radius: 24px;
border-bottom-right-radius: 24px;
box-shadow: 0 -2px 8px rgba(25, 118, 210, 0.04);
}
.conv-new-btn-new {
border-radius: 14px;
font-weight: bold;
font-size: 16px;
box-shadow: 0 2px 8px rgba(25, 118, 210, 0.10);
padding: 14px 0;
}
.conv-empty-state-new {
opacity: 0.8;
font-size: 16px;
text-align: center;
margin-top: 48px;
}
.conv-list-fade-enter-active, .conv-list-fade-leave-active {
transition: all 0.3s cubic-bezier(.4,0,.2,1);
}
.conv-list-fade-enter-from, .conv-list-fade-leave-to {
opacity: 0;
transform: translateY(20px);
}
.conv-list-item-v {
border-radius: 14px;
margin-bottom: 4px;
transition: background 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.conv-list-item-v:hover, .conv-list-item-v.v-list-item--active {
background: linear-gradient(90deg, #e3f2fd 0%, #f5faff 100%);
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.10);
}
.conv-preview-table {
font-size: 12px;
margin-top: 2px;
background: #f8fafc;
border-radius: 6px;
overflow: hidden;
}
/* استایل‌های جدید دیالوگ گفت‌وگو */
.conversation-dialog-card {
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.conversation-dialog-header {
background: linear-gradient(135deg, #1976d2 0%, #42a5f5 100%);
padding: 24px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.conversation-dialog-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.3) 50%, transparent 100%);
}
.header-content {
display: flex;
align-items: center;
gap: 16px;
}
.header-icon-wrapper {
width: 48px;
height: 48px;
background: rgba(255, 255, 255, 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
backdrop-filter: blur(10px);
}
.header-text {
display: flex;
flex-direction: column;
gap: 4px;
}
.header-title {
color: white;
font-size: 20px;
font-weight: 700;
margin: 0;
line-height: 1.2;
}
.header-subtitle {
color: rgba(255, 255, 255, 0.8);
font-size: 14px;
margin: 0;
font-weight: 400;
}
.header-actions {
display: flex;
align-items: center;
gap: 8px;
}
.header-action-btn {
width: 36px;
height: 36px;
border-radius: 8px;
transition: all 0.3s ease;
}
.header-action-btn:hover {
background: rgba(255, 255, 255, 0.15);
transform: translateY(-1px);
}
.delete-all-btn:hover {
background: rgba(244, 67, 54, 0.2);
}
.close-btn:hover {
background: rgba(255, 255, 255, 0.2);
}
.conversation-dialog-footer {
padding: 20px 24px;
background: #fafafa;
border-top: 1px solid #e0e0e0;
}
.new-conversation-btn {
border-radius: 12px;
font-weight: 600;
font-size: 16px;
text-transform: none;
letter-spacing: 0.5px;
box-shadow: 0 4px 16px rgba(25, 118, 210, 0.2);
transition: all 0.3s ease;
}
.new-conversation-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(25, 118, 210, 0.3);
}
</style>