Compare commits

...

11 commits

35 changed files with 4801 additions and 70 deletions

View file

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

View file

@ -450,6 +450,15 @@ class AdminController extends AbstractController
$resp['parsianGatewayAPI'] = $registryMGR->get('system', key: 'parsianGatewayAPI'); $resp['parsianGatewayAPI'] = $registryMGR->get('system', key: 'parsianGatewayAPI');
$resp['paypingKey'] = $registryMGR->get('system', key: 'paypingKey'); $resp['paypingKey'] = $registryMGR->get('system', key: 'paypingKey');
$resp['bitpayKey'] = $registryMGR->get('system', key: 'bitpayKey'); $resp['bitpayKey'] = $registryMGR->get('system', key: 'bitpayKey');
$resp['inquiryPanel'] = $registryMGR->get('system', key: 'inquiryPanel');
$resp['inquiryZohalAPIKey'] = $registryMGR->get('system', key: 'inquiryZohalAPIKey');
$resp['enablePostalCodeToAddress'] = $registryMGR->get('system', key: 'enablePostalCodeToAddress');
$resp['inquiryPanelEnable'] = $registryMGR->get('system', key: 'inquiryPanelEnable');
$resp['postalCodeToAddressFee'] = $registryMGR->get('system', key: 'postalCodeToAddressFee');
$resp['enableCardToSheba'] = $registryMGR->get('system', key: 'enableCardToSheba');
$resp['cardToShebaFee'] = $registryMGR->get('system', key: 'cardToShebaFee');
$resp['enableAccountToSheba'] = $registryMGR->get('system', key: 'enableAccountToSheba');
$resp['accountToShebaFee'] = $registryMGR->get('system', key: 'accountToShebaFee');
return $this->json($resp); return $this->json($resp);
} }
@ -474,6 +483,15 @@ class AdminController extends AbstractController
$registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']); $registryMGR->update('system', 'parsianGatewayAPI', $params['parsianGatewayAPI']);
$registryMGR->update('system', 'paypingKey', $params['paypingKey']); $registryMGR->update('system', 'paypingKey', $params['paypingKey']);
$registryMGR->update('system', 'bitpayKey', $params['bitpayKey']); $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']);
$entityManager->persist($item); $entityManager->persist($item);
$entityManager->flush(); $entityManager->flush();
return $this->json(['result' => 1]); return $this->json(['result' => 1]);
@ -592,6 +610,7 @@ class AdminController extends AbstractController
$temp['cardPan'] = $item->getCardPan(); $temp['cardPan'] = $item->getCardPan();
$temp['refID'] = $item->getRefID(); $temp['refID'] = $item->getRefID();
$temp['shaba'] = $item->getShaba(); $temp['shaba'] = $item->getShaba();
$temp['amount'] = $item->getAmount();
$temp['dateSubmit'] = $jdate->jdate('Y/n/d H:i', $item->getDateSubmit()); $temp['dateSubmit'] = $jdate->jdate('Y/n/d H:i', $item->getDateSubmit());
$temp['gatePay'] = $item->getGatePay(); $temp['gatePay'] = $item->getGatePay();
$resp[] = $temp; $resp[] = $temp;

View file

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

View file

@ -143,7 +143,8 @@ class CommodityController extends AbstractController
$count += $row->getCommdityCount(); $count += $row->getCommdityCount();
} else { } else {
$count -= $row->getCommdityCount(); $count -= $row->getCommdityCount();
} } }
}
$temp['count'] = $count; $temp['count'] = $count;
} }
return $temp; return $temp;
@ -1071,7 +1072,8 @@ class CommodityController extends AbstractController
if ($content = $request->getContent()) { if ($content = $request->getContent()) {
$params = json_decode($content, true); $params = json_decode($content, true);
} }
if (!array_key_exists('upper', $params) || !array_key_exists('text', $params))
if (!array_key_exists('text', $params))
return $this->json(['result' => -1]); return $this->json(['result' => -1]);
if ($this->isDefaultCategoryName($params['text'])) { if ($this->isDefaultCategoryName($params['text'])) {
@ -1081,6 +1083,29 @@ class CommodityController extends AbstractController
'errorCode' => 'DEFAULT_CATEGORY_NAME' 'errorCode' => 'DEFAULT_CATEGORY_NAME'
]); ]);
} }
if (!array_key_exists('upper', $params)) {
$upper = $entityManager->getRepository(CommodityCat::class)->findOneBy([
'upper' => null,
'bid' => $acc['bid']
]);
if (!$upper) {
$upper = new CommodityCat();
$upper->setBid($acc['bid']);
$upper->setUpper(null);
$upper->setName('دسته بندی ها');
$upper->setRoot(true);
$entityManager->persist($upper);
$entityManager->flush();
}
$cat = new CommodityCat();
$cat->setBid($acc['bid']);
$cat->setRoot(false);
$cat->setName($params['text']);
$cat->setUpper($upper->getId());
$entityManager->persist($cat);
$entityManager->flush();
return $this->json(['result' => 1, 'id' => $cat->getId()]);
}
$upper = $entityManager->getRepository(CommodityCat::class)->find($params['upper']); $upper = $entityManager->getRepository(CommodityCat::class)->find($params['upper']);
if ($upper) { if ($upper) {

View file

@ -0,0 +1,201 @@
<?php
namespace App\Controller\Plugins;
use App\Service\Access;
use App\Service\Extractor;
use App\Service\Log;
use App\Service\registryMGR;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use App\Entity\PluginTaxsettingsKey;
class TaxSettingsController extends AbstractController
{
#[Route('/api/plugins/tax/settings/get', name: 'plugin_tax_settings_get', methods: ['GET'])]
public function plugin_tax_settings_get(EntityManagerInterface $em, Access $access): JsonResponse
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$userId = $this->getUser()->getId();
// دریافت تنظیمات از جدول اختصاصی
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
$settings = [
'taxMemoryId' => $entity ? $entity->getTaxMemoryId() : '',
'economicCode' => $entity ? $entity->getEconomicCode() : '',
'privateKey' => $entity ? $entity->getPrivateKey() : '',
];
return $this->json($settings);
}
#[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
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$params = $request->getPayload()->all();
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$userId = $this->getUser()->getId();
// بررسی وجود رکورد قبلی
$repo = $em->getRepository(PluginTaxsettingsKey::class);
$entity = $repo->findOneBy(['business_id' => $businessId, 'user_id' => $userId]);
if (!$entity) {
$entity = new PluginTaxsettingsKey();
$entity->setBusinessId($businessId);
$entity->setUserId($userId);
$entity->setCreatedAt(new \DateTime());
}
$entity->setPrivateKey($params['privateKey'] ?? '');
$entity->setTaxMemoryId($params['taxMemoryId'] ?? null);
$entity->setEconomicCode($params['economicCode'] ?? null);
$entity->setUpdatedAt(new \DateTime());
$em->persist($entity);
$em->flush();
$log->insert('تنظیمات مالیاتی', 'تنظیمات مالیاتی ذخیره شد (در جدول اختصاصی)', $this->getUser(), $businessId);
return $this->json(['success' => true, 'message' => 'تنظیمات با موفقیت ذخیره شد']);
}
private function generatePrivateKey(): string
{
// تولید کلید خصوصی واقعی با OpenSSL
$config = [
"private_key_bits" => 2048,
"private_key_type" => OPENSSL_KEYTYPE_RSA,
];
$res = openssl_pkey_new($config);
if (!$res) {
throw new \Exception('خطا در تولید کلید خصوصی: ' . openssl_error_string());
}
$privateKey = '';
if (!openssl_pkey_export($res, $privateKey)) {
throw new \Exception('خطا در استخراج کلید خصوصی: ' . openssl_error_string());
}
openssl_pkey_free($res);
return $privateKey;
}
private function generatePublicKey(string $privateKey): string
{
// استخراج کلید عمومی از کلید خصوصی
$res = openssl_pkey_get_private($privateKey);
if (!$res) {
throw new \Exception('خطا در خواندن کلید خصوصی: ' . openssl_error_string());
}
$keyDetails = openssl_pkey_get_details($res);
if (!$keyDetails) {
throw new \Exception('خطا در استخراج جزئیات کلید: ' . openssl_error_string());
}
openssl_pkey_free($res);
return $keyDetails['key'];
}
#[Route('/api/plugins/tax/settings/generate-csr', name: 'plugin_tax_settings_generate_csr', methods: ['POST'])]
public function plugin_tax_settings_generate_csr(Request $request, registryMGR $registryMGR, Access $access, Log $log): JsonResponse
{
$acc = $access->hasRole('plugTaxSettings');
if (!$acc) {
throw $this->createAccessDeniedException('شما دسترسی لازم را ندارید.');
}
$params = $request->getPayload()->all();
// بررسی فیلدهای اجباری
if (empty($params['nationalId']) || empty($params['nameFa']) || empty($params['nameEn']) || empty($params['email'])) {
return $this->json([
'success' => false,
'message' => 'تمام فیلدها الزامی هستند'
]);
}
try {
$privateKey = $this->generatePrivateKey();
$publicKey = $this->generatePublicKey($privateKey);
$csr = $this->generateCSR($privateKey, $params);
// هیچ ذخیره‌ای در دیتابیس انجام نمی‌شود
$businessId = is_object($acc['bid']) ? $acc['bid']->getId() : $acc['bid'];
$log->insert('تنظیمات مالیاتی', 'کلید و CSR تولید شد (بدون ذخیره)', $this->getUser(), $businessId);
return $this->json([
'success' => true,
'message' => 'کلید و CSR با موفقیت تولید شد',
'privateKey' => $privateKey,
'publicKey' => $publicKey,
'csr' => $csr
]);
} catch (\Exception $e) {
return $this->json([
'success' => false,
'message' => 'خطا در تولید کلید و CSR: ' . $e->getMessage()
]);
}
}
private function generateCSR(string $privateKey, array $params): string
{
// تولید CSR واقعی با OpenSSL
$dn = [
"countryName" => "IR",
"stateOrProvinceName" => "Tehran",
"localityName" => "Tehran",
"organizationName" => $params['nameEn'],
"organizationalUnitName" => "Tax Department",
"commonName" => $params['nameFa'],
"emailAddress" => $params['email']
];
// اضافه کردن شناسه ملی به عنوان extension
$config = [
"req" => [
"distinguished_name" => $dn,
"req_extensions" => "v3_req",
"x509_extensions" => "v3_req"
],
"v3_req" => [
"subjectAltName" => "email:" . $params['email'],
"subjectKeyIdentifier" => "hash"
]
];
// ایجاد CSR
$res = openssl_csr_new($dn, $privateKey, [
'config' => $config,
'digest_alg' => 'sha256',
'req_extensions' => 'v3_req'
]);
if (!$res) {
throw new \Exception('خطا در تولید CSR: ' . openssl_error_string());
}
$csr = '';
if (!openssl_csr_export($res, $csr)) {
throw new \Exception('خطا در استخراج CSR: ' . openssl_error_string());
}
return $csr;
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -129,6 +129,12 @@ class Permission
#[ORM\Column(nullable: true)] #[ORM\Column(nullable: true)]
private ?bool $plugGhestaManager = null; private ?bool $plugGhestaManager = null;
#[ORM\Column(nullable: true)]
private ?bool $plugTaxSettings = null;
#[ORM\Column(nullable: true)]
private ?bool $inquiry = null;
public function getId(): ?int public function getId(): ?int
{ {
return $this->id; return $this->id;
@ -590,4 +596,27 @@ class Permission
return $this; return $this;
} }
public function isPlugTaxSettings(): ?bool
{
return $this->plugTaxSettings;
}
public function setPlugTaxSettings(?bool $plugTaxSettings): static
{
$this->plugTaxSettings = $plugTaxSettings;
return $this;
}
public function isInquiry(): ?bool
{
return $this->inquiry;
}
public function setInquiry(?bool $inquiry): static
{
$this->inquiry = $inquiry;
return $this;
}
} }

View file

@ -0,0 +1,53 @@
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: "plugin_taxsettings_keys")]
class PluginTaxsettingsKey
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: "integer")]
private $id;
#[ORM\Column(type: "integer")]
private $business_id;
#[ORM\Column(type: "integer")]
private $user_id;
#[ORM\Column(type: "text", nullable: true)]
private $private_key;
#[ORM\Column(type: "string", length: 64, nullable: true)]
private $tax_memory_id;
#[ORM\Column(type: "string", length: 64, nullable: true)]
private $economic_code;
#[ORM\Column(type: "datetime")]
private $created_at;
#[ORM\Column(type: "datetime")]
private $updated_at;
// Getters and setters ...
public function getId() { return $this->id; }
public function getBusinessId() { return $this->business_id; }
public function setBusinessId($val) { $this->business_id = $val; }
public function getUserId() { return $this->user_id; }
public function setUserId($val) { $this->user_id = $val; }
public function getPrivateKey() { return $this->private_key; }
public function setPrivateKey($val) { $this->private_key = $val; }
public function getTaxMemoryId() { return $this->tax_memory_id; }
public function setTaxMemoryId($val) { $this->tax_memory_id = $val; }
public function getEconomicCode() { return $this->economic_code; }
public function setEconomicCode($val) { $this->economic_code = $val; }
public function getCreatedAt() { return $this->created_at; }
public function setCreatedAt($val) { $this->created_at = $val; }
public function getUpdatedAt() { return $this->updated_at; }
public function setUpdatedAt($val) { $this->updated_at = $val; }
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -173,6 +173,7 @@ const fa_lang = {
reports: "گزارشات", reports: "گزارشات",
settings: "تنظیمات", settings: "تنظیمات",
bid_settings: "تنظیمات کسب‌و‌کار", bid_settings: "تنظیمات کسب‌و‌کار",
tax_settings: "تنظیمات مالیاتی",
print_settings: "چاپ اسناد", print_settings: "چاپ اسناد",
user_perms: "کاربران و دسترسی‌ها", user_perms: "کاربران و دسترسی‌ها",
avatar_settings: "نمایه و مهر کسب‌و‌کار", avatar_settings: "نمایه و مهر کسب‌و‌کار",
@ -193,9 +194,12 @@ const fa_lang = {
plugins_invoices: "صورت حساب‌ها", plugins_invoices: "صورت حساب‌ها",
repservice: "مدیریت تعمیرگاه", repservice: "مدیریت تعمیرگاه",
repservice_reqs: "درخواست‌ها", repservice_reqs: "درخواست‌ها",
inquiry: "استعلامات",
hrm: 'منابع انسانی', hrm: 'منابع انسانی',
hrm_docs: 'سند حقوق', hrm_docs: 'سند حقوق',
buysellByPerson: "گزارش خرید و فروش های اشخاص", buysellByPerson: "گزارش خرید و فروش های اشخاص",
tax_system: "سامانه مودیان مالیاتی",
tax_invoices: "صورتحساب‌ ها",
}, },
time: { time: {
month: "{id} ماه", month: "{id} ماه",
@ -268,6 +272,19 @@ const fa_lang = {
"fetchError": "خطا در دریافت", "fetchError": "خطا در دریافت",
"cancel": "لغو", "cancel": "لغو",
"confirm": "تأیید", "confirm": "تأیید",
"updateSourceTitle": "منبع به‌روزرسانی",
"updateSourceLabel": "آدرس منبع به‌روزرسانی",
"changeSourceButton": "تغییر منبع",
"sourceUrlRequired": "لطفاً آدرس منبع به‌روزرسانی را وارد کنید",
"changingSourceMessage": "در حال تغییر منبع به‌روزرسانی...",
"sourceChangeSuccess": "منبع به‌روزرسانی با موفقیت تغییر یافت",
"sourceChangeError": "خطایی در تغییر منبع به‌روزرسانی رخ داد",
"notGitRepository": "این پروژه یک مخزن Git نیست",
"invalidRepositoryUrl": "آدرس مخزن نامعتبر است. لطفاً یک آدرس HTTP یا SSH معتبر وارد کنید.",
"repositoryUrlError": "خطا در دریافت آدرس مخزن",
"repositoryChangeError": "خطا در تغییر آدرس مخزن",
"repositoryConnectionError": "خطا در اتصال به مخزن جدید",
"repositoryChangeSuccess": "آدرس مخزن با موفقیت تغییر یافت و اتصال برقرار شد",
}, },
static: { static: {
not_found: "صفحه مورد نظر یافت نشد", not_found: "صفحه مورد نظر یافت نشد",
@ -799,6 +816,19 @@ const fa_lang = {
sms_settings_plug_accpro_pass_cheque_input: "واگذاری چک", sms_settings_plug_accpro_pass_cheque_input: "واگذاری چک",
sms_settings_reject_cheque_input: "برگشت چک", sms_settings_reject_cheque_input: "برگشت چک",
sms_settings_plug_accpro_reject_cheque_input: "برگشت چک", sms_settings_plug_accpro_reject_cheque_input: "برگشت چک",
inquiry_zohal_api_key: "کلید API زحل",
inquiry_zohal_api_key_des: "کلید API زحل برای دریافت اطلاعات از سامانه زحل",
inquiry_zohal_api_key_placeholder: "کلید API زحل",
inquiry_zohal_api_key_label: "کلید API زحل",
enable_postalcode_to_address: "تبدیل کد پستی به آدرس",
postalcode_to_address_fee: "کارمزد تبدیل کد پستی به آدرس",
enable_card_to_sheba: "تبدیل شماره کارت به شبا",
card_to_sheba_fee: "کارمزد تبدیل شماره کارت به شبا",
enable_account_to_sheba: "تبدیل حساب به شبا",
account_to_sheba_fee: "کارمزد تبدیل حساب به شبا",
inquiry_panel_enable: "فعال سازی پنل سامانه استعلامات",
inquiry_panel: "پنل سامانه استعلامات",
inquiry_panel_zohal: "زحل",
app_site: "آدرس اینترفیس(رابط کاربری)", app_site: "آدرس اینترفیس(رابط کاربری)",
keywords: "کلیدواژه‌ها با کاما (,) از هم جدا شوند", keywords: "کلیدواژه‌ها با کاما (,) از هم جدا شوند",
zarinpal_api: "کد API زرین‌پال", zarinpal_api: "کد API زرین‌پال",

View file

@ -510,6 +510,18 @@ const router = createRouter({
component: () => component: () =>
import('../views/acc/settings/extramoneys.vue'), import('../views/acc/settings/extramoneys.vue'),
}, },
{
path: 'plugins/tax/settings',
name: 'business_tax_settings',
component: () =>
import('../views/acc/tax/tax-settings.vue'),
},
{
path: 'plugins/tax/invoices/list',
name: 'tax_invoices_list',
component: () =>
import('../views/acc/tax/invoices/list.vue'),
},
{ {
path: 'business/logs', path: 'business/logs',
name: 'business_logs', name: 'business_logs',
@ -988,6 +1000,12 @@ const router = createRouter({
component: () => component: () =>
import('../views/acc/plugins/hrm/docs/view.vue'), import('../views/acc/plugins/hrm/docs/view.vue'),
}, },
{
path: 'inquiry/panel',
name: 'inquiry_panel',
component: () =>
import('../views/acc/inquiry/panel.vue'),
},
], ],
}, },
{ {

View file

@ -169,8 +169,9 @@ export default {
{ path: '/acc/business/extramoneys', key: '-', label: this.$t('drawer.extra_moneys'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('accpro') }, { path: '/acc/business/extramoneys', key: '-', label: this.$t('drawer.extra_moneys'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('accpro') },
{ path: '/acc/business/logs', key: '=', label: this.$t('drawer.history'), ctrl: true, shift: true, permission: () => this.permissions.log }, { path: '/acc/business/logs', key: '=', label: this.$t('drawer.history'), ctrl: true, shift: true, permission: () => this.permissions.log },
{ path: '/acc/plugin/repservice/order/list', key: '[', label: this.$t('drawer.repservice_reqs'), ctrl: true, shift: true, permission: () => this.permissions.plugRepservice && this.isPluginActive('repservice') }, { path: '/acc/plugin/repservice/order/list', key: '[', label: this.$t('drawer.repservice_reqs'), ctrl: true, shift: true, permission: () => this.permissions.plugRepservice && this.isPluginActive('repservice') },
{ path: '/acc/sms/panel', key: ']', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/inquiry/panel', key: ']', label: this.$t('drawer.inquiry'), ctrl: true, shift: true, permission: () => true },
{ path: '/acc/printers/list', key: ';', label: this.$t('drawer.cloud_printers'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/printers/list', key: ';', label: this.$t('drawer.cloud_printers'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/sms/panel', key: '`', label: this.$t('drawer.sms_panel'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/archive/list', key: '\'', label: this.$t('drawer.archive_files'), ctrl: true, shift: true, permission: () => this.permissions.archiveUpload || this.permissions.archiveMod || this.permissions.archiveDelete }, { path: '/acc/archive/list', key: '\'', label: this.$t('drawer.archive_files'), ctrl: true, shift: true, permission: () => this.permissions.archiveUpload || this.permissions.archiveMod || this.permissions.archiveDelete },
{ path: '/acc/archive/order/new', key: ',', label: this.$t('drawer.archive_order'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/archive/order/new', key: ',', label: this.$t('drawer.archive_order'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/archive/order/list', key: '.', label: this.$t('drawer.archive_log'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/archive/order/list', key: '.', label: this.$t('drawer.archive_log'), ctrl: true, shift: true, permission: () => this.permissions.owner },
@ -179,6 +180,8 @@ export default {
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner }, { path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs }, { path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs },
{ path: '/acc/plugins/ghesta/list', key: 'G', label: this.$t('drawer.ghesta_invoices'), ctrl: true, shift: true, permission: () => this.isPluginActive('ghesta') && this.permissions.plugGhestaManager }, { path: '/acc/plugins/ghesta/list', key: 'G', label: this.$t('drawer.ghesta_invoices'), ctrl: true, shift: true, permission: () => this.isPluginActive('ghesta') && this.permissions.plugGhestaManager },
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
]; ];
}, },
restorePermissions(shortcuts) { restorePermissions(shortcuts) {
@ -788,6 +791,13 @@ export default {
</template> </template>
</v-list-item> </v-list-item>
</v-list-group> </v-list-group>
<v-list-item to="/acc/inquiry/panel">
<template v-slot:prepend><v-icon icon="mdi-magnify"></v-icon></template>
<v-list-item-title>
{{ $t('drawer.inquiry') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/inquiry/panel') }}</span>
</v-list-item-title>
</v-list-item>
<v-list-group v-show="isPluginActive('hrm') && permissions.plugHrmDocs"> <v-list-group v-show="isPluginActive('hrm') && permissions.plugHrmDocs">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.hrm')"> <v-list-item class="text-dark" v-bind="props" :title="$t('drawer.hrm')">
@ -828,6 +838,23 @@ export default {
</template> </template>
</v-list-item> </v-list-item>
</v-list-group> </v-list-group>
<v-list-group v-show="isPluginActive('taxsettings') && permissions.plugTaxSettings">
<template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.tax_system')">
<template v-slot:prepend><v-icon icon="mdi-file-document-multiple" color="primary"></v-icon></template>
</v-list-item>
</template>
<v-list-item to="/acc/plugins/tax/invoices/list">
<v-list-item-title>
{{ $t('drawer.tax_invoices') }}
</v-list-item-title>
</v-list-item>
<v-list-item to="/acc/plugins/tax/settings">
<v-list-item-title>
{{ $t('drawer.tax_settings') }}
</v-list-item-title>
</v-list-item>
</v-list-group>
<v-list-item class="text-dark" v-if="permissions.owner" to="/acc/sms/panel"> <v-list-item class="text-dark" v-if="permissions.owner" to="/acc/sms/panel">
<template v-slot:prepend><v-icon icon="mdi-message-cog" color="primary"></v-icon></template> <template v-slot:prepend><v-icon icon="mdi-message-cog" color="primary"></v-icon></template>
<v-list-item-title> <v-list-item-title>

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -241,6 +241,12 @@ const router = createRouter({
component: () => component: () =>
import ('../views/settings/extramoneys.vue'), 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', path: '/acc/business/logs',
name: 'business_logs', name: 'business_logs',

View file

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

View file

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

View file

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

View file

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

View file

@ -7,6 +7,12 @@ export default defineComponent({
name: "system", name: "system",
data: () => { data: () => {
return { return {
activeTab: 0,
tabs: [
{ title: 'تنظیمات پایه', icon: 'mdi-cog' },
{ title: 'درگاه‌های پرداخت', icon: 'mdi-credit-card' },
{ title: 'پنل استعلامات', icon: 'mdi-magnify' }
],
gatepays: [ gatepays: [
{ {
title: 'زرین‌پال', title: 'زرین‌پال',
@ -29,6 +35,13 @@ export default defineComponent({
props: { subtitle: 'bitpay.ir' }, props: { subtitle: 'bitpay.ir' },
}, },
], ],
inquiryPanel: [
{
title: 'پنل زحل',
value: 'zohal',
props: { subtitle: 'zohal.ir' },
},
],
systemInfo: { systemInfo: {
keywords: '', keywords: '',
description: '', description: '',
@ -38,6 +51,15 @@ export default defineComponent({
parsianGatewayAPI: '', parsianGatewayAPI: '',
paypingKey: '', paypingKey: '',
bitpayKey: '', bitpayKey: '',
inquiryPanel: '',
inquiryZohalAPIKey: '',
enablePostalCodeToAddress: false,
inquiryPanelEnable: false,
postalCodeToAddressFee: 0,
enableCardToSheba: false,
cardToShebaFee: 0,
enableAccountToSheba: false,
accountToShebaFee: 0,
}, },
loading: true, loading: true,
} }
@ -47,12 +69,67 @@ export default defineComponent({
this.loading = true; this.loading = true;
axios.post('/api/admin/settings/system/info') axios.post('/api/admin/settings/system/info')
.then((response) => { .then((response) => {
this.systemInfo = response.data; // Convert string values to proper types for switches and selects
const data = response.data;
this.systemInfo = {
...data,
enablePostalCodeToAddress: data.enablePostalCodeToAddress === '1' || data.enablePostalCodeToAddress === true,
inquiryPanelEnable: data.inquiryPanelEnable === '1' || data.inquiryPanelEnable === true,
enableCardToSheba: data.enableCardToSheba === '1' || data.enableCardToSheba === true,
enableAccountToSheba: data.enableAccountToSheba === '1' || data.enableAccountToSheba === true,
activeGateway: data.activeGateway || 'zarinpal',
inquiryPanel: data.inquiryPanel || 'zohal'
};
this.loading = false; this.loading = false;
}) })
}, },
submit() { submit() {
this.loading = true; this.loading = true;
// Validation: if inquiry panel is enabled, a panel must be selected
if (this.systemInfo.inquiryPanelEnable && !this.systemInfo.inquiryPanel) {
Swal.fire({
text: 'در صورت فعال بودن پنل سامانه استعلامات، حتماً باید یک پنل انتخاب شود.',
icon: 'error',
confirmButtonText: 'قبول',
});
this.loading = false;
return;
}
// Validation: if postal code to address is enabled, fee must be set
if (this.systemInfo.enablePostalCodeToAddress && this.systemInfo.postalCodeToAddressFee < 0) {
Swal.fire({
text: 'کارمزد تبدیل کد پستی به آدرس نمی‌تواند منفی باشد.',
icon: 'error',
confirmButtonText: 'قبول',
});
this.loading = false;
return;
}
// Validation: if card to sheba is enabled, fee must be set
if (this.systemInfo.enableCardToSheba && this.systemInfo.cardToShebaFee < 0) {
Swal.fire({
text: 'کارمزد تبدیل شماره کارت به شبا نمی‌تواند منفی باشد.',
icon: 'error',
confirmButtonText: 'قبول',
});
this.loading = false;
return;
}
// Validation: if account to sheba is enabled, fee must be set
if (this.systemInfo.enableAccountToSheba && this.systemInfo.accountToShebaFee < 0) {
Swal.fire({
text: 'کارمزد تبدیل حساب به شبا نمی‌تواند منفی باشد.',
icon: 'error',
confirmButtonText: 'قبول',
});
this.loading = false;
return;
}
axios.post('/api/admin/settings/system/info/save', this.systemInfo).then((resp) => { axios.post('/api/admin/settings/system/info/save', this.systemInfo).then((resp) => {
this.loading = false; this.loading = false;
if (resp.data.result == 1) { if (resp.data.result == 1) {
@ -62,6 +139,13 @@ export default defineComponent({
confirmButtonText: 'قبول', confirmButtonText: 'قبول',
}); });
} }
}).catch((error) => {
this.loading = false;
Swal.fire({
text: 'خطا در ذخیره تنظیمات. لطفاً دوباره تلاش کنید.',
icon: 'error',
confirmButtonText: 'قبول',
});
}) })
} }
@ -75,46 +159,491 @@ export default defineComponent({
<template> <template>
<v-toolbar color="toolbar" :title="$t('pages.manager.system_settings_basic')"> <v-toolbar color="toolbar" :title="$t('pages.manager.system_settings_basic')">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-tooltip text="ذخیره تنظیمات" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
icon
@click="submit()"
:loading="loading"
color="primary"
class="ml-2"
>
<v-icon>mdi-content-save</v-icon>
</v-btn>
</template>
</v-tooltip>
</v-toolbar> </v-toolbar>
<v-container class="pa-0"> <v-container class="pa-0">
<v-card :loading="loading ? 'red' : null" :disabled="loading"> <v-card :loading="loading" :disabled="loading">
<v-card-text class=""> <v-tabs v-model="activeTab" color="primary" grow>
<v-row class="mb-2"> <v-tab
<v-col cols="12" sm="12" md="12"> v-for="(tab, index) in tabs"
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.app_site')" :key="index"
v-model="systemInfo.appSite" type="text" prepend-inner-icon="mdi-card-text" :value="index"
:rules="[() => systemInfo.appSite.length > 0 || $t('validator.required')]"></v-text-field> class="text-none"
</v-col> >
<v-col cols="12" sm="12" md="4"> <v-icon start>{{ tab.icon }}</v-icon>
<v-select v-model="systemInfo.activeGateway" hide-details="auto" prepend-inner-icon="mdi-signal" :items="gatepays" item-title="title" {{ tab.title }}
item-value="value" label="Select" single-line> </v-tab>
</v-select> </v-tabs>
</v-col>
<v-col cols="12" sm="12" md="4"> <v-window v-model="activeTab">
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.zarinpal_api')" <!-- تب اول: تنظیمات پایه -->
v-model="systemInfo.zarinpal" type="text" prepend-inner-icon="mdi-text"></v-text-field> <v-window-item :value="0">
</v-col> <v-card-text class="pa-8">
<v-col cols="12" sm="12" md="4"> <v-row class="mb-6">
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.parsian_api')" <v-col cols="12">
v-model="systemInfo.parsianGatewayAPI" type="text" prepend-inner-icon="mdi-text"></v-text-field> <v-card variant="outlined" class="pa-6" elevation="0">
</v-col> <v-card-text class="pa-0">
<v-col cols="12" sm="12" md="4"> <v-row>
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.payping_api')" <v-col cols="12" sm="12" md="8" lg="6">
v-model="systemInfo.paypingKey" type="text" prepend-inner-icon="mdi-text"></v-text-field> <v-text-field
</v-col> class=""
<v-col cols="12" sm="12" md="4"> hide-details="auto"
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.bitpay_api')" :label="$t('pages.manager.app_site')"
v-model="systemInfo.bitpayKey" type="text" prepend-inner-icon="mdi-text"></v-text-field> v-model="systemInfo.appSite"
</v-col> type="text"
<v-col cols="12" sm="12" md="12"> prepend-inner-icon="mdi-link"
<v-btn type="submit" @click="submit()" color="primary" prepend-icon="mdi-content-save" :loading="loading"> :rules="[() => systemInfo.appSite.length > 0 || $t('validator.required')]"
{{ $t('dialog.save') }} variant="outlined"
</v-btn> density="comfortable"
placeholder="https://example.com"
></v-text-field>
<div class="text-caption text-medium-emphasis mt-2 d-flex align-center">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
آدرس اصلی سایت که در سیستم استفاده میشود
</div>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- تب دوم: درگاههای پرداخت -->
<v-window-item :value="1">
<v-card-text class="pa-8">
<v-row class="mb-6">
<v-col cols="12">
<v-card variant="outlined" class="pa-6" elevation="0">
<v-card-text class="pa-0">
<v-row>
<v-col cols="12" sm="12" md="6" lg="4">
<v-select
v-model="systemInfo.activeGateway"
hide-details="auto"
prepend-inner-icon="mdi-check-circle"
:items="gatepays"
item-title="title"
item-value="value"
label="درگاه فعال"
single-line
variant="outlined"
density="comfortable"
></v-select>
<div class="text-caption text-medium-emphasis mt-2 d-flex align-center">
<v-icon size="16" class="mr-1">mdi-information</v-icon>
درگاه پرداخت پیشفرض سیستم
</div>
</v-col>
</v-row>
<v-divider class="my-6"></v-divider>
<div class="d-flex align-center mb-4">
<v-icon size="20" color="secondary" class="mr-2">mdi-key</v-icon>
<h4 class="text-subtitle-1 font-weight-medium">کلیدهای API</h4>
</div>
<v-row>
<v-col cols="12" sm="12" md="6" lg="4">
<v-text-field
class=""
hide-details="auto"
:label="$t('pages.manager.zarinpal_api')"
v-model="systemInfo.zarinpal"
type="text"
prepend-inner-icon="mdi-shield-key"
variant="outlined"
density="comfortable"
placeholder="کلید API زرین‌پال"
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="6" lg="4">
<v-text-field
class=""
hide-details="auto"
:label="$t('pages.manager.parsian_api')"
v-model="systemInfo.parsianGatewayAPI"
type="text"
prepend-inner-icon="mdi-shield-key"
variant="outlined"
density="comfortable"
placeholder="کلید API پارسیان"
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="6" lg="4">
<v-text-field
class=""
hide-details="auto"
:label="$t('pages.manager.payping_api')"
v-model="systemInfo.paypingKey"
type="text"
prepend-inner-icon="mdi-shield-key"
variant="outlined"
density="comfortable"
placeholder="کلید API پی‌پینگ"
></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="6" lg="4">
<v-text-field
class=""
hide-details="auto"
:label="$t('pages.manager.bitpay_api')"
v-model="systemInfo.bitpayKey"
type="text"
prepend-inner-icon="mdi-shield-key"
variant="outlined"
density="comfortable"
placeholder="کلید API بیت‌پی"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
<!-- تب سوم: پنل استعلامات -->
<v-window-item :value="2">
<v-card-text class="pa-8">
<v-row class="mb-6">
<v-col cols="12">
<!-- تنظیمات اصلی پنل -->
<v-card variant="outlined" class="pa-6 mb-6" elevation="0">
<v-card-title class="text-subtitle-1 font-weight-medium pb-3 d-flex align-center">
<v-icon start class="mr-2" color="secondary">mdi-cog</v-icon>
تنظیمات اصلی
</v-card-title>
<v-card-text class="pa-0">
<v-row>
<v-col cols="12" sm="12" md="6">
<v-switch
v-model="systemInfo.inquiryPanelEnable"
:label="$t('pages.manager.inquiry_panel_enable')"
color="primary"
hide-details="auto"
inset
density="compact"
></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>
فعال/غیرفعال کردن پنل استعلامات
</div>
</v-col>
<v-col cols="12" sm="12" md="6">
<v-select
v-model="systemInfo.inquiryPanel"
hide-details="auto"
prepend-inner-icon="mdi-view-dashboard"
:items="inquiryPanel"
item-title="title"
item-value="value"
label="انتخاب پنل"
single-line
variant="outlined"
density="comfortable"
></v-select>
</v-col>
<v-col cols="12" sm="12" md="12">
<v-text-field
class=""
hide-details="auto"
:label="$t('pages.manager.inquiry_zohal_api_key')"
v-model="systemInfo.inquiryZohalAPIKey"
type="text"
prepend-inner-icon="mdi-key"
variant="outlined"
density="comfortable"
placeholder="کلید API پنل زحل"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
<!-- سرویسهای تبدیل -->
<div class="d-flex align-center mb-6">
<div class="d-flex align-center bg-primary-lighten-5 pa-3 rounded-lg">
<v-icon size="28" color="primary" class="mr-3">mdi-sync</v-icon>
<div>
<h4 class="text-h6 font-weight-medium text-primary mb-1">سرویسهای تبدیل</h4>
<p class="text-caption text-medium-emphasis mb-0">مدیریت قابلیتهای تبدیل اطلاعات</p>
</div>
</div>
</div>
<v-row class="mb-4">
<!-- تبدیل کد پستی به آدرس -->
<v-col cols="12" sm="12" md="4">
<v-card
variant="outlined"
class="service-card h-100"
elevation="0"
:class="{ 'service-card-active': systemInfo.enablePostalCodeToAddress }"
>
<div class="service-card-header bg-success-lighten-5 pa-4">
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center">
<v-icon size="32" color="success" class="mr-3">mdi-map-marker</v-icon>
<div>
<h5 class="text-subtitle-1 font-weight-medium text-success mb-1">تبدیل کد پستی به آدرس</h5>
<p class="text-caption text-medium-emphasis mb-0">تبدیل خودکار کد پستی به آدرس کامل</p>
</div>
</div>
<v-chip
:color="systemInfo.enablePostalCodeToAddress ? 'success' : 'grey'"
size="small"
variant="flat"
>
{{ systemInfo.enablePostalCodeToAddress ? 'فعال' : 'غیرفعال' }}
</v-chip>
</div>
</div>
<v-card-text class="pa-4">
<v-row>
<v-col cols="12" class="mb-3">
<v-switch
v-model="systemInfo.enablePostalCodeToAddress"
:label="$t('pages.manager.enable_postalcode_to_address')"
color="success"
hide-details="auto"
inset
density="comfortable"
></v-switch>
</v-col>
<v-col cols="12">
<v-text-field
v-model.number="systemInfo.postalCodeToAddressFee"
:label="$t('pages.manager.postalcode_to_address_fee')"
type="number"
min="0"
prepend-inner-icon="mdi-currency-usd"
hide-details="auto"
suffix="ریال"
density="comfortable"
variant="outlined"
:disabled="!systemInfo.enablePostalCodeToAddress"
:rules="[v => v >= 0 || 'کارمزد نمی‌تواند منفی باشد']"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<!-- تبدیل شماره کارت به شبا -->
<v-col cols="12" sm="12" md="4">
<v-card
variant="outlined"
class="service-card h-100"
elevation="0"
:class="{ 'service-card-active': systemInfo.enableCardToSheba }"
>
<div class="service-card-header bg-info-lighten-5 pa-4">
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center">
<v-icon size="32" color="info" class="mr-3">mdi-credit-card</v-icon>
<div>
<h5 class="text-subtitle-1 font-weight-medium text-info mb-1">تبدیل شماره کارت به شبا</h5>
<p class="text-caption text-medium-emphasis mb-0">تبدیل شماره کارت بانکی به شماره شبا</p>
</div>
</div>
<v-chip
:color="systemInfo.enableCardToSheba ? 'info' : 'grey'"
size="small"
variant="flat"
>
{{ systemInfo.enableCardToSheba ? 'فعال' : 'غیرفعال' }}
</v-chip>
</div>
</div>
<v-card-text class="pa-4">
<v-row>
<v-col cols="12" class="mb-3">
<v-switch
v-model="systemInfo.enableCardToSheba"
:label="$t('pages.manager.enable_card_to_sheba')"
color="info"
hide-details="auto"
inset
density="comfortable"
></v-switch>
</v-col>
<v-col cols="12">
<v-text-field
v-model.number="systemInfo.cardToShebaFee"
:label="$t('pages.manager.card_to_sheba_fee')"
type="number"
min="0"
prepend-inner-icon="mdi-currency-usd"
hide-details="auto"
suffix="ریال"
density="comfortable"
variant="outlined"
:disabled="!systemInfo.enableCardToSheba"
:rules="[v => v >= 0 || 'کارمزد نمی‌تواند منفی باشد']"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
<!-- تبدیل حساب به شبا -->
<v-col cols="12" sm="12" md="4">
<v-card
variant="outlined"
class="service-card h-100"
elevation="0"
:class="{ 'service-card-active': systemInfo.enableAccountToSheba }"
>
<div class="service-card-header bg-warning-lighten-5 pa-4">
<div class="d-flex align-center justify-space-between mb-3">
<div class="d-flex align-center">
<v-icon size="32" color="warning" class="mr-3">mdi-bank</v-icon>
<div>
<h5 class="text-subtitle-1 font-weight-medium text-warning mb-1">تبدیل حساب به شبا</h5>
<p class="text-caption text-medium-emphasis mb-0">تبدیل شماره حساب بانکی به شماره شبا</p>
</div>
</div>
<v-chip
:color="systemInfo.enableAccountToSheba ? 'warning' : 'grey'"
size="small"
variant="flat"
>
{{ systemInfo.enableAccountToSheba ? 'فعال' : 'غیرفعال' }}
</v-chip>
</div>
</div>
<v-card-text class="pa-4">
<v-row>
<v-col cols="12" class="mb-3">
<v-switch
v-model="systemInfo.enableAccountToSheba"
:label="$t('pages.manager.enable_account_to_sheba')"
color="warning"
hide-details="auto"
inset
density="comfortable"
></v-switch>
</v-col>
<v-col cols="12">
<v-text-field
v-model.number="systemInfo.accountToShebaFee"
:label="$t('pages.manager.account_to_sheba_fee')"
type="number"
min="0"
prepend-inner-icon="mdi-currency-usd"
hide-details="auto"
suffix="ریال"
density="comfortable"
variant="outlined"
:disabled="!systemInfo.enableAccountToSheba"
:rules="[v => v >= 0 || 'کارمزد نمی‌تواند منفی باشد']"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-col>
</v-row>
</v-card-text>
</v-window-item>
</v-window>
</v-card>
</v-container> </v-container>
</template> </template>
<style scoped></style> <style scoped>
.service-card {
transition: all 0.3s ease;
border: 2px solid transparent;
position: relative;
overflow: hidden;
}
.service-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1) !important;
}
.service-card-active {
border-color: var(--v-primary-base);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
}
.service-card-active::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, var(--v-primary-base), var(--v-secondary-base));
}
.service-card-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
position: relative;
}
.service-card-header::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent, rgba(0, 0, 0, 0.1), transparent);
}
/* انیمیشن برای chip ها */
.v-chip {
transition: all 0.3s ease;
}
.v-chip:hover {
transform: scale(1.05);
}
/* استایل برای فیلدهای غیرفعال */
.v-text-field--disabled {
opacity: 0.6;
}
/* بهبود ظاهر switch */
.v-switch {
margin-bottom: 8px;
}
/* استایل برای آیکون‌ها */
.service-card .v-icon {
transition: transform 0.3s ease;
}
.service-card:hover .v-icon {
transform: scale(1.1);
}
</style>

View file

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

View file

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