bug fix in progress in AI

This commit is contained in:
Hesabix 2025-05-03 21:37:39 +00:00
parent 1537bf238f
commit c8b2c53e2b
39 changed files with 3369 additions and 1103 deletions

View file

@ -540,6 +540,7 @@ class BusinessController extends AbstractController
'plugAccproRfsell' => true,
'plugAccproRfbuy' => true,
'plugAccproCloseYear' => true,
'plugAccproPresell' => true,
'plugRepservice' => true,
];
} elseif ($perm) {
@ -581,10 +582,10 @@ class BusinessController extends AbstractController
'plugAccproRfbuy' => $perm->isPlugAccproRfbuy(),
'plugAccproCloseYear' => $perm->isPlugAccproCloseYear(),
'plugRepservice' => $perm->isPlugRepservice(),
'plugAccproPresell' => $perm->isPlugAccproPresell(),
];
}
return $this->json($result);
return $this->json(['result' => -1]);
}
#[Route('/api/business/save/user/permissions', name: 'api_business_save_user_permission')]
@ -646,6 +647,7 @@ class BusinessController extends AbstractController
$perm->setPlugAccproCloseYear($params['plugAccproCloseYear']);
$perm->setPlugAccproRfbuy($params['plugAccproRfbuy']);
$perm->setPlugAccproRfsell($params['plugAccproRfsell']);
$perm->setPlugAccproPresell($params['plugAccproPresell']);
$perm->setPlugAccproAccounting($params['plugAccproAccounting']);
$perm->setPlugRepservice($params['plugRepservice']);
$entityManager->persist($perm);

View file

@ -31,6 +31,13 @@ use Psr\Log\LoggerInterface;
class CommodityController extends AbstractController
{
private const DEFAULT_ROOT_CATEGORY = 'دسته بندی ها';
private const DEFAULT_NO_CATEGORY = 'بدون دسته‌بندی';
private function isDefaultCategoryName(string $name): bool
{
return $name === self::DEFAULT_ROOT_CATEGORY || $name === self::DEFAULT_NO_CATEGORY;
}
#[Route('/api/commodities/search', name: 'search_commodities')]
public function searchCommodities(
@ -1063,6 +1070,15 @@ class CommodityController extends AbstractController
}
if (!array_key_exists('upper', $params) || !array_key_exists('text', $params))
return $this->json(['result' => -1]);
if ($this->isDefaultCategoryName($params['text'])) {
return $this->json([
'result' => 4,
'message' => 'این نام برای دسته‌بندی مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_NAME'
]);
}
$upper = $entityManager->getRepository(CommodityCat::class)->find($params['upper']);
if ($upper) {
if ($upper->getBid() == $acc['bid']) {
@ -1090,9 +1106,26 @@ class CommodityController extends AbstractController
}
if (!array_key_exists('id', $params) || !array_key_exists('text', $params))
return $this->json(['result' => -1]);
if ($this->isDefaultCategoryName($params['text'])) {
return $this->json([
'result' => 4,
'message' => 'این نام برای دسته‌بندی مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_NAME'
]);
}
$node = $entityManager->getRepository(CommodityCat::class)->find($params['id']);
if ($node) {
if ($node->getBid() == $acc['bid']) {
// بررسی دسته‌بندی پیش‌فرض
if ($this->isDefaultCategoryName($node->getName())) {
return $this->json([
'result' => 4,
'message' => 'ویرایش دسته‌بندی پیش‌فرض مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_EDIT'
]);
}
$node->setName($params['text']);
$entityManager->persist($node);
$entityManager->flush();
@ -1150,7 +1183,7 @@ class CommodityController extends AbstractController
public function createDefaultCat(Business $bid, EntityManagerInterface $en): array
{
$item = new CommodityCat();
$item->setName('دسته بندی ها');
$item->setName(self::DEFAULT_ROOT_CATEGORY);
$item->setUpper(null);
$item->setBid($bid);
$item->setRoot(true);
@ -1160,7 +1193,7 @@ class CommodityController extends AbstractController
$child = new CommodityCat();
$child->setUpper($item->getId());
$child->setBid($bid);
$child->setName('بدون دسته‌بندی');
$child->setName(self::DEFAULT_NO_CATEGORY);
$en->persist($child);
$en->flush();
return [$item, $child];
@ -1522,4 +1555,64 @@ class CommodityController extends AbstractController
$log->insert('کالا/خدمات', 'قیمت تعدادی از کالا‌ها به صورت گروهی ویرایش شد.', $this->getUser(), $acc['bid']->getId());
return $this->json($extractor->operationSuccess());
}
#[Route('/api/commodity/cat/delete', name: 'app_commodity_cat_delete', methods: ['POST'])]
public function app_commodity_cat_delete(
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('commodity');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true);
if (!isset($params['id'])) {
return $this->json(['Success' => false, 'message' => 'شناسه دسته‌بندی ارسال نشده است'], 400);
}
$category = $entityManager->getRepository(CommodityCat::class)->findOneBy([
'bid' => $acc['bid'],
'id' => $params['id']
]);
if (!$category) {
return $this->json(['Success' => false, 'message' => 'دسته‌بندی یافت نشد'], 404);
}
// بررسی دسته‌بندی پیش‌فرض
if ($this->isDefaultCategoryName($category->getName())) {
return $this->json([
'Success' => false,
'message' => 'حذف دسته‌بندی پیش‌فرض مجاز نیست',
'errorCode' => 'DEFAULT_CATEGORY_DELETE'
], 400);
}
// بررسی دسته‌بندی ریشه
if ($category->isRoot()) {
return $this->json(['Success' => false, 'message' => 'دسته‌بندی ریشه قابل حذف نیست'], 400);
}
// بررسی وجود زیرمجموعه
$hasChildren = $this->hasChild($entityManager, $category);
if ($hasChildren) {
return $this->json(['Success' => false, 'message' => 'این دسته‌بندی دارای زیرمجموعه است و قابل حذف نیست'], 400);
}
// بررسی وجود کالا در این دسته‌بندی
$hasCommodities = $entityManager->getRepository(Commodity::class)->findOneBy(['cat' => $category]);
if ($hasCommodities) {
return $this->json(['Success' => false, 'message' => 'این دسته‌بندی دارای کالا است و قابل حذف نیست'], 400);
}
$catName = $category->getName();
$entityManager->remove($category);
$entityManager->flush();
$log->insert('کالا/خدمات', 'دسته‌بندی با نام ' . $catName . ' حذف شد.', $this->getUser(), $acc['bid']->getId());
return $this->json(['Success' => true, 'message' => 'دسته‌بندی با موفقیت حذف شد']);
}
}

View file

@ -20,7 +20,7 @@ use App\Entity\Salary;
use App\Entity\Person;
use App\Service\Log;
use Doctrine\Common\Collections\ArrayCollection;
use App\Repository\HesabdariTableRepository;
class CostController extends AbstractController
{
#[Route('/api/cost/dashboard/data', name: 'app_cost_dashboard_data', methods: ['GET'])]
@ -180,6 +180,7 @@ class CostController extends AbstractController
Request $request,
Access $access,
EntityManagerInterface $entityManager,
HesabdariTableRepository $hesabdariTableRepository,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('cost');
@ -189,22 +190,24 @@ class CostController extends AbstractController
$params = json_decode($request->getContent(), true) ?? [];
// پارامترهای ورودی
// Input parameters
$filters = $params['filters'] ?? [];
$pagination = $params['pagination'] ?? ['page' => 1, 'limit' => 10];
$sort = $params['sort'] ?? ['sortBy' => 'id', 'sortDesc' => true];
$type = $params['type'] ?? 'cost';
// تنظیم پارامترهای صفحه‌بندی
// Set pagination parameters
$page = max(1, $pagination['page'] ?? 1);
$limit = max(1, min(100, $pagination['limit'] ?? 10));
// ساخت کوئری پایه
// Build base query
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter')
->from('App\Entity\HesabdariDoc', 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.hesabdariRows', 'r')
->leftJoin('r.ref', 't')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
@ -214,14 +217,12 @@ class CostController extends AbstractController
->setParameter('type', $type)
->setParameter('money', $acc['money']);
// اعمال فیلترها
// Apply filters
if (!empty($filters)) {
// جستجوی متنی
// Text search
if (isset($filters['search'])) {
$searchValue = is_array($filters['search']) ? $filters['search']['value'] : $filters['search'];
$queryBuilder->leftJoin('d.hesabdariRows', 'r')
->leftJoin('r.person', 'p')
->leftJoin('r.ref', 't')
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
@ -229,13 +230,25 @@ class CostController extends AbstractController
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
't.name LIKE :search'
't.name LIKE :search',
't.code LIKE :search'
)
)
->setParameter('search', "%{$searchValue}%");
}
// فیلتر زمانی
// Cost center filter
if (isset($filters['account']) && $filters['account'] !== '67') {
$accountCodes = $hesabdariTableRepository->findAllSubAccountCodes($filters['account'], $acc['bid']->getId());
if (!empty($accountCodes)) {
$queryBuilder->andWhere('t.code IN (:accountCodes)')
->setParameter('accountCodes', $accountCodes);
} else {
$queryBuilder->andWhere('1 = 0');
}
}
// Time filter
if (isset($filters['timeFilter'])) {
$today = $jdate->jdate('Y/m/d', time());
switch ($filters['timeFilter']) {
@ -262,31 +275,28 @@ class CostController extends AbstractController
->setParameter('dateTo', $filters['dateTo']);
}
break;
case 'all':
default:
// بدون فیلتر زمانی اضافه
break;
}
}
// Amount filter
if (isset($filters['amount'])) {
$queryBuilder->andWhere('d.amount = :amount')
->setParameter('amount', $filters['amount']);
}
}
// اعمال مرتب‌سازی
// Apply sorting
$sortField = is_array($sort['sortBy']) ? ($sort['sortBy']['key'] ?? 'id') : ($sort['sortBy'] ?? 'id');
$sortDirection = ($sort['sortDesc'] ?? true) ? 'DESC' : 'ASC';
$queryBuilder->orderBy("d.$sortField", $sortDirection);
// محاسبه تعداد کل نتایج
// Calculate total items
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
// اعمال صفحه‌بندی
// Apply pagination
$queryBuilder->setFirstResult(($page - 1) * $limit)
->setMaxResults($limit);
@ -305,9 +315,9 @@ class CostController extends AbstractController
'submitter' => $doc['submitter'],
];
// دریافت اطلاعات مرکز هزینه و مبلغ
// Get cost center details
$costDetails = $entityManager->createQueryBuilder()
->select('t.name as center_name, r.bd as amount, r.des as des')
->select('t.name as center_name, t.code as center_code, r.bd as amount, r.des as des')
->from('App\Entity\HesabdariRow', 'r')
->join('r.ref', 't')
->where('r.doc = :docId')
@ -319,12 +329,13 @@ class CostController extends AbstractController
$item['costCenters'] = array_map(function ($detail) {
return [
'name' => $detail['center_name'],
'code' => $detail['center_code'],
'amount' => (int) $detail['amount'],
'des' => $detail['des'],
];
}, $costDetails);
// دریافت اطلاعات شخص مرتبط
// Get related person info
$personInfo = $entityManager->createQueryBuilder()
->select('p.id, p.nikename, p.code')
->from('App\Entity\HesabdariRow', 'r')

View file

@ -1143,32 +1143,36 @@ class HesabdariController extends AbstractController
throw $this->createAccessDeniedException();
}
$depth = (int) $request->query->get('depth', 2); // عمق پیش‌فرض 2
$rootId = (int) $request->query->get('rootId', 1); // گره ریشه پیش‌فرض
$rootCode = $request->query->get('rootCode');
if (!$rootCode) {
return $this->json(['Success' => false, 'message' => 'کد ریشه مشخص نشده است'], 400);
}
// جستجوی ریشه بر اساس code
$root = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => $rootCode,
'bid' => [$acc['bid']->getId(), null]
]);
$root = $entityManager->getRepository(HesabdariTable::class)->find($rootId);
if (!$root) {
return $this->json(['Success' => false, 'message' => 'نود ریشه یافت نشد'], 404);
}
$buildTree = function ($node, $depth, $currentDepth = 0) use ($entityManager, $acc, &$buildTree) {
if ($currentDepth >= $depth) {
return null;
}
// تابع بازگشتی برای ساخت درخت
$buildTree = function ($node) use ($entityManager, $acc, &$buildTree) {
$children = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $node,
'bid' => [$acc['bid']->getId(), null],
]);
], ['code' => 'ASC']); // مرتب‌سازی بر اساس کد
$result = [];
foreach ($children as $child) {
$childData = [
'id' => $child->getId(),
'name' => $child->getName(),
'code' => $child->getCode(),
'name' => $child->getName(),
'type' => $child->getType(),
'children' => $buildTree($child, $depth, $currentDepth + 1),
'children' => $buildTree($child)
];
$result[] = $childData;
}
@ -1176,12 +1180,12 @@ class HesabdariController extends AbstractController
return $result;
};
// ساخت درخت کامل
$tree = [
'id' => $root->getId(),
'name' => $root->getName(),
'code' => $root->getCode(),
'name' => $root->getName(),
'type' => $root->getType(),
'children' => $buildTree($root, $depth),
'children' => $buildTree($root)
];
return $this->json(['Success' => true, 'data' => $tree]);

View file

@ -317,6 +317,9 @@ class PersonsController extends AbstractController
$person->setPrelabel($prelabel);
}
}
elseif ($params['prelabel'] == null) {
$person->setPrelabel(null);
}
}
//inset cards
if (array_key_exists('accounts', $params)) {
@ -995,11 +998,6 @@ class PersonsController extends AbstractController
$params = json_decode($content, true);
}
// پارامترهای صفحه‌بندی
$page = $params['page'] ?? 1;
$limit = $params['limit'] ?? 10;
$offset = ($page - 1) * $limit;
$queryBuilder = $entityManager->getRepository(HesabdariDoc::class)->createQueryBuilder('d')
->where('d.bid = :bid')
->andWhere('d.type = :type')
@ -1022,12 +1020,23 @@ class PersonsController extends AbstractController
->getQuery()
->getSingleScalarResult();
// دریافت داده‌های صفحه فعلی
$items = $queryBuilder->select('d')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
// اگر درخواست با صفحه‌بندی است
if (array_key_exists('page', $params) && array_key_exists('limit', $params)) {
$page = $params['page'];
$limit = $params['limit'];
$offset = ($page - 1) * $limit;
$items = $queryBuilder->select('d')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
} else {
// دریافت همه آیتم‌ها بدون صفحه‌بندی
$items = $queryBuilder->select('d')
->getQuery()
->getResult();
}
// اضافه کردن اطلاعات اشخاص به هر آیتم
foreach ($items as $item) {
@ -1048,16 +1057,16 @@ class PersonsController extends AbstractController
'bid' => $acc['bid'],
'items' => $items,
'totalItems' => $totalItems,
'currentPage' => $page,
'totalPages' => ceil($totalItems / $limit)
'currentPage' => $params['page'] ?? 1,
'totalPages' => array_key_exists('limit', $params) ? ceil($totalItems / $params['limit']) : 1
])
);
return $this->json([
'id' => $pid,
'totalItems' => $totalItems,
'currentPage' => $page,
'totalPages' => ceil($totalItems / $limit)
'currentPage' => $params['page'] ?? 1,
'totalPages' => array_key_exists('limit', $params) ? ceil($totalItems / $params['limit']) : 1
]);
}

View file

@ -88,7 +88,7 @@ class PreinvoiceController extends AbstractController
#[Route('/api/preinvoice/save', name: 'app_preinvoice_save')]
public function savePreinvoice(Access $access,Provider $provider, Log $log, EntityManagerInterface $entityManager, Request $request): JsonResponse
{
$acc = $access->hasRole('preinvoice');
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
@ -170,7 +170,7 @@ class PreinvoiceController extends AbstractController
#[Route('/api/preinvoice/delete/{id}', name: 'app_preinvoice_delete')]
public function deletePreinvoice(Access $access, Log $log, EntityManagerInterface $entityManager, int $id): JsonResponse
{
$acc = $access->hasRole('preinvoice');
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
@ -207,7 +207,7 @@ class PreinvoiceController extends AbstractController
#[Route('/api/preinvoice/docs/search', name: 'app_presell_search')]
public function searchPreinvoices(Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('preinvoice');
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
@ -244,7 +244,7 @@ class PreinvoiceController extends AbstractController
#[Route('/api/preinvoice/remove/group', name: 'app_presell_delete_group')]
public function deletePreinvoiceGroup(Log $log, Access $access, EntityManagerInterface $entityManager, Request $request): JsonResponse
{
$acc = $access->hasRole('preinvoice');
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
return new JsonResponse($this->extractor->operationFail('دسترسی ندارید'), 403);
}
@ -289,7 +289,7 @@ class PreinvoiceController extends AbstractController
$params = json_decode($content, true);
}
$acc = $access->hasRole('preinvoice');
$acc = $access->hasRole('plugAccproPresell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
@ -312,7 +312,9 @@ class PreinvoiceController extends AbstractController
'taxInfo' => true,
'discountInfo' => true,
'note' => true,
'paper' => 'A4-L'
'paper' => 'A4-L',
'invoiceIndex' => false,
'businessStamp' => false
];
if (array_key_exists('printOptions', $params)) {
@ -334,6 +336,12 @@ class PreinvoiceController extends AbstractController
if (array_key_exists('paper', $params['printOptions'])) {
$printOptions['paper'] = $params['printOptions']['paper'];
}
if (array_key_exists('invoiceIndex', $params['printOptions'])) {
$printOptions['invoiceIndex'] = $params['printOptions']['invoiceIndex'];
}
if (array_key_exists('businessStamp', $params['printOptions'])) {
$printOptions['businessStamp'] = $params['printOptions']['businessStamp'];
}
}
$note = '';

View file

@ -57,6 +57,8 @@ class PrintersController extends AbstractController
$temp['sell']['noteString'] = $settings->getSellNoteString();
$temp['sell']['pays'] = $settings->isSellPays();
$temp['sell']['paper'] = $settings->getSellPaper();
$temp['sell']['businessStamp'] = $settings->isSellBusinessStamp();
$temp['sell']['invoiceIndex'] = $settings->isSellInvoiceIndex();
if (!$temp['sell']['paper']) {
$temp['sell']['paper'] = 'A4-L';
}
@ -135,6 +137,8 @@ class PrintersController extends AbstractController
$settings->setSellNoteString($params['sell']['noteString']);
$settings->setSellPays($params['sell']['pays']);
$settings->setSellPaper($params['sell']['paper']);
$settings->setSellBusinessStamp($params['sell']['businessStamp']);
$settings->setSellInvoiceIndex($params['sell']['invoiceIndex']);
if ($params['buy']['bidInfo'] == null) {
$settings->setBuyBidInfo(false);
} else {

View file

@ -368,262 +368,262 @@ class SellController extends AbstractController
}
#[Route('/api/sell/docs/search', name: 'app_sell_docs_search', methods: ['POST'])]
public function searchSellDocs(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$page = max(1, $params['page'] ?? 1);
$perPage = max(1, min(100, $params['perPage'] ?? 10));
$types = $params['types'] ?? [];
$dateFilter = $params['dateFilter'] ?? 'all';
$sortBy = $params['sortBy'] ?? [];
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'sell')
->setParameter('money', $acc['money']);
// اعمال فیلترهای تاریخ
$today = $jdate->jdate('Y/m/d', time());
if ($dateFilter === 'today') {
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
} elseif ($dateFilter === 'week') {
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
} elseif ($dateFilter === 'month') {
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
}
if ($searchTerm) {
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// فیلدهای معتبر برای مرتب‌سازی توی دیتابیس
$validDbFields = [
'id' => 'd.id',
'dateSubmit' => 'd.dateSubmit',
'date' => 'd.date',
'type' => 'd.type',
'code' => 'd.code',
'des' => 'd.des',
'amount' => 'd.amount',
'mdate' => 'd.mdate',
'plugin' => 'd.plugin',
'refData' => 'd.refData',
'shortlink' => 'd.shortlink',
'status' => 'd.status',
'submitter' => 'u.fullName',
'label' => 'l.label', // از InvoiceLabel
];
// اعمال مرتب‌سازی توی دیتابیس
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if ($key === 'profit' || $key === 'receivedAmount') {
continue; // این‌ها توی PHP مرتب می‌شن
} elseif (isset($validDbFields[$key])) {
$queryBuilder->addOrderBy($validDbFields[$key], $direction);
}
// اگه کلید معتبر نبود، نادیده گرفته می‌شه
public function searchSellDocs(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('sell');
if (!$acc) {
throw $this->createAccessDeniedException();
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
$params = json_decode($request->getContent(), true) ?? [];
$searchTerm = $params['search'] ?? '';
$page = max(1, $params['page'] ?? 1);
$perPage = max(1, min(100, $params['perPage'] ?? 10));
$types = $params['types'] ?? [];
$dateFilter = $params['dateFilter'] ?? 'all';
$sortBy = $params['sortBy'] ?? [];
$queryBuilder->setFirstResult(($page - 1) * $perPage)
->setMaxResults($perPage);
$queryBuilder = $entityManager->createQueryBuilder()
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
->addSelect('u.fullName as submitter')
->addSelect('l.code as labelCode, l.label as labelLabel')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.submitter', 'u')
->leftJoin('d.InvoiceLabel', 'l')
->leftJoin('d.hesabdariRows', 'r')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.type = :type')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('type', 'sell')
->setParameter('money', $acc['money']);
$docs = $queryBuilder->getQuery()->getArrayResult();
// اعمال فیلترهای تاریخ
$today = $jdate->jdate('Y/m/d', time());
if ($dateFilter === 'today') {
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
} elseif ($dateFilter === 'week') {
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
} elseif ($dateFilter === 'month') {
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
}
$dataTemp = [];
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
if ($searchTerm) {
$queryBuilder->leftJoin('r.person', 'p')
->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'd.date LIKE :search',
'd.amount LIKE :search',
'p.nikename LIKE :search',
'p.mobile LIKE :search'
)
)
->setParameter('search', "%$searchTerm%");
}
if (!empty($types)) {
$queryBuilder->andWhere('l.code IN (:types)')
->setParameter('types', $types);
}
// فیلدهای معتبر برای مرتب‌سازی توی دیتابیس
$validDbFields = [
'id' => 'd.id',
'dateSubmit' => 'd.dateSubmit',
'date' => 'd.date',
'type' => 'd.type',
'code' => 'd.code',
'des' => 'd.des',
'amount' => 'd.amount',
'mdate' => 'd.mdate',
'plugin' => 'd.plugin',
'refData' => 'd.refData',
'shortlink' => 'd.shortlink',
'status' => 'd.status',
'submitter' => 'u.fullName',
'label' => 'l.label', // از InvoiceLabel
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? [
'id' => $mainRow->getPerson()->getId(),
'nikename' => $mainRow->getPerson()->getNikename(),
'code' => $mainRow->getPerson()->getCode()
] : null;
// اعمال مرتب‌سازی توی دیتابیس
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC';
if ($key === 'profit' || $key === 'receivedAmount') {
continue; // این‌ها توی PHP مرتب می‌شن
} elseif (isset($validDbFields[$key])) {
$queryBuilder->addOrderBy($validDbFields[$key], $direction);
}
// اگه کلید معتبر نبود، نادیده گرفته می‌شه
}
} else {
$queryBuilder->orderBy('d.id', 'DESC');
}
// استفاده از SQL خام برای محاسبه پرداختی‌ها
$sql = "
$totalItemsQuery = clone $queryBuilder;
$totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)')
->getQuery()
->getSingleScalarResult();
$queryBuilder->setFirstResult(($page - 1) * $perPage)
->setMaxResults($perPage);
$docs = $queryBuilder->getQuery()->getArrayResult();
$dataTemp = [];
foreach ($docs as $doc) {
$item = [
'id' => $doc['id'],
'dateSubmit' => $doc['dateSubmit'],
'date' => $doc['date'],
'type' => $doc['type'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'submitter' => $doc['submitter'],
'label' => $doc['labelCode'] ? [
'code' => $doc['labelCode'],
'label' => $doc['labelLabel']
] : null,
];
$mainRow = $entityManager->getRepository(HesabdariRow::class)
->createQueryBuilder('r')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $mainRow && $mainRow->getPerson() ? [
'id' => $mainRow->getPerson()->getId(),
'nikename' => $mainRow->getPerson()->getNikename(),
'code' => $mainRow->getPerson()->getCode()
] : null;
// استفاده از SQL خام برای محاسبه پرداختی‌ها
$sql = "
SELECT SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs
FROM hesabdari_doc rd
JOIN hesabdari_doc_hesabdari_doc rel ON rel.hesabdari_doc_target = rd.id
WHERE rel.hesabdari_doc_source = :sourceDocId
AND rd.bid_id = :bidId
";
$stmt = $entityManager->getConnection()->prepare($sql);
$stmt->bindValue('sourceDocId', $doc['id']);
$stmt->bindValue('bidId', $acc['bid']->getId());
$result = $stmt->executeQuery()->fetchAssociative();
$stmt = $entityManager->getConnection()->prepare($sql);
$stmt->bindValue('sourceDocId', $doc['id']);
$stmt->bindValue('bidId', $acc['bid']->getId());
$result = $stmt->executeQuery()->fetchAssociative();
$relatedDocsPays = $result['total_pays'] ?? 0;
$relatedDocsCount = $result['count_docs'] ?? 0;
$relatedDocsPays = $result['total_pays'] ?? 0;
$relatedDocsCount = $result['count_docs'] ?? 0;
$item['relatedDocsCount'] = (int) $relatedDocsCount;
$item['relatedDocsPays'] = $relatedDocsPays;
$item['profit'] = $this->calculateProfit($doc['id'], $acc, $entityManager);
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$item['relatedDocsCount'] = (int) $relatedDocsCount;
$item['relatedDocsPays'] = $relatedDocsPays;
$item['profit'] = $this->calculateProfit($doc['id'], $acc, $entityManager);
$item['discountAll'] = 0;
$item['transferCost'] = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '104') {
$item['discountAll'] = $row->getBd();
} elseif ($row->getRef()->getCode() == '61') {
$item['transferCost'] = $row->getBs();
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]);
foreach ($rows as $row) {
if ($row->getRef()->getCode() == '104') {
$item['discountAll'] = $row->getBd();
} elseif ($row->getRef()->getCode() == '61') {
$item['transferCost'] = $row->getBs();
}
}
$dataTemp[] = $item;
}
// مرتب‌سازی توی PHP برای profit و receivedAmount
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? SORT_DESC : SORT_ASC;
if ($key === 'profit') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['profit'] - $b['profit'] : $b['profit'] - $a['profit'];
});
} elseif ($key === 'receivedAmount') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['relatedDocsPays'] - $b['relatedDocsPays'] : $b['relatedDocsPays'] - $a['relatedDocsPays'];
});
}
}
}
$dataTemp[] = $item;
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'perPage' => $perPage,
]);
}
// مرتب‌سازی توی PHP برای profit و receivedAmount
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$key = $sort['key'] ?? 'id';
$direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? SORT_DESC : SORT_ASC;
if ($key === 'profit') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['profit'] - $b['profit'] : $b['profit'] - $a['profit'];
});
} elseif ($key === 'receivedAmount') {
usort($dataTemp, function ($a, $b) use ($direction) {
return $direction === SORT_ASC ? $a['relatedDocsPays'] - $b['relatedDocsPays'] : $b['relatedDocsPays'] - $a['relatedDocsPays'];
});
}
}
}
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'perPage' => $perPage,
]);
}
// متد calculateProfit بدون تغییر
private function calculateProfit(int $docId, array $acc, EntityManagerInterface $entityManager): int
{
$profit = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $docId]);
foreach ($rows as $item) {
if ($item->getCommdityCount() && $item->getBs()) {
$commodityId = $item->getCommodity() ? $item->getCommodity()->getId() : null;
if ($acc['bid']->getProfitCalctype() === 'lis') {
if ($commodityId) {
$last = $entityManager->getRepository(HesabdariRow::class)
->findOneBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
if ($last) {
$price = $last->getBd() / $last->getCommdityCount();
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
// متد calculateProfit بدون تغییر
private function calculateProfit(int $docId, array $acc, EntityManagerInterface $entityManager): int
{
$profit = 0;
$rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $docId]);
foreach ($rows as $item) {
if ($item->getCommdityCount() && $item->getBs()) {
$commodityId = $item->getCommodity() ? $item->getCommodity()->getId() : null;
if ($acc['bid']->getProfitCalctype() === 'lis') {
if ($commodityId) {
$last = $entityManager->getRepository(HesabdariRow::class)
->findOneBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
if ($last) {
$price = $last->getBd() / $last->getCommdityCount();
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
} else {
$profit += $item->getBs();
}
} else {
$profit += $item->getBs();
}
} elseif ($acc['bid']->getProfitCalctype() === 'simple') {
if ($item->getCommodity() && $item->getCommodity()->getPriceSell() !== null && $item->getCommodity()->getPriceBuy() !== null) {
$profit += (($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceBuy()) * $item->getCommdityCount());
} else {
$profit += $item->getBs();
}
} else {
$profit += $item->getBs();
}
} elseif ($acc['bid']->getProfitCalctype() === 'simple') {
if ($item->getCommodity() && $item->getCommodity()->getPriceSell() !== null && $item->getCommodity()->getPriceBuy() !== null) {
$profit += (($item->getCommodity()->getPriceSell() - $item->getCommodity()->getPriceBuy()) * $item->getCommdityCount());
} else {
$profit += $item->getBs();
}
} else {
if ($commodityId) {
$lasts = $entityManager->getRepository(HesabdariRow::class)
->findBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
$avg = array_sum(array_map(fn($last) => $last->getBd(), $lasts));
$count = array_sum(array_map(fn($last) => $last->getCommdityCount(), $lasts));
if ($count != 0) {
$price = $avg / $count;
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
if ($commodityId) {
$lasts = $entityManager->getRepository(HesabdariRow::class)
->findBy(['commodity' => $commodityId, 'bs' => 0], ['id' => 'DESC']);
$avg = array_sum(array_map(fn($last) => $last->getBd(), $lasts));
$count = array_sum(array_map(fn($last) => $last->getCommdityCount(), $lasts));
if ($count != 0) {
$price = $avg / $count;
$profit += ((($item->getBs() / $item->getCommdityCount()) - $price) * $item->getCommdityCount());
} else {
$profit += $item->getBs();
}
} else {
$profit += $item->getBs();
}
} else {
$profit += $item->getBs();
}
}
}
return round($profit);
}
return round($profit);
}
#[Route('/api/sell/rows/{code}', name: 'app_sell_rows', methods: ['GET'])]
public function getSellRows(
@ -678,15 +678,25 @@ private function calculateProfit(int $docId, array $acc, EntityManagerInterface
#[Route('/api/sell/print/invoice', name: 'app_sell_print_invoice')]
public function app_sell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$acc = $access->hasRole('sell');
if (!$acc)
throw $this->createAccessDeniedException();
$params = json_decode($request->getContent(), true);
$printOptions = $params['printOptions'] ?? [];
// اضافه کردن کلیدهای پیش‌فرض
$printOptions = array_merge([
'note' => true,
'bidInfo' => true,
'taxInfo' => true,
'discountInfo' => true,
'pays' => false,
'paper' => 'A4-L',
'invoiceIndex' => false,
'businessStamp' => false
], $printOptions);
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'code' => $params['code'],
@ -735,6 +745,12 @@ private function calculateProfit(int $docId, array $acc, EntityManagerInterface
if (array_key_exists('paper', $params['printOptions'])) {
$printOptions['paper'] = $params['printOptions']['paper'];
}
if (array_key_exists('invoiceIndex', $params['printOptions'])) {
$printOptions['invoiceIndex'] = $params['printOptions']['invoiceIndex'];
}
if (array_key_exists('businessStamp', $params['printOptions'])) {
$printOptions['businessStamp'] = $params['printOptions']['businessStamp'];
}
}
$note = '';
$printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]);

View file

@ -199,6 +199,7 @@ class UserController extends AbstractController
$temp = [];
$temp['name'] = $perm->getUser()->getFullName();
$temp['email'] = $perm->getUser()->getEmail();
$temp['mobile'] = $perm->getUser()->getMobile();
$temp['owner'] = $perm->isOwner();
$out[] = $temp;
}

View file

@ -0,0 +1,112 @@
<?php
namespace App\Entity;
use App\Repository\BackBuiltModuleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: BackBuiltModuleRepository::class)]
class BackBuiltModule
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'backBuiltModules')]
#[ORM\JoinColumn(nullable: false)]
private ?User $submitter = null;
#[ORM\Column(length: 40)]
private ?string $dateSubmit = null;
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $code = null;
#[ORM\Column(nullable: true)]
private ?bool $locked = null;
#[ORM\Column(nullable: true)]
private ?bool $public = null;
#[ORM\Column(length: 120)]
private ?string $type = null;
public function getId(): ?int
{
return $this->id;
}
public function getSubmitter(): ?User
{
return $this->submitter;
}
public function setSubmitter(?User $submitter): static
{
$this->submitter = $submitter;
return $this;
}
public function getDateSubmit(): ?string
{
return $this->dateSubmit;
}
public function setDateSubmit(string $dateSubmit): static
{
$this->dateSubmit = $dateSubmit;
return $this;
}
public function getCode(): ?string
{
return $this->code;
}
public function setCode(?string $code): static
{
$this->code = $code;
return $this;
}
public function isLocked(): ?bool
{
return $this->locked;
}
public function setLocked(?bool $locked): static
{
$this->locked = $locked;
return $this;
}
public function isPublic(): ?bool
{
return $this->public;
}
public function setPublic(?bool $public): static
{
$this->public = $public;
return $this;
}
public function getType(): ?string
{
return $this->type;
}
public function setType(string $type): static
{
$this->type = $type;
return $this;
}
}

View file

@ -59,8 +59,8 @@ class HesabdariRow
#[Ignore]
private ?Commodity $commodity = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 0, nullable: true)]
private ?int $commdityCount = null;
#[ORM\Column(type: Types::DECIMAL, precision: 20, scale: 4, nullable: true)]
private ?float $commdityCount = null;
#[ORM\ManyToOne(inversedBy: 'hesabdariRows')]
#[Ignore]
@ -220,15 +220,14 @@ class HesabdariRow
return $this;
}
public function getCommdityCount(): ?int
public function getCommdityCount(): ?float
{
return $this->commdityCount;
}
public function setCommdityCount(?int $commdityCount): self
public function setCommdityCount(?float $commdityCount): self
{
$this->commdityCount = $commdityCount;
return $this;
}

View file

@ -120,6 +120,9 @@ class Permission
#[ORM\Column(nullable: true)]
private ?bool $plugRepservice = null;
#[ORM\Column(nullable: true)]
private ?bool $plugAccproPresell = null;
public function getId(): ?int
{
return $this->id;
@ -545,4 +548,16 @@ class Permission
return $this;
}
public function isPlugAccproPresell(): ?bool
{
return $this->plugAccproPresell;
}
public function setPlugAccproPresell(?bool $plugAccproPresell): static
{
$this->plugAccproPresell = $plugAccproPresell;
return $this;
}
}

View file

@ -126,6 +126,12 @@ class PrintOptions
#[ORM\Column(type: Types::TEXT, nullable: true)]
private ?string $rightFooter = null;
#[ORM\Column(nullable: true)]
private ?bool $sellInvoiceIndex = null;
#[ORM\Column(nullable: true)]
private ?bool $sellBusinessStamp = null;
public function getId(): ?int
{
return $this->id;
@ -574,4 +580,26 @@ class PrintOptions
return $this;
}
public function isSellInvoiceIndex(): ?bool
{
return $this->sellInvoiceIndex;
}
public function setSellInvoiceIndex(bool $sellInvoiceIndex): self
{
$this->sellInvoiceIndex = $sellInvoiceIndex;
return $this;
}
public function isSellBusinessStamp(): ?bool
{
return $this->sellBusinessStamp;
}
public function setSellBusinessStamp(bool $sellBusinessStamp): self
{
$this->sellBusinessStamp = $sellBusinessStamp;
return $this;
}
}

View file

@ -125,6 +125,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: AccountingPackageOrder::class, orphanRemoval: true)]
private Collection $accountingPackageOrders;
/**
* @var Collection<int, BackBuiltModule>
*/
#[ORM\OneToMany(targetEntity: BackBuiltModule::class, mappedBy: 'submitter', orphanRemoval: true)]
private Collection $backBuiltModules;
public function __construct()
{
$this->userTokens = new ArrayCollection();
@ -148,6 +154,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->preInvoiceDocs = new ArrayCollection();
$this->dashboardSettings = new ArrayCollection();
$this->accountingPackageOrders = new ArrayCollection();
$this->backBuiltModules = new ArrayCollection();
}
public function getId(): ?int
@ -923,4 +930,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, BackBuiltModule>
*/
public function getBackBuiltModules(): Collection
{
return $this->backBuiltModules;
}
public function addBackBuiltModule(BackBuiltModule $backBuiltModule): static
{
if (!$this->backBuiltModules->contains($backBuiltModule)) {
$this->backBuiltModules->add($backBuiltModule);
$backBuiltModule->setSubmitter($this);
}
return $this;
}
public function removeBackBuiltModule(BackBuiltModule $backBuiltModule): static
{
if ($this->backBuiltModules->removeElement($backBuiltModule)) {
// set the owning side to null (unless already changed)
if ($backBuiltModule->getSubmitter() === $this) {
$backBuiltModule->setSubmitter(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Repository;
use App\Entity\BackBuiltModule;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<BackBuiltModule>
*/
class BackBuiltModuleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, BackBuiltModule::class);
}
// /**
// * @return BackBuiltModule[] Returns an array of BackBuiltModule objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('b.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?BackBuiltModule
// {
// return $this->createQueryBuilder('b')
// ->andWhere('b.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -3,6 +3,7 @@
namespace App\Repository;
use App\Entity\HesabdariTable;
use App\Entity\Business;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
@ -67,4 +68,58 @@ class HesabdariTableRepository extends ServiceEntityRepository
->getQuery()
->getOneOrNullResult();
}
/**
* Find all sub-account codes recursively, filtered by business ID.
*
* @param string $accountCode The account code to start with
* @param int|Business|null $business The business ID or Business entity (or null for no business filter)
* @return array List of unique account codes
*/
public function findAllSubAccountCodes(string $accountCode, $business): array
{
$businessId = $business instanceof Business ? $business->getId() : $business;
$result = [$accountCode];
// Find the parent node by account code and business filter
$qb = $this->createQueryBuilder('t')
->select('t.id')
->where('t.code = :code')
->setParameter('code', $accountCode);
// Apply business filter
if ($businessId !== null) {
$qb->andWhere('t.bid IS NULL OR t.bid = :businessId')
->setParameter('businessId', $businessId);
}
$parent = $qb->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
if (!$parent) {
return array_unique($result);
}
// Find direct children using the upper relationship and business filter
$qb = $this->createQueryBuilder('t')
->select('t.code')
->where('t.upper = :parentId')
->setParameter('parentId', $parent['id']);
// Apply business filter for children
if ($businessId !== null) {
$qb->andWhere('t.bid IS NULL OR t.bid = :businessId')
->setParameter('businessId', $businessId);
}
$subAccounts = $qb->getQuery()->getResult();
// Recursively find sub-accounts for each child
foreach ($subAccounts as $subAccount) {
$result = array_merge($result, $this->findAllSubAccountCodes($subAccount['code'], $businessId));
}
return array_unique($result);
}
}

View file

@ -26,7 +26,9 @@
<thead>
<tr>
<td style="width:20%">
<img src="{{ url('front_avatar_file_get', {id: bid.id}) }}" width="65"/>
{% if printOptions.invoiceIndex is defined and printOptions.invoiceIndex %}
<img src="{{ url('front_avatar_file_get', {id: bid.id}) }}" width="65"/>
{% endif %}
</td>
<td style="width:60%; text-align:center">
<h3 class="">پیش فاکتور فروش کالا و خدمات</h3>
@ -266,7 +268,9 @@
<td class="center" style="height:90px">
<h4>مهر و امضا فروشنده:</h4>
<br>
<img src="{{ url('front_seal_file_get', {id: bid.id}) }}" width="160"/>
{% if printOptions.businessStamp is defined and printOptions.businessStamp %}
<img src="{{ url('front_seal_file_get', {id: bid.id}) }}" width="160"/>
{% endif %}
</td>
</tr>
</tbody>

View file

@ -26,7 +26,9 @@
<thead>
<tr>
<td style="width:20%">
<img src="{{ url('front_avatar_file_get', {id: bid.id},)}}" width="65"/>
{% if printOptions.invoiceIndex %}
<img src="{{ url('front_avatar_file_get', {id: bid.id},)}}" width="65"/>
{% endif %}
</td>
<td style="width:60%; text-align:center">
<h3 class="">صورتحساب فروش کالا و خدمات</h3>
@ -214,7 +216,13 @@
{{ item.commdityCount }}
{{ item.commodity.unit.name }}
</td>
<td class="center item">{{ ((item.bs - item.tax + item.discount) / item.commdityCount) | number_format }}</td>
<td class="center item">
{% if item.commdityCount > 0 %}
{{ ((item.bs|number_format(0, '.', '') - item.tax|number_format(0, '.', '') + item.discount|number_format(0, '.', '')) / item.commdityCount) | number_format }}
{% else %}
0
{% endif %}
</td>
{% if printOptions.discountInfo %}
<td class="center item">{{ item.discount | number_format }}</td>
{% endif %}
@ -304,7 +312,9 @@
مهر و امضا فروشنده:
</h4>
<br>
<img src="{{ url('front_seal_file_get', {id: bid.id},)}}" width="160"/>
{% if printOptions.businessStamp %}
<img src="{{ url('front_seal_file_get', {id: bid.id},)}}" width="160"/>
{% endif %}
</td>
</tr>
</tbody>

View file

@ -0,0 +1,147 @@
<template>
<v-dialog v-model="dialog" width="auto">
<v-card>
<v-toolbar color="primary">
<v-toolbar-title class="text-white">
<v-icon icon="mdi-file-pdf-box" class="ml-2"></v-icon>
{{ $t('dialog.export_pdf') }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip :text="$t('dialog.print')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-printer" color="white" @click="handlePrint"></v-btn>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.cancel')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-close" color="white" @click="handleCancel"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-card-text>
<v-select class="mb-2" v-model="printOptions.paper" :items="paperSizes" :label="$t('dialog.paper_size')">
</v-select>
<v-row>
<v-col cols="12" sm="6">
<v-tooltip :text="$t('dialog.bid_info_label')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.bidInfo" color="primary" :label="$t('dialog.bid_info_label')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_pays')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.pays" color="primary" :label="$t('dialog.invoice_pays')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_footer_note')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.note" color="primary" :label="$t('dialog.invoice_footer_note')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
</v-col>
<v-col cols="12" sm="6">
<v-tooltip :text="$t('dialog.tax_dexpo')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.taxInfo" color="primary" :label="$t('dialog.tax_dexpo')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.discount_dexpo')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.discountInfo" color="primary" :label="$t('dialog.discount_dexpo')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.business_stamp')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-if="isPluginActive('accpro')" v-bind="props" inset v-model="printOptions.businessStamp" color="primary" :label="$t('dialog.business_stamp')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_index')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-if="isPluginActive('accpro')" v-bind="props" inset v-model="printOptions.invoiceIndex" color="primary" :label="$t('dialog.invoice_index')" hide-details density="compact"></v-switch>
</template>
</v-tooltip>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script>
import { ref, defineComponent } from 'vue';
export default defineComponent({
name: 'PrintDialog',
props: {
modelValue: {
type: Boolean,
default: false
},
plugins: {
type: Object,
default: () => ({})
}
},
emits: ['update:modelValue', 'print', 'cancel'],
setup(props, { emit }) {
const dialog = ref(props.modelValue);
const printOptions = ref({
note: true,
bidInfo: true,
taxInfo: true,
discountInfo: true,
pays: false,
paper: 'A4-L',
businessStamp: false,
invoiceIndex: false
});
const paperSizes = [
{ title: 'A4 عمودی', value: 'A4' },
{ title: 'A4 افقی', value: 'A4-L' },
{ title: 'A5 عمودی', value: 'A5' },
{ title: 'A5 افقی', value: 'A5-L' }
];
const isPluginActive = (pluginCode) => {
return props.plugins && props.plugins[pluginCode];
};
const handlePrint = () => {
emit('print', printOptions.value);
dialog.value = false;
};
const handleCancel = () => {
emit('cancel');
dialog.value = false;
};
return {
dialog,
printOptions,
paperSizes,
isPluginActive,
handlePrint,
handleCancel
};
},
watch: {
modelValue(newVal) {
this.dialog = newVal;
},
dialog(newVal) {
this.$emit('update:modelValue', newVal);
}
}
});
</script>
<style scoped>
.v-card {
border-radius: 8px;
}
.v-dialog {
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
</style>

View file

@ -0,0 +1,377 @@
<template>
<v-menu
v-model="menu"
:close-on-content-click="false"
location="bottom"
width="400"
>
<template v-slot:activator="{ props }">
<v-text-field
v-bind="props"
:model-value="selectedAccountName"
:label="label"
:rules="rules"
readonly
hide-details
density="compact"
@click="openMenu"
>
<template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template>
</v-text-field>
</template>
<v-card>
<v-progress-linear v-if="isLoading" indeterminate color="primary" />
<v-card-text v-if="accountData.length > 0" class="pa-0">
<div class="tree-container">
<tree-node
v-for="node in accountData"
:key="node.code"
:node="node"
:selected-code="selectedAccount?.code"
:selectable-only="selectableOnly"
@select="handleNodeSelect"
@toggle="toggleNode"
/>
</div>
</v-card-text>
<v-card-text v-else>
{{ isLoading ? 'در حال بارگذاری...' : 'هیچ حسابی یافت نشد' }}
</v-card-text>
</v-card>
</v-menu>
<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>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import axios from 'axios';
import TreeNode from '@/components/forms/TreeNode.vue';
// اضافه کردن کش سراسری
const globalCache = {
data: null as AccountNode[] | null,
isLoading: false,
promise: null as Promise<void> | null,
lastUpdate: 0,
};
type ValidationRule = (value: any) => boolean | string;
interface AccountNode {
code: string;
name: string;
children?: AccountNode[];
isOpen?: boolean;
tableType?: string;
type?: string;
}
const props = defineProps({
modelValue: {
type: String,
default: '',
},
label: {
type: String,
default: 'حساب',
},
rules: {
type: Array as () => ValidationRule[],
default: () => [],
},
initialAccount: {
type: Object,
default: () => ({ code: '', name: '' }),
},
showSubTree: {
type: Boolean,
default: false,
},
selectableOnly: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue', 'select', 'tableType', 'accountSelected']);
const menu = ref(false);
const accountData = ref<AccountNode[]>([]);
const selectedAccount = ref<AccountNode | null>(null);
const cache = ref(new Map<string, AccountNode[]>());
const isLoading = ref(false);
const snackbar = ref({
show: false,
text: '',
color: 'success',
});
const selectedAccountName = computed(() => {
return selectedAccount.value?.name || '';
});
// نمایش پیام خطا یا موفقیت
const showMessage = (text: string, color = 'success') => {
snackbar.value.text = text;
snackbar.value.color = color;
snackbar.value.show = true;
};
// رمزگشایی کاراکترهای یونیکد
const decodeUnicode = (str: string): string => {
try {
return decodeURIComponent(
str.replace(/\\u([\dA-F]{4})/gi, (match, grp) =>
String.fromCharCode(parseInt(grp, 16))
)
);
} catch (e) {
console.error('خطا در رمزگشایی یونیکد:', e);
return str;
}
};
// پردازش دادهها
const processTreeData = (items: any[]): any[] => {
return items.map((item) => {
if (cache.value.has(`processed-${item.code}`)) {
return cache.value.get(`processed-${item.code}`);
}
const processedItem = {
...item,
name: decodeUnicode(item.name),
children: item.children ? processTreeData(item.children) : [],
isOpen: false,
tableType: item.type,
};
cache.value.set(`processed-${item.code}`, processedItem);
return processedItem;
});
};
// بارگذاری دادهها
const fetchHesabdariTables = async () => {
const now = Date.now();
const CACHE_DURATION = 5 * 60 * 1000; // 5 دقیقه
// اگر کش معتبر است و در حالت showSubTree نیستیم، از کش استفاده میکنیم
if (globalCache.data && !props.showSubTree && (now - globalCache.lastUpdate) < CACHE_DURATION) {
accountData.value = globalCache.data;
return;
}
// اگر در حال بارگذاری هستیم، صبر میکنیم
if (globalCache.isLoading && globalCache.promise) {
await globalCache.promise;
if (!props.showSubTree) {
accountData.value = globalCache.data || [];
}
return;
}
globalCache.isLoading = true;
isLoading.value = true;
try {
globalCache.promise = new Promise(async (resolve) => {
try {
let response;
// اگر initialAccount وجود داشت، از آن برای فیلتر کردن استفاده میکنیم
if (props.initialAccount?.code) {
response = await axios.get(`/api/hesabdari/tables/tree?rootCode=${props.initialAccount.code}`);
} else if (props.showSubTree && selectedAccount.value) {
response = await axios.get(`/api/hesabdari/tables/tree?rootCode=${selectedAccount.value.code}`);
} else {
response = await axios.get('/api/hesabdari/tables/all');
}
if (response.data.Success && response.data.data) {
let data;
if (props.initialAccount?.code) {
// در حالت initialAccount فقط زیرمجموعهها را نمایش میدهیم
data = response.data.data.children || [];
} else if (props.showSubTree) {
data = [response.data.data];
} else {
data = response.data.data.children || [];
}
const processedData = processTreeData(data);
if (!props.showSubTree && !props.initialAccount?.code) {
globalCache.data = processedData;
globalCache.lastUpdate = now;
}
accountData.value = processedData;
} else {
showMessage('هیچ حسابی یافت نشد', 'warning');
}
} catch (error) {
console.error('خطا در بارگذاری حساب‌ها:', error);
showMessage('خطا در بارگذاری حساب‌ها', 'error');
} finally {
globalCache.isLoading = false;
isLoading.value = false;
resolve();
}
});
await globalCache.promise;
} catch (error) {
console.error('خطا در بارگذاری حساب‌ها:', error);
showMessage('خطا در بارگذاری حساب‌ها', 'error');
globalCache.isLoading = false;
isLoading.value = false;
}
};
// جستجوی حساب در دادههای موجود
const findAccount = (nodes: any[]): any => {
for (const node of nodes) {
if (node.code === props.modelValue) {
return node;
}
if (node.children && node.children.length) {
const found = findAccount(node.children);
if (found) return found;
}
}
return null;
};
// تنظیم مقدار اولیه حساب
const initializeAccount = async () => {
if (props.modelValue) {
if (props.initialAccount && !selectedAccount.value) {
selectedAccount.value = {
code: props.modelValue,
name: props.initialAccount.name || '',
tableType: props.initialAccount.tableType || '',
};
emit('select', selectedAccount.value);
emit('accountSelected', selectedAccount.value);
if (selectedAccount.value.tableType) {
emit('tableType', selectedAccount.value.tableType);
}
}
// اگر initialAccount وجود داشت، دادهها را بارگذاری میکنیم
if (props.initialAccount?.code || !accountData.value.length) {
await fetchHesabdariTables();
}
const account = findAccount(accountData.value);
if (account && (!selectedAccount.value || selectedAccount.value.code !== account.code)) {
selectedAccount.value = account;
emit('select', account);
emit('accountSelected', account);
if (account.tableType && account.tableType !== selectedAccount.value?.tableType) {
emit('tableType', account.tableType);
}
}
} else {
if (selectedAccount.value) {
selectedAccount.value = null;
emit('select', null);
emit('accountSelected', null);
emit('tableType', null);
}
}
};
// تغییر وضعیت گره
const toggleNode = (node: any) => {
node.isOpen = !node.isOpen;
};
// مدیریت انتخاب گره
const handleNodeSelect = async (node: any) => {
if (props.selectableOnly && node.children && node.children.length > 0) {
showMessage('این گزینه قابل انتخاب نیست زیرا دارای زیرمجموعه است', 'error');
return;
}
selectedAccount.value = node;
emit('select', node);
emit('update:modelValue', node.code);
emit('accountSelected', node);
menu.value = false;
if (props.showSubTree) {
await fetchHesabdariTables();
}
};
// باز کردن منو
const openMenu = async () => {
menu.value = true;
// اگر initialAccount وجود داشت یا کش منقضی شده بود، دادهها را بارگذاری میکنیم
if (props.initialAccount?.code || !accountData.value.length || (Date.now() - globalCache.lastUpdate) > 5 * 60 * 1000) {
await fetchHesabdariTables();
}
};
// اصلاح watch برای modelValue
watch(
() => props.modelValue,
async (newVal) => {
if (newVal === selectedAccount.value?.code) return;
await initializeAccount();
},
{ immediate: true }
);
// اضافه کردن watch برای initialAccount
watch(
() => props.initialAccount,
async (newVal) => {
if (newVal && props.modelValue) {
await initializeAccount();
}
},
{ immediate: true }
);
// اضافه کردن watch برای showSubTree
watch(
() => props.showSubTree,
async () => {
if (menu.value) {
await fetchHesabdariTables();
}
}
);
onMounted(() => {
if (props.modelValue || props.initialAccount) {
initializeAccount();
}
});
</script>
<style scoped>
.tree-container {
max-height: 300px;
overflow-y: auto;
padding: 8px;
}
</style>

View file

@ -1,25 +1,36 @@
<template>
<div class="tree-node" :class="{ 'has-children': node.children && node.children.length > 0 }">
<div class="tree-node-content" @click="handleNodeClick">
<div class="tree-node-toggle" @click.stop="toggleNode">
<v-icon v-if="node.children && node.children.length > 0">
{{ node.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</div>
<div
class="node-content"
:class="{
'selected': node.code === selectedCode,
'has-children': node.children && node.children.length > 0,
'not-selectable': node.children && node.children.length > 0
}"
@click="handleClick"
>
<v-icon
v-if="node.children && node.children.length > 0"
@click.stop="toggleNode"
:class="{ 'rotated': node.isOpen }"
>
mdi-chevron-right
</v-icon>
<div class="tree-node-icon">
<v-icon v-if="node.children && node.children.length > 0">mdi-folder</v-icon>
<v-icon v-else>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedId === node.id }">
<div class="tree-node-label" :class="{ 'selected': node.code === selectedCode }">
{{ node.name }}
</div>
</div>
<div v-if="node.isOpen && node.children && node.children.length > 0" class="tree-children">
<tree-node
v-for="child in node.children"
:key="child.id"
:key="child.code"
:node="child"
:selected-id="selectedId"
:selected-code="selectedCode"
:selectable-only="selectableOnly"
@select="$emit('select', $event)"
@toggle="$emit('toggle', $event)"
/>
@ -35,15 +46,19 @@
type: Object,
required: true,
},
selectedId: {
type: Number,
selectedCode: {
type: String,
default: null,
},
selectableOnly: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['select', 'toggle']);
const handleNodeClick = () => {
const handleClick = () => {
emit('select', props.node);
};
@ -57,7 +72,7 @@
margin-left: 24px;
}
.tree-node-content {
.node-content {
display: flex;
align-items: center;
padding: 4px 8px;
@ -66,15 +81,18 @@
transition: background-color 0.2s;
}
.tree-node-content:hover {
.node-content:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
}
.tree-node-toggle {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
.node-content.selected {
background-color: var(--v-primary-base);
color: white;
}
.node-content.not-selectable {
opacity: 0.7;
cursor: not-allowed;
}
.tree-node-icon {
@ -101,4 +119,8 @@
border-right: 2px solid rgba(var(--v-theme-primary), 0.1);
padding-right: 8px;
}
.rotated {
transform: rotate(90deg);
}
</style>

View file

@ -408,6 +408,8 @@ const fa_lang = {
search_txt: "جست و جو ...",
prev_page: "صفحه قبل",
next_page: "صفحه بعد",
business_stamp: "مهر کسب و کار",
invoice_index: "نمایه فاکتور",
change_password: {
title: "تغییر کلمه عبور",
new_password: "کلمه عبور جدید",
@ -443,7 +445,7 @@ const fa_lang = {
import_excel: "درون ریزی از اکسل",
selected: "انتخاب شده‌ها",
selected_all: "همه‌ی موارد",
export_pdf: "خروجی پی دی اف",
export_pdf: "خروجی PDF",
export_excel: "خروجی اکسل",
filter_results: "فیلتر نتایج",
account: "حساب کاربری",

View file

@ -125,7 +125,7 @@ export default {
});
},
isPluginActive(plugName) {
return this.plugins[plugName] !== undefined;
return this.plugins && this.plugins[plugName] !== undefined;
},
getDefaultShortcuts() {
return [
@ -152,7 +152,7 @@ export default {
{ path: '/acc/costs/list', key: 'G', label: this.$t('drawer.costs'), ctrl: true, shift: true, permission: () => this.permissions.cost },
{ path: '/acc/sell/fast-mod', key: 'J', label: this.$t('drawer.fast_sell'), ctrl: true, shift: true, permission: () => this.permissions.sell },
{ path: '/acc/sell/list', key: 'V', label: this.$t('drawer.sell_invoices'), ctrl: true, shift: true, permission: () => this.permissions.sell },
{ path: '/acc/presell/list', key: 'X', label: this.$t('drawer.presells'), ctrl: true, shift: true, permission: () => this.permissions.sell && this.isPluginActive('accpro') },
{ path: '/acc/presell/list', key: 'X', label: this.$t('drawer.presells'), ctrl: true, shift: true, permission: () => this.permissions.plugAccproPresell && this.isPluginActive('accpro') },
{ path: '/acc/rfsell/list', key: 'Z', label: this.$t('drawer.rfsell_invoices'), ctrl: true, shift: true, permission: () => this.permissions.plugAccproRfsell && this.isPluginActive('accpro') },
{ path: '/acc/incomes/list', key: 'A', label: this.$t('drawer.incomes'), ctrl: true, shift: true, permission: () => this.permissions.income },
{ path: '/acc/accounting/list', key: '1', label: this.$t('drawer.accounting_docs'), ctrl: true, shift: true, permission: () => this.permissions.accounting },
@ -621,7 +621,7 @@ export default {
</v-tooltip>
</template>
</v-list-item>
<v-list-item v-if="this.isPluginActive('accpro') && permissions.sell" to="/acc/presell/list">
<v-list-item v-if="this.isPluginActive('accpro') && permissions.plugAccproPresell" to="/acc/presell/list">
<v-list-item-title>
{{ $t('drawer.presells') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/presell/list') }}</span>
@ -883,8 +883,8 @@ export default {
<v-spacer></v-spacer>
<v-tooltip text="جادوگر" location="bottom">
<template v-slot:activator="{ props }">
<v-btn class="d-none d-sm-flex" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-wizard-hat</v-icon>
<v-btn class="" stacked v-bind="props" to="/acc/wizard/home">
<v-icon>mdi-robot</v-icon>
</v-btn>
</template>
</v-tooltip>

View file

@ -1,115 +1,213 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button" class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="mx-2 fa fa-tree"></i>
دستهبندیهای کالا و خدمات </h3>
<div class="block-options">
</div>
</div>
<div class="block-content pt-1 pb-3">
<loading color="blue" loader="dots" v-model:active="isLoading" :is-full-page="false"/>
<div class="alert alert-info">
<div class="alert-heading">
<i class="fa fa-warning"></i>
هشدار</div>
<div class="">در انتخاب نوع دسته بندی دقت کنید . تا انتشار به روز رسانی بعدی حذف دسته بندی تنها از طریق تیکت پشتیبانی قابل انجام است.</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 m-0 py-2 px-1 border border-1 rounded-2">
<Tree
:nodes="tree"
:config="config"
>
<template #after-input="props">
<button class="btn btn-link" type="button" @click="addChild(props.node.id)">
<i class="fa fa-plus-circle"></i>
</button>
<button class="btn btn-link text-warning" type="button" @click="editeNode(props.node)">
<i class="fa fa-edit"></i>
</button>
<v-toolbar color="grey-lighten-4" density="compact" title="دسته‌بندی‌های کالا و خدمات">
<template v-slot:prepend>
<v-btn icon variant="text" color="warning" class="d-none d-sm-none d-md-block" @click="$router.back()">
<v-icon>mdi-arrow-right</v-icon>
</v-btn>
</template>
</v-toolbar>
<v-container>
<v-card variant="outlined" class="pa-2">
<v-skeleton-loader
v-if="isLoading"
type="table-heading"
class="mb-2"
></v-skeleton-loader>
<Tree v-else :nodes="tree" :config="config">
<template #after-input="props">
<v-tooltip location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-btn v-bind="tooltipProps" icon variant="text" size="small" @click="addChild(props.node.id)">
<v-icon size="small">mdi-plus-circle</v-icon>
</v-btn>
</template>
</Tree>
</div>
افزودن زیرمجموعه
</v-tooltip>
<v-tooltip location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-btn v-bind="tooltipProps" icon variant="text" color="warning" size="small"
@click="editeNode(props.node)">
<v-icon size="small">mdi-pencil</v-icon>
</v-btn>
</template>
ویرایش دستهبندی
</v-tooltip>
<v-tooltip v-if="!props.node.children || props.node.children.length === 0" location="top">
<template v-slot:activator="{ props: tooltipProps }">
<v-btn v-bind="tooltipProps" icon variant="text" color="error" size="small"
@click="deleteNode(props.node)">
<v-icon size="small">mdi-delete</v-icon>
</v-btn>
</template>
حذف دستهبندی
</v-tooltip>
</template>
</Tree>
</v-card>
<!-- Modal add child -->
<v-dialog v-model="showAddModal" persistent max-width="400">
<v-card class="rounded-lg">
<v-card-title class="text-h6 bg-grey-lighten-4 py-4">
<v-icon color="primary" class="me-2">mdi-plus-circle</v-icon>
افزودن دستهبندی
</v-card-title>
<v-card-text class="pt-6">
<v-text-field
v-model="addCatText"
label="نام دسته بندی"
required
variant="outlined"
density="comfortable"
hide-details="auto"
class="mb-2"
:rules="[v => !!v || 'نام دسته‌بندی الزامی است']"
:disabled="isLoading"
>
<template v-slot:label>
<span class="text-danger">(لازم)</span> نام دسته بندی
</template>
</v-text-field>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="secondary" variant="text" @click="showAddModal = false" class="me-2" min-width="100" :disabled="isLoading">
<v-icon start>mdi-close</v-icon>
انصراف
</v-btn>
<v-btn color="primary" @click="submitChild" min-width="100" :loading="isLoading">
<v-icon start>mdi-content-save</v-icon>
ثبت
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Modal edit node -->
<v-dialog v-model="showEditModal" persistent max-width="400">
<v-card class="rounded-lg">
<v-card-title class="text-h6 bg-grey-lighten-4 py-4">
<v-icon color="warning" class="me-2">mdi-pencil</v-icon>
ویرایش دستهبندی
</v-card-title>
<v-card-text class="pt-6">
<v-text-field
v-model="editCatText"
label="نام دسته بندی"
required
variant="outlined"
density="comfortable"
hide-details="auto"
class="mb-2"
:rules="[v => !!v || 'نام دسته‌بندی الزامی است']"
:disabled="isLoading"
>
<template v-slot:label>
<span class="text-danger">(لازم)</span> نام دسته بندی
</template>
</v-text-field>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="secondary" variant="text" @click="showEditModal = false" class="me-2" min-width="100" :disabled="isLoading">
<v-icon start>mdi-close</v-icon>
انصراف
</v-btn>
<v-btn color="primary" @click="submitEditChild" min-width="100" :loading="isLoading">
<v-icon start>mdi-content-save</v-icon>
ثبت
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Dialog delete confirmation -->
<v-dialog v-model="showDeleteDialog" persistent max-width="400">
<v-card class="rounded-lg">
<v-card-title class="text-h6 bg-error-lighten-5 py-4">
<v-icon color="error" class="me-2">mdi-alert-circle</v-icon>
حذف دستهبندی
</v-card-title>
<v-card-text class="pt-6">
<v-alert
type="warning"
variant="tonal"
class="mb-4"
border="start"
>
<div class="text-subtitle-2 font-weight-bold mb-2">هشدار</div>
آیا از حذف این دستهبندی اطمینان دارید؟
<div class="text-caption mt-2">این عملیات غیرقابل بازگشت است.</div>
</v-alert>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="secondary" variant="text" @click="showDeleteDialog = false" class="me-2" min-width="100" :disabled="isLoading">
<v-icon start>mdi-close</v-icon>
خیر
</v-btn>
<v-btn color="error" @click="confirmDelete" min-width="100" :loading="isLoading">
<v-icon start>mdi-delete</v-icon>
بله، حذف شود
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- Snackbar for notifications -->
<v-snackbar
v-model="showSnackbar"
:color="snackbarColor"
:timeout="3000"
location="bottom"
class="rounded-lg"
elevation="2"
>
<div class="d-flex align-center">
<v-icon :color="snackbarColor" class="me-2">
{{ snackbarColor === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
</v-icon>
{{ snackbarText }}
</div>
</div>
</div>
<!-- Modal add child -->
<div class="modal fade" id="ModalAdd" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">افزودن دستهبندی</h1>
<div class="block-options">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="form-floating mb-4">
<input v-model="addCatText" class="form-control" type="text">
<label class="form-label"><span class="text-danger">(لازم)</span> نام دسته بندی</label>
</div>
</div>
<div class="modal-footer">
<button @click="this.submitChild" type="button" class="btn btn-primary">
<i class="fa fa-save"></i>
ثبت</button>
<button id="modalAddClose" type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fa fa-close"></i>
انصراف</button>
</div>
</div>
</div>
</div>
<!-- Modal edit node -->
<div class="modal fade" id="ModalEdit" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1" aria-labelledby="staticBackdropLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="staticBackdropLabel">ویرایش دستهبندی</h1>
<div class="block-options">
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
</div>
<div class="modal-body">
<div class="form-floating mb-4">
<input v-model="editCatText" class="form-control" type="text">
<label class="form-label"><span class="text-danger">(لازم)</span> نام دسته بندی</label>
</div>
</div>
<div class="modal-footer">
<button @click="this.submitEditChild" type="button" class="btn btn-primary">
<i class="fa fa-save"></i>
ثبت</button>
<button id="modalEditClose" type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fa fa-close"></i>
انصراف</button>
</div>
</div>
</div>
</div>
<template v-slot:actions>
<v-btn icon variant="text" @click="showSnackbar = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</template>
</v-snackbar>
</v-container>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
import {ref} from "vue";
import Loading from "vue-loading-overlay";
import { ref, onMounted } from "vue";
import treeview from "vue3-treeview";
import "vue3-treeview/dist/style.css";
export default {
name: "list",
components:{
Tree : treeview,
Loading
components: {
Tree: treeview
},
data: ()=>{return {
isLoading: true,
config: {
setup() {
const isLoading = ref(true);
const showAddModal = ref(false);
const showEditModal = ref(false);
const showDeleteDialog = ref(false);
const showSnackbar = ref(false);
const snackbarText = ref('');
const snackbarColor = ref('success');
const selectedNode = ref(0);
const nodeToDelete = ref(null);
const addCatText = ref('');
const editCatText = ref('');
const tree = ref([]);
const config = ref({
roots: [],
opened: true,
openedIcon: {
@ -126,89 +224,258 @@ export default {
viewBox: "0 0 24 24",
draw: `M 12 2 L 12 22 M 2 12 L 22 12`,
},
},
tree: [],
selectedNode:0,
addCatText:'',
editCatText:''
}},
mounted() {
this.loadData();
},
methods:{
loadData(){
axios.post('/api/commodity/cat/get').then((response)=>{
this.tree = response.data.items;
this.config.roots.push(response.data.root.toString());
this.isLoading =false;
})
},
addChild(id){
this.selectedNode = id;
const myModalAlternative = new bootstrap.Modal('#ModalAdd', {})
myModalAlternative.show();
},
submitChild(node){
if(this.addCatText.trim().length === 0 ){
Swal.fire({
text: 'نام دسته‌بندی الزامی است.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
else {
axios.post('/api/commodity/cat/insert',{
upper:this.selectedNode,
text:this.addCatText
}).then((response)=>{
Swal.fire({
text: 'دسته بندی افزوده شد.',
icon: 'success',
confirmButtonText: 'قبول'
}).then((res)=>{
document.getElementById('modalAddClose').click();
window.location.reload();
} );
})
}
},
editeNode(node){
this.selectedNode = node.id;
this.editCatText = node.text;
const myModalAlternative = new bootstrap.Modal('#ModalEdit', {})
myModalAlternative.show();
},
submitEditChild(node){
if(this.editCatText.trim().length === 0 ){
Swal.fire({
text: 'نام دسته‌بندی الزامی است.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
else {
axios.post('/api/commodity/cat/edit',{
id:this.selectedNode,
text:this.editCatText
}).then((response)=>{
Swal.fire({
text: 'دسته بندی ویرایش شد.',
icon: 'success',
confirmButtonText: 'قبول'
}).then((res)=>{
document.getElementById('modalEditClose').click();
window.location.reload();
} );
})
}
},
});
const showNotification = (text, color = 'success') => {
snackbarText.value = text;
snackbarColor.value = color;
showSnackbar.value = true;
};
const loadData = async () => {
isLoading.value = true;
try {
const response = await axios.post('/api/commodity/cat/get');
const convertIdsToString = (items) => {
return Object.entries(items).reduce((acc, [key, value]) => {
acc[key] = {
...value,
id: String(value.id),
parent: value.parent ? String(value.parent) : null,
children: value.children ? value.children.map(child => String(child)) : [],
state: {
...value.state,
disabled: false,
opened: true,
selected: false,
loading: false
}
};
return acc;
}, {});
};
tree.value = convertIdsToString(response.data.items);
config.value = {
...config.value,
roots: [String(response.data.root)],
opened: true,
editing: null,
disabled: false
};
} catch (error) {
console.error('Error loading data:', error);
showNotification('خطا در بارگذاری اطلاعات', 'error');
} finally {
isLoading.value = false;
}
};
const addChild = (id) => {
selectedNode.value = String(id);
showAddModal.value = true;
};
const submitChild = async () => {
if (addCatText.value.trim().length === 0) {
showNotification('نام دسته‌بندی الزامی است', 'error');
return;
}
isLoading.value = true;
try {
const response = await axios.post('/api/commodity/cat/insert', {
upper: selectedNode.value,
text: addCatText.value
});
if (response.data && response.data.id) {
const newNode = {
id: String(response.data.id),
text: addCatText.value,
parent: selectedNode.value,
children: [],
state: {
disabled: false,
opened: true,
selected: false,
loading: false
}
};
if (tree.value[selectedNode.value]) {
if (!tree.value[selectedNode.value].children) {
tree.value[selectedNode.value].children = [];
}
tree.value[selectedNode.value].children.push(String(response.data.id));
}
tree.value[String(response.data.id)] = newNode;
showNotification('دسته‌بندی با موفقیت افزوده شد');
showAddModal.value = false;
addCatText.value = '';
}
} catch (error) {
console.error('Error adding category:', error);
showNotification('خطا در افزودن دسته‌بندی', 'error');
} finally {
isLoading.value = false;
}
};
const editeNode = (node) => {
selectedNode.value = String(node.id);
editCatText.value = node.text;
showEditModal.value = true;
};
const submitEditChild = async () => {
if (editCatText.value.trim().length === 0) {
showNotification('نام دسته‌بندی الزامی است', 'error');
return;
}
isLoading.value = true;
try {
await axios.post('/api/commodity/cat/edit', {
id: selectedNode.value,
text: editCatText.value
});
if (tree.value[selectedNode.value]) {
tree.value[selectedNode.value].text = editCatText.value;
}
showNotification('دسته‌بندی با موفقیت ویرایش شد');
showEditModal.value = false;
editCatText.value = '';
} catch (error) {
console.error('Error editing category:', error);
showNotification('خطا در ویرایش دسته‌بندی', 'error');
} finally {
isLoading.value = false;
}
};
const deleteNode = (node) => {
nodeToDelete.value = node;
showDeleteDialog.value = true;
};
const confirmDelete = async () => {
if (!nodeToDelete.value) return;
isLoading.value = true;
try {
await axios.post('/api/commodity/cat/delete', {
id: nodeToDelete.value.id
});
if (tree.value[nodeToDelete.value.parent]) {
const parentNode = tree.value[nodeToDelete.value.parent];
parentNode.children = parentNode.children.filter(child => child !== nodeToDelete.value.id);
}
delete tree.value[nodeToDelete.value.id];
showNotification('دسته‌بندی با موفقیت حذف شد');
showDeleteDialog.value = false;
nodeToDelete.value = null;
} catch (error) {
console.error('Error deleting category:', error);
showNotification('خطا در حذف دسته‌بندی', 'error');
} finally {
isLoading.value = false;
}
};
onMounted(() => {
loadData();
});
return {
isLoading,
showAddModal,
showEditModal,
showDeleteDialog,
showSnackbar,
snackbarText,
snackbarColor,
selectedNode,
addCatText,
editCatText,
tree,
config,
loadData,
addChild,
submitChild,
editeNode,
submitEditChild,
deleteNode,
confirmDelete
};
}
}
</script>
<style scoped>
.node-input, .node-text, .tree{
.node-input,
.node-text,
.tree {
font-family: 'vazir', sans-serif;
}
:deep(.v-btn) {
min-width: 24px !important;
width: 24px !important;
height: 24px !important;
}
:deep(.v-btn:not(.v-btn--icon)) {
min-width: 100px !important;
width: auto !important;
height: 36px !important;
}
:deep(.v-icon) {
font-size: 16px !important;
}
:deep(.v-tooltip__content) {
font-family: 'vazir', sans-serif !important;
font-size: 12px !important;
}
:deep(.v-card-title) {
padding: 16px !important;
}
:deep(.v-card-text) {
padding: 0 16px !important;
}
:deep(.v-dialog) {
border-radius: 8px !important;
}
:deep(.v-alert) {
border-radius: 8px !important;
}
:deep(.v-snackbar) {
border-radius: 8px !important;
margin-bottom: 16px !important;
}
:deep(.v-snackbar__content) {
padding: 12px !important;
font-family: 'vazir', sans-serif !important;
}
:deep(.v-snackbar__wrapper) {
min-width: 300px !important;
}
:deep(.v-skeleton-loader) {
border-radius: 8px !important;
}
</style>

View file

@ -91,6 +91,36 @@
<v-icon>mdi-filter</v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<!-- فیلتر درختی حسابها -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
فیلتر مرکز هزینه:
<v-btn
v-if="selectedAccountId !== '67'"
size="small"
color="primary"
variant="text"
class="ms-2"
@click="resetAccountFilter"
>
<v-icon size="small" class="me-1">mdi-refresh</v-icon>
بازنشانی
</v-btn>
</v-list-item-title>
<hesabdari-tree-view
v-model="selectedAccountId"
:show-sub-tree="true"
:selectable-only="false"
:initial-account="{ code: '67', name: 'هزینه‌ها' }"
@select="handleAccountSelect"
@account-selected="handleAccountSelected"
/>
</v-list-item>
<v-divider class="my-2"></v-divider>
<!-- فیلترهای زمانی -->
<v-list-item v-for="(filter, index) in timeFilters" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
@ -152,7 +182,7 @@
<v-list density="compact" class="pa-0 ma-0">
<v-list-item :border="true" v-for="(center, index) in item.costCenters" :key="index">
<v-list-item-title>
{{ center.name }}
{{ center.name }} ({{ center.code }})
{{ $t('dialog.acc_price') }} : {{ $filters.formatNumber(center.amount) }}
{{ $t('dialog.des') }} : {{ center.des }}
</v-list-item-title>
@ -211,6 +241,7 @@ import Swal from 'sweetalert2';
import { debounce } from 'lodash';
import { getApiUrl } from '/src/hesabixConfig';
import moment from 'jalali-moment';
import HesabdariTreeView from '@/components/forms/HesabdariTreeView.vue';
const apiUrl = getApiUrl();
axios.defaults.baseURL = apiUrl;
@ -222,7 +253,8 @@ const selectedItems = ref(new Set());
const totalItems = ref(0);
const searchQuery = ref('');
const timeFilter = ref('all');
const expanded = ref([]); // برای مدیریت ردیفهای گسترشیافته
const expanded = ref([]);
const selectedAccountId = ref('67');
// فیلترهای زمانی
const timeFilters = ref([
@ -232,7 +264,7 @@ const timeFilters = ref([
{ label: 'همه', value: 'all', checked: true },
]);
// تعریف ستونهای جدول (ستون costCenter از هدرها حذف شده)
// تعریف ستونهای جدول
const headers = ref([
{ title: '', key: 'checkbox', sortable: false, width: '50', align: 'center' },
{ title: 'ردیف', key: 'index', align: 'center', sortable: false, width: '70' },
@ -265,6 +297,26 @@ const selectedCost = computed(() => {
.reduce((sum, item) => sum + Number(item.amount || 0), 0);
});
// متدهای مدیریت فیلتر حساب
const handleAccountSelect = (account) => {
if (account) {
selectedAccountId.value = account.code;
fetchData();
}
};
const handleAccountSelected = (account) => {
if (account) {
fetchData();
}
};
// متد ریست کردن فیلتر حساب
const resetAccountFilter = () => {
selectedAccountId.value = '67';
fetchData();
};
// فچ کردن دادهها از سرور
const fetchData = async () => {
try {
@ -291,11 +343,13 @@ const fetchData = async () => {
filters.dateFrom = moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD');
filters.dateTo = today;
break;
case 'all':
default:
break;
}
}
// اضافه کردن فیلتر حساب
if (selectedAccountId.value) {
filters.account = selectedAccountId.value;
}
const sortByArray = Array.isArray(serverOptions.value.sortBy) ? serverOptions.value.sortBy : [];
const sortDescArray = Array.isArray(serverOptions.value.sortDesc) ? serverOptions.value.sortDesc : [];

View file

@ -400,7 +400,9 @@ const print = async (allItems = true) => {
if (allItems) {
response = await axios.post('/api/person/receive/list/print');
} else {
response = await axios.post('/api/person/receive/list/print', { items: selectedItems.value });
response = await axios.post('/api/person/receive/list/print', {
items: selectedItems.value
});
}
const printID = response.data.id;

View file

@ -118,29 +118,12 @@
</v-col>
</v-row>
<!-- Print Modal -->
<v-dialog v-model="modal" width="auto">
<v-card :subtitle="$t('dialog.print_info_des')" prepend-icon="mdi-file-pdf-box" :title="$t('dialog.export_pdf')">
<template v-slot:text>
<v-select class=mb-2 v-model="printOptions.paper" :items="paperSizes" :label="$t('dialog.paper_size')">
</v-select>
<v-switch inset v-model="printOptions.bidInfo" color="primary" :label="$t('dialog.bid_info_label')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.note" color="primary" :label="$t('dialog.invoice_footer_note')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.taxInfo" color="primary" :label="$t('dialog.tax_dexpo')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.discountInfo" color="primary" :label="$t('dialog.discount_dexpo')"
hide-details></v-switch>
</template>
<template v-slot:actions>
<v-btn variant="tonal" class="" prepend-icon="mdi-printer" color="primary" :text="$t('dialog.print')"
@click="modal = false; printInvoice()"></v-btn>
<v-btn variant="tonal" class="" prepend-icon="mdi-undo" color="secondary" :text="$t('dialog.cancel')"
@click="modal = false"></v-btn>
</template>
</v-card>
</v-dialog>
<PrintDialog
v-model="modal"
:plugins="plugins"
@print="printInvoice"
@cancel="modal = false"
/>
<!-- End Print Modal -->
<!-- Delete Dialog -->
<v-dialog v-model="deleteDialog" width="auto">
@ -205,9 +188,13 @@
<script>
import axios from "axios";
import { ref, defineComponent } from "vue";
import PrintDialog from '@/components/PrintDialog.vue';
export default defineComponent ({
name: "list",
components: {
PrintDialog
},
data() {
let self = this;
return {
@ -236,8 +223,11 @@ export default defineComponent ({
taxInfo: true,
discountInfo: true,
selectedPrintCode: 0,
paper: 'A4-L'
paper: 'A4-L',
businessStamp: true,
invoiceIndex: true
},
plugins: {},
sumSelected: 0,
sumTotal: 0,
itemsSelected: [],
@ -267,7 +257,20 @@ export default defineComponent ({
}
},
methods: {
isPluginActive(pluginCode) {
return this.plugins && this.plugins[pluginCode] !== undefined;
},
async loadPlugins() {
try {
const response = await axios.post('/api/plugin/get/actives');
this.plugins = response.data || {};
} catch (error) {
console.error('Error loading plugins:', error);
this.plugins = {};
}
},
loadData() {
this.loadPlugins();
axios.post("/api/printers/options/info").then((response) => {
this.printOptions = response.data.sell;
});

View file

@ -14,7 +14,7 @@
<v-spacer></v-spacer>
<v-tooltip :text="$t('dialog.export_pdf')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="primary" @click="printInvoice()"></v-btn>
<v-btn v-bind="props" icon="mdi-file-pdf-box" color="primary" @click="modal = true"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
@ -94,15 +94,28 @@
</v-card-text>
</v-card>
</v-dialog>
<!-- Print Dialog -->
<PrintDialog
v-model="modal"
:plugins="plugins"
@print="handlePrint"
@cancel="modal = false"
/>
<!-- End Print Dialog -->
</div>
</template>
<script>
import axios from "axios";
import { ref, defineComponent } from "vue";
import PrintDialog from '@/components/PrintDialog.vue';
export default defineComponent({
name: "PresellView",
components: {
PrintDialog
},
props: {
code: {
type: [String, Number],
@ -116,7 +129,9 @@ export default defineComponent({
data() {
return {
dialog: this.modelValue,
modal: false,
presellData: {},
plugins: {},
itemsHeaders: [
{ text: "کد کالا", value: "itemCode" },
{ text: "نام کالا", value: "itemName" },
@ -124,14 +139,7 @@ export default defineComponent({
{ text: "قیمت", value: "price" },
{ text: "مبلغ", value: "amount" },
{ text: "جمع", value: "total" }
],
printOptions: {
note: true,
bidInfo: true,
taxInfo: true,
discountInfo: true,
paper: 'A4-L'
}
]
}
},
methods: {
@ -140,25 +148,37 @@ export default defineComponent({
this.$emit('update:modelValue', value);
this.$emit('close');
},
async loadPlugins() {
try {
const response = await axios.post('/api/plugin/get/actives');
this.plugins = response.data;
} catch (error) {
console.error('خطا در بارگذاری افزونه‌ها:', error);
}
},
handlePrint(printOptions) {
this.printInvoice(true, true, printOptions);
},
printInvoice(pdf = true, cloudePrinters = true, printOptions = null) {
axios.post('/api/preinvoice/print/invoice', {
'code': this.code,
'pdf': pdf,
'printers': cloudePrinters,
'printOptions': printOptions
}).then((response) => {
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
});
},
loadData() {
axios.post('/api/preinvoice/get/' + this.code)
.then((response) => {
this.presellData = response.data;
});
},
printInvoice() {
axios.post('/api/preinvoice/print/invoice', {
'code': this.code,
'pdf': true,
'printers': true,
'printOptions': this.printOptions
}).then((response) => {
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');
});
}
},
created() {
this.loadData();
this.loadPlugins();
}
});
</script>

View file

@ -193,28 +193,12 @@
<v-progress-circular color="primary" indeterminate></v-progress-circular>
</v-overlay>
<!-- Print Modal -->
<v-dialog v-model="modal" width="auto">
<v-card :subtitle="$t('dialog.print_info_des')" prepend-icon="mdi-file-pdf-box" :title="$t('dialog.export_pdf')">
<template v-slot:text>
<v-select class="mb-2" v-model="printOptions.paper" :items="paperSizes" :label="$t('dialog.paper_size')">
</v-select>
<v-switch inset v-model="printOptions.bidInfo" color="primary" :label="$t('dialog.bid_info_label')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.note" color="primary" :label="$t('dialog.invoice_footer_note')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.taxInfo" color="primary" :label="$t('dialog.tax_dexpo')"
hide-details></v-switch>
<v-switch inset v-model="printOptions.discountInfo" color="primary" :label="$t('dialog.discount_dexpo')"
hide-details></v-switch>
</template>
<template v-slot:actions>
<v-btn variant="tonal" class="" prepend-icon="mdi-printer" color="primary" :text="$t('dialog.print')"
@click="modal = false; printInvoice()"></v-btn>
<v-btn variant="tonal" class="" prepend-icon="mdi-undo" color="secondary" :text="$t('dialog.cancel')"
@click="modal = false"></v-btn>
</template>
</v-card>
</v-dialog>
<PrintDialog
v-model="modal"
:plugins="plugins"
@print="handlePrint"
@cancel="modal = false"
/>
<!-- End Print Modal -->
<!-- Delete Dialog -->
<v-dialog v-model="deleteDialog" max-width="500">
@ -265,9 +249,13 @@
<script>
import axios from "axios";
import { ref, computed } from 'vue';
import PrintDialog from '@/components/PrintDialog.vue';
export default {
name: "viewInvoice",
components: {
PrintDialog
},
setup() {
const items = ref([]);
const totalInvoice = ref(0);
@ -287,24 +275,12 @@ export default {
const preinvoiceCode = ref('');
const modal = ref(false);
const deleteDialog = ref(false);
const plugins = ref({});
const snackbar = ref({
show: false,
text: '',
color: 'success'
});
const printOptions = ref({
note: true,
bidInfo: true,
taxInfo: true,
discountInfo: true,
paper: 'A4-L'
});
const paperSizes = ref([
{ title: 'A4 عمودی', value: 'A4' },
{ title: 'A4 افقی', value: 'A4-L' },
{ title: 'A5 عمودی', value: 'A5' },
{ title: 'A5 افقی', value: 'A5-L' }
]);
const customerName = computed(() => {
return customer.value?.nikename || customer.value?.name || 'نامشخص';
@ -330,15 +306,15 @@ export default {
preinvoiceCode,
modal,
deleteDialog,
snackbar,
printOptions,
paperSizes
plugins,
snackbar
};
},
mounted() {
const id = this.$route.params.id;
if (id) {
this.loadPreinvoice(id);
this.loadPlugins();
}
},
methods: {
@ -420,13 +396,24 @@ export default {
this.finalTotal = total + this.taxAmount - calculatedTotalDiscount + (Number(this.shippingCost) || 0);
},
printInvoice(pdf = true, cloudePrinters = true) {
async loadPlugins() {
try {
const response = await axios.post('/api/plugin/get/actives');
this.plugins = response.data;
} catch (error) {
console.error('خطا در بارگذاری افزونه‌ها:', error);
}
},
handlePrint(printOptions) {
this.printInvoice(true, true, printOptions);
},
printInvoice(pdf = true, cloudePrinters = true, printOptions = null) {
this.loading = true;
axios.post('/api/preinvoice/print/invoice', {
'code': this.preinvoiceCode,
'pdf': pdf,
'printers': cloudePrinters,
'printOptions': this.printOptions
'printOptions': printOptions
}).then((response) => {
this.loading = false;
window.open(this.$API_URL + '/front/print/' + response.data.id, '_blank', 'noreferrer');

View file

@ -236,20 +236,69 @@
</v-dialog>
<v-dialog v-model="modal" width="auto">
<v-card :subtitle="$t('dialog.print_info_des')" prepend-icon="mdi-file-pdf-box" :title="$t('dialog.export_pdf')">
<template v-slot:text>
<v-card>
<v-toolbar color="primary">
<v-toolbar-title class="text-white">
<v-icon icon="mdi-file-pdf-box" class="ml-2"></v-icon>
{{ $t('dialog.export_pdf') }}
</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip :text="$t('dialog.print')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-printer" color="white" @click="modal = false; printInvoice()"></v-btn>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.cancel')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-close" color="white" @click="modal = false"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-card-text>
<v-select class="mb-2" v-model="printOptions.paper" :items="paperSizes" :label="$t('dialog.paper_size')">
</v-select>
<v-switch inset v-model="printOptions.bidInfo" color="primary" :label="$t('dialog.bid_info_label')" hide-details></v-switch>
<v-switch inset v-model="printOptions.pays" color="primary" :label="$t('dialog.invoice_pays')" hide-details></v-switch>
<v-switch inset v-model="printOptions.note" color="primary" :label="$t('dialog.invoice_footer_note')" hide-details></v-switch>
<v-switch inset v-model="printOptions.taxInfo" color="primary" :label="$t('dialog.tax_dexpo')" hide-details></v-switch>
<v-switch inset v-model="printOptions.discountInfo" color="primary" :label="$t('dialog.discount_dexpo')" hide-details></v-switch>
</template>
<template v-slot:actions>
<v-btn variant="tonal" prepend-icon="mdi-printer" color="primary" :text="$t('dialog.print')" @click="modal = false; printInvoice()"></v-btn>
<v-btn variant="tonal" prepend-icon="mdi-undo" color="secondary" :text="$t('dialog.cancel')" @click="modal = false"></v-btn>
</template>
<v-row>
<v-col cols="12" sm="6">
<v-tooltip :text="$t('dialog.bid_info_label')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.bidInfo" color="primary" :label="$t('dialog.bid_info_label')" hide-details></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_pays')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.pays" color="primary" :label="$t('dialog.invoice_pays')" hide-details></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_footer_note')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.note" color="primary" :label="$t('dialog.invoice_footer_note')" hide-details></v-switch>
</template>
</v-tooltip>
</v-col>
<v-col cols="12" sm="6">
<v-tooltip :text="$t('dialog.tax_dexpo')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.taxInfo" color="primary" :label="$t('dialog.tax_dexpo')" hide-details></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.discount_dexpo')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-bind="props" inset v-model="printOptions.discountInfo" color="primary" :label="$t('dialog.discount_dexpo')" hide-details></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.business_stamp')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-if="isPluginActive('accpro')" v-bind="props" inset v-model="printOptions.businessStamp" color="primary" :label="$t('dialog.business_stamp')" hide-details></v-switch>
</template>
</v-tooltip>
<v-tooltip :text="$t('dialog.invoice_index')" location="right">
<template v-slot:activator="{ props }">
<v-switch v-if="isPluginActive('accpro')" v-bind="props" inset v-model="printOptions.invoiceIndex" color="primary" :label="$t('dialog.invoice_index')" hide-details></v-switch>
</template>
</v-tooltip>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</div>
@ -280,8 +329,11 @@ export default defineComponent({
taxInfo: true,
discountInfo: true,
selectedPrintCode: 0,
paper: 'A4-L'
paper: 'A4-L',
businessStamp: true,
invoiceIndex: true
},
plugins: {},
sumSelected: 0,
sumTotal: 0,
itemsSelected: [],
@ -330,6 +382,18 @@ export default defineComponent({
},
},
methods: {
isPluginActive(pluginCode) {
return this.plugins && this.plugins[pluginCode] !== undefined;
},
async loadPlugins() {
try {
const response = await axios.post('/api/plugin/get/actives');
this.plugins = response.data || {};
} catch (error) {
console.error('Error loading plugins:', error);
this.plugins = {};
}
},
changeLabel(label) {
if (this.itemsSelected.length === 0) {
Swal.fire({
@ -606,6 +670,7 @@ export default defineComponent({
},
created() {
this.loadColumnSettings();
this.loadPlugins();
this.loadData();
},
watch: {

View file

@ -1083,7 +1083,7 @@
this.items.push({
commodity: item.commodity,
count: item.commodity_count,
price: parseInt((parseInt(item.bs) - parseInt(item.tax) + parseInt(item.discount)) / parseInt(item.commodity_count)),
price: parseFloat((parseFloat(item.bs) - parseFloat(item.tax) + parseFloat(item.discount)) / parseFloat(item.commodity_count)),
bs: item.bs,
bd: item.bd,
type: 'commodity',
@ -1091,7 +1091,7 @@
des: item.des,
discount: item.discount,
tax: item.tax,
sumWithoutTax: item.bs - item.tax,
sumWithoutTax: parseFloat(item.bs) - parseFloat(item.tax),
sumTotal: item.bs,
table: 53
});

View file

@ -83,6 +83,12 @@
<v-col cols="12" sm="6" md="4" lg="3">
<v-switch v-model="settings.sell.discountInfo" label="تخفیف به تفکیک اقلام" color="primary" hide-details density="compact"></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" lg="3">
<v-switch v-model="settings.sell.businessStamp" label="مهر کسب و کار" color="primary" hide-details density="compact"></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" lg="3">
<v-switch v-model="settings.sell.invoiceIndex" label="نمایه فاکتور" color="primary" hide-details density="compact"></v-switch>
</v-col>
</v-row>
</v-col>
</v-row>
@ -283,6 +289,8 @@ export default {
taxInfo: true,
discountInfo: true,
paper: 'A4-L',
businessStamp: true,
invoiceIndex: true
},
buy: {
pays: true,

View file

@ -1,256 +1,574 @@
<template>
<div class="block block-content-full">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<router-link class="text-warning mx-2 px-2" to="/acc/business/users">
<i class="fa fw-bold fa-arrow-right"></i>
</router-link>
تنظیمات دسترسی
</h3>
<div class="block-options">
<div>
<v-toolbar color="toolbar" title="تنظیمات دسترسی">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
</v-toolbar>
<v-container>
<v-card class="mb-4" elevation="2">
<v-card-text>
<v-alert
type="info"
variant="tonal"
class="mb-4 rounded-lg custom-alert"
border="start"
border-color="primary"
elevation="2"
>
<template v-slot:prepend>
<v-avatar color="primary" class="me-3">
<v-icon color="white">mdi-shield-account</v-icon>
</v-avatar>
</template>
<div class="d-flex flex-column">
<div class="text-h6 font-weight-bold mb-3 text-indigo-darken-2">
تنظیم دسترسیهای کاربر
</div>
<v-card variant="outlined" class="mb-3 pa-3 custom-card">
<div class="d-flex align-center mb-2">
<v-icon color="indigo-darken-2" class="me-2">mdi-account-circle</v-icon>
<span class="text-body-1 text-grey-darken-1">نام کاربر:</span>
<strong class="text-body-1 me-2 text-indigo-darken-2">{{ info.user }}</strong>
</div>
<div class="d-flex align-center">
<v-icon color="indigo-darken-2" class="me-2">mdi-email-outline</v-icon>
<span class="text-body-1 text-grey-darken-1">پست الکترونیکی:</span>
<strong class="text-body-1 text-indigo-darken-2">{{ info.email }}</strong>
</div>
</v-card>
<v-alert
type="info"
variant="tonal"
class="mt-2 custom-warning"
density="compact"
border="start"
border-color="indigo-darken-2"
>
<template v-slot:prepend>
<v-icon color="indigo-darken-2">mdi-shield-alert</v-icon>
</template>
<div class="text-body-2 text-indigo-darken-2">
لطفاً در تنظیم دسترسیها دقت کنید. دسترسیهای نامناسب میتواند امنیت سیستم را به خطر بیندازد.
</div>
</v-alert>
</div>
</v-alert>
</div>
</div>
<div class="block-content p-0">
<div class="alert alert-primary">
ویرایش دسترسیهای کاربر با نام
<span class="fw-bold">{{ this.info.user }}</span>
با پست الکترونیکی
<span class="fw-bold">{{ this.info.email }}</span>
</div>
<div class="container-fluid">
<div class="row">
<label class="form-label">دسترسی ها</label>
<div class="col-sm-12 col-md-4">
<div class="mb-2">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.persons" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">اشخاص</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.getpay" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">دریافت و پرداخت</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.commodity" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">کالا و خدمات</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.bank" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">حسابهای بانکی</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.salary" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">تنخواه گردانها</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.bankTransfer" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">انتقال بین بانکی</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.archiveUpload" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">افزودن فایل به آرشیو</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.archiveView" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">مشاهده فایل های آرشیو</label>
</div>
</div>
</div>
</div>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.buy" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">فاکتورهای خرید</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.sell" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">فاکتورهای فروش</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.cost" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">هزینهها</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.income" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">درآمدها</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.report" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">گزارشات</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.cashdesk" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">صندوقها</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.shareholder" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">سهامداران</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.archiveMod" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">ویرایش فایل های آرشیو</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.cheque" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">مدیریت چکها</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.accounting" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">اسناد حسابداری</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.settings" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">تنظیمات کسب و کار</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.log" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">تاریخچه کسبوکار</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.permission" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">تغییر سطوح دسترسی</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.store" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">انبارداری</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.wallet" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">کیف پول</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.archiveDelete" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">حذف فایل های آرشیو</label>
</div>
</div>
</div>
</div>
<div v-show="this.isPluginActive('accpro')" class="row mt-2">
<b>بسته حسابداری پیشرفته</b>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugAccproRfbuy" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">فاکتورهای برگشت از خرید</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugAccproCloseYear" @change="savePerms()" class="form-check-input"
type="checkbox">
<label class="form-check-label">بستن سال مالی</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugAccproRfsell" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">فاکتورهای برگشت از فروش</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugAccproAccounting" @change="savePerms()" class="form-check-input"
type="checkbox">
<label class="form-check-label">صدور و ویرایش اسناد حسابداری</label>
</div>
</div>
</div>
</div>
<div v-show="this.isPluginActive('repservice')" class="row mt-2">
<b>افزونه تعمیرکاران</b>
<div class="col-sm-12 col-md-4">
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugRepservice" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">ثبت و ویرایش</label>
</div>
</div>
</div>
</div>
<div class="row mt-2">
<div v-show="this.isPluginActive('noghre')" class="col-sm-12 col-md-4">
<b>افزونه کارگاه نقره سازی</b>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugNoghreAdmin" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">مدیر</label>
</div>
</div>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugNoghreSell" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">صندوق / فروش</label>
</div>
</div>
</div>
<div v-show="this.isPluginActive('cc')" class="col-sm-12 col-md-4">
<b>افزونه باشگاه مشتریان</b>
<div class="space-y-2">
<div class="form-check form-switch">
<input v-model="info.plugCCAdmin" @change="savePerms()" class="form-check-input" type="checkbox">
<label class="form-check-label">مدیر</label>
</div>
</div>
</div>
</div>
</div>
</div>
<v-row>
<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.persons"
label="اشخاص"
@change="savePerms('persons')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.persons"
:disabled="loadingSwitches.persons"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.getpay"
label="دریافت و پرداخت"
@change="savePerms('getpay')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.getpay"
:disabled="loadingSwitches.getpay"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.commodity"
label="کالا و خدمات"
@change="savePerms('commodity')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.commodity"
:disabled="loadingSwitches.commodity"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.bank"
label="حساب‌های بانکی"
@change="savePerms('bank')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.bank"
:disabled="loadingSwitches.bank"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.salary"
label="تنخواه گردان‌ها"
@change="savePerms('salary')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.salary"
:disabled="loadingSwitches.salary"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.bankTransfer"
label="انتقال بین بانکی"
@change="savePerms('bankTransfer')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.bankTransfer"
:disabled="loadingSwitches.bankTransfer"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.archiveUpload"
label="افزودن فایل به آرشیو"
@change="savePerms('archiveUpload')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.archiveUpload"
:disabled="loadingSwitches.archiveUpload"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.archiveView"
label="مشاهده فایل های آرشیو"
@change="savePerms('archiveView')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.archiveView"
:disabled="loadingSwitches.archiveView"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</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.buy"
label="فاکتورهای خرید"
@change="savePerms('buy')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.buy"
:disabled="loadingSwitches.buy"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.sell"
label="فاکتورهای فروش"
@change="savePerms('sell')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.sell"
:disabled="loadingSwitches.sell"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.cost"
label="هزینه‌ها"
@change="savePerms('cost')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.cost"
:disabled="loadingSwitches.cost"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.income"
label="درآمدها"
@change="savePerms('income')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.income"
:disabled="loadingSwitches.income"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.report"
label="گزارشات"
@change="savePerms('report')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.report"
:disabled="loadingSwitches.report"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.cashdesk"
label="صندوق‌ها"
@change="savePerms('cashdesk')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.cashdesk"
:disabled="loadingSwitches.cashdesk"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.shareholder"
label="سهامداران"
@change="savePerms('shareholder')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.shareholder"
:disabled="loadingSwitches.shareholder"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.archiveMod"
label="ویرایش فایل های آرشیو"
@change="savePerms('archiveMod')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.archiveMod"
:disabled="loadingSwitches.archiveMod"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</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.cheque"
label="مدیریت چک‌ها"
@change="savePerms('cheque')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.cheque"
:disabled="loadingSwitches.cheque"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.accounting"
label="اسناد حسابداری"
@change="savePerms('accounting')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.accounting"
:disabled="loadingSwitches.accounting"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.settings"
label="تنظیمات کسب و کار"
@change="savePerms('settings')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.settings"
:disabled="loadingSwitches.settings"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.log"
label="تاریخچه کسب‌و‌کار"
@change="savePerms('log')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.log"
:disabled="loadingSwitches.log"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.permission"
label="تغییر سطوح دسترسی"
@change="savePerms('permission')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.permission"
:disabled="loadingSwitches.permission"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.store"
label="انبارداری"
@change="savePerms('store')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.store"
:disabled="loadingSwitches.store"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.wallet"
label="کیف پول"
@change="savePerms('wallet')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.wallet"
:disabled="loadingSwitches.wallet"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.archiveDelete"
label="حذف فایل های آرشیو"
@change="savePerms('archiveDelete')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.archiveDelete"
:disabled="loadingSwitches.archiveDelete"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row v-if="isPluginActive('accpro')" 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.plugAccproRfbuy"
label="فاکتورهای برگشت از خرید"
@change="savePerms('plugAccproRfbuy')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugAccproRfbuy"
:disabled="loadingSwitches.plugAccproRfbuy"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.plugAccproCloseYear"
label="بستن سال مالی"
@change="savePerms('plugAccproCloseYear')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugAccproCloseYear"
:disabled="loadingSwitches.plugAccproCloseYear"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</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.plugAccproRfsell"
label="فاکتورهای برگشت از فروش"
@change="savePerms('plugAccproRfsell')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugAccproRfsell"
:disabled="loadingSwitches.plugAccproRfsell"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.plugAccproPresell"
label="پیش فاکتور فروش"
@change="savePerms('plugAccproPresell')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugAccproPresell"
:disabled="loadingSwitches.plugAccproPresell"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</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.plugAccproAccounting"
label="صدور و ویرایش اسناد حسابداری"
@change="savePerms('plugAccproAccounting')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugAccproAccounting"
:disabled="loadingSwitches.plugAccproAccounting"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row v-if="isPluginActive('repservice')" 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.plugRepservice"
label="ثبت و ویرایش"
@change="savePerms('plugRepservice')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugRepservice"
:disabled="loadingSwitches.plugRepservice"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-row class="mt-4">
<v-col v-if="isPluginActive('noghre')" cols="12" md="4">
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه کارگاه نقره سازی</v-card-title>
<v-card variant="outlined" class="h-100">
<v-card-text>
<v-list>
<v-list-item>
<v-switch
v-model="info.plugNoghreAdmin"
label="مدیر"
@change="savePerms('plugNoghreAdmin')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugNoghreAdmin"
:disabled="loadingSwitches.plugNoghreAdmin"
></v-switch>
</v-list-item>
<v-list-item>
<v-switch
v-model="info.plugNoghreSell"
label="صندوق / فروش"
@change="savePerms('plugNoghreSell')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugNoghreSell"
:disabled="loadingSwitches.plugNoghreSell"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
<v-col v-if="isPluginActive('cc')" cols="12" md="4">
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه باشگاه مشتریان</v-card-title>
<v-card variant="outlined" class="h-100">
<v-card-text>
<v-list>
<v-list-item>
<v-switch
v-model="info.plugCCAdmin"
label="مدیر"
@change="savePerms('plugCCAdmin')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugCCAdmin"
:disabled="loadingSwitches.plugCCAdmin"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</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>
@ -258,33 +576,92 @@ import axios from "axios";
export default {
name: "user_perm_edit",
data: () => {
return {
info: {},
plugins: {}
}
},
data: () => ({
info: {},
plugins: {},
snackbar: {
show: false,
text: '',
color: 'success'
},
loadingSwitches: {}
}),
methods: {
isPluginActive(plugName) {
return this.plugins[plugName] !== undefined;
},
getData(id) {
// مقداردهی اولیه همه فیلدها با false
const defaultPermissions = {
persons: false,
getpay: false,
commodity: false,
bank: false,
salary: false,
bankTransfer: false,
archiveUpload: false,
archiveView: false,
buy: false,
sell: false,
cost: false,
income: false,
report: false,
cashdesk: false,
shareholder: false,
archiveMod: false,
cheque: false,
accounting: false,
settings: false,
log: false,
permission: false,
store: false,
wallet: false,
archiveDelete: false,
plugAccproRfbuy: false,
plugAccproCloseYear: false,
plugAccproRfsell: false,
plugAccproPresell: false,
plugAccproAccounting: false,
plugRepservice: false,
plugNoghreAdmin: false,
plugNoghreSell: false,
plugCCAdmin: false
};
axios.post('/api/business/get/user/permissions',
{
'bid': localStorage.getItem('activeBid'),
'email': id
}
).then((response) => {
this.info = response.data;
this.info.bid = localStorage.getItem('activeBid');
// ترکیب مقادیر پیشفرض با مقادیر دریافتی از سرور
this.info = {
...defaultPermissions,
...response.data,
bid: localStorage.getItem('activeBid')
};
});
//get active plugins
axios.post('/api/plugin/get/actives',).then((response) => {
this.plugins = response.data;
});
},
savePerms() {
axios.post('/api/business/save/user/permissions', this.info)
async savePerms() {
try {
await axios.post('/api/business/save/user/permissions', this.info);
this.snackbar = {
show: true,
text: 'دسترسی‌ها با موفقیت ذخیره شد',
color: 'success'
};
} catch (error) {
console.error('Error saving permissions:', error);
this.snackbar = {
show: true,
text: 'خطا در ذخیره دسترسی‌ها',
color: 'error'
};
}
}
},
beforeRouteEnter(to, from, next) {
@ -295,4 +672,34 @@ export default {
}
</script>
<style scoped></style>
<style scoped>
.v-list-item {
min-height: 48px;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
}
.v-switch {
margin-right: 16px;
}
.custom-alert {
background: linear-gradient(to right, #f8f9fa, #e9ecef);
}
.custom-card {
background: linear-gradient(to right, #ffffff, #f8f9fa);
border-color: #dee2e6;
}
.custom-warning {
background: linear-gradient(to right, #e8eaf6, #c5cae9);
}
.text-indigo-darken-2 {
color: #3949ab !important;
}
.v-switch.v-input--disabled {
opacity: 0.7;
}
</style>

View file

@ -1,148 +1,226 @@
<template>
<div class="block block-content-full">
<div id="fixed-header" class="block-header block-header-default bg-gray-light" >
<h3 class="block-title text-primary-dark">
<i class="fa fa-users-gear"></i>
کاربران و دسترسیها </h3>
<div class="block-options">
</div>
</div>
<div class="block-content p-0">
<div class="mb-4">
<div class="input-group p-3">
<div class="form-floating">
<input v-model="newEmail" class="form-control" type="email">
<label for="example-group3-floating2">برای افزودن کاربر جدید پست الکترونیکی را وارد کنید.</label>
</div>
<button @click="submitData" class="btn btn-primary" type="button"> افزودن </button>
<template> <v-toolbar color="toolbar" title="کاربران و دسترسی‌ها">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
</v-toolbar>
<v-container>
<v-row>
<v-col cols="12">
<v-form @submit.prevent="submitData">
<v-row>
<v-col cols="10">
<v-text-field v-model="newEmail" label="برای افزودن کاربر جدید پست الکترونیکی را وارد کنید" type="email"
variant="outlined" density="comfortable" class="email-field"></v-text-field>
</v-col>
<v-col cols="2">
<v-btn color="primary" type="submit" block class="submit-btn">
افزودن
</v-btn>
</v-col>
</v-row>
</v-form>
</v-col>
</v-row>
<v-data-table :headers="headers" :items="users" :items-per-page="10" class="elevation-1">
<template v-slot:item.index="{ index }">
{{ index + 1 }}
</template>
<template v-slot:item.name="{ item }">
<span class="font-weight-bold text-primary">{{ item.name }}</span>
</template>
<template v-slot:item.email="{ item }">
<v-chip color="primary" size="small">{{ item.email }}</v-chip>
</template>
<template v-slot:item.mobile="{ item }">
<v-chip color="info" size="small">{{ item.mobile || '-' }}</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<template v-if="item.owner != 1">
<v-btn-group>
<v-btn :to="{ name: 'business_user_roll_edit', params: { email: item.email } }" icon="mdi-pencil"
variant="text" color="primary" size="small"></v-btn>
<v-btn @click="confirmDelete(item.email)" icon="mdi-delete" variant="text" color="error"
size="small"></v-btn>
</v-btn-group>
</template>
<v-chip v-else color="success" size="small">مدیر کل</v-chip>
</template>
</v-data-table>
</v-container>
<!-- دیالوگ تایید حذف -->
<v-dialog v-model="deleteDialog" max-width="400" transition="dialog-bottom-transition">
<v-card class="rounded-lg" title="تایید حذف کاربر">
<template v-slot:prepend>
<v-avatar>
<v-icon color="error">mdi-alert-circle</v-icon>
</v-avatar>
</template>
<v-divider></v-divider>
<v-card-text class="pt-4">
<div class="text-body-1">
آیا از حذف کاربر <span class="font-weight-bold text-primary">{{ userToDelete?.name }}</span> با ایمیل <span class="font-weight-bold text-primary">{{ userToDelete?.email }}</span> اطمینان دارید؟
</div>
</div>
<table class="table table-hover table-vcenter table-sm">
<thead>
<tr>
<th class="text-center" style="width: 50px;">#</th>
<th>نام / نام خانوادگی</th>
<th>پست الکترونیکی</th>
<th class="text-center" style="width: 100px;">عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="(item,index) in users">
<th class="text-center" scope="row">{{ index + 1 }}</th>
<td class="fw-semibold text-primary-dark">{{ item.name}}</td>
<td class="d-none d-sm-table-cell">
<span class="badge bg-primary">{{item.email}}</span>
</td>
<td class="text-center">
<div class="btn-group btn-group-sm" v-if="item.owner != 1">
<router-link :to="{'name':'business_user_roll_edit','params':{'email':item.email}}" class="btn btn-alt-primary" type="button" aria-label=" ویرایش ">
<i class="fa fa-pencil-alt"></i>
</router-link>
<button @click="deleteUser(item.email)" class="btn btn-alt-primary" type="button" aria-label=" حذف ">
<i class="fa fa-times"></i>
</button>
</div>
<span class="badge bg-success" v-if="item.owner == 1">مدیر کل</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="text-caption text-medium-emphasis mt-2">
این عملیات غیرقابل بازگشت است.
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="deleteDialog = false" class="px-4">
انصراف
</v-btn>
<v-btn color="error" variant="flat" @click="confirmDeleteAction" class="px-4">
<v-icon start>mdi-delete</v-icon>
حذف کاربر
</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 variant="text" @click="snackbar.show = false">
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
export default {
name: "user_rolls",
data: ()=>{return{
users : {},
data: () => ({
users: [],
newEmail: '',
}},
methods:{
deleteUser(email){
Swal.fire({
title: 'آیا برای حذف کاربر مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: `خیر`,
}).then((result) => {
/* Read more about isConfirmed, isDenied below */
if (result.isConfirmed) {
axios.post('/api/business/delete/user',{
'bid':localStorage.getItem('activeBid'),
'email': email}
).then((response)=>{
if(response.data.result == 1){
let index = 0;
for(let z=0; z<this.users.length; z++){
index ++;
if(this.users[z]['email'] == email){
this.users.splice(index -1 ,1);
}
}
Swal.fire({
text: 'کاربر با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
})
}
})
headers: [
{ title: '#', key: 'index', align: 'center', sortable: false },
{ title: 'نام / نام خانوادگی', key: 'name', align: 'center' },
{ title: 'پست الکترونیکی', key: 'email', align: 'center' },
{ title: 'شماره موبایل', key: 'mobile', align: 'center' },
{ title: 'عملیات', key: 'actions', align: 'center', sortable: false }
],
deleteDialog: false,
userToDelete: null,
snackbar: {
show: false,
text: '',
color: 'success'
}
}),
methods: {
showSnackbar(text, color = 'success') {
this.snackbar.text = text;
this.snackbar.color = color;
this.snackbar.show = true;
},
submitData(){
if(this.newEmail == ''){
Swal.fire({
text: 'پست الکترونیکی را وارد کنید.',
icon: 'error',
confirmButtonText: 'قبول'
confirmDelete(email) {
const user = this.users.find(u => u.email === email);
this.userToDelete = {
email: email,
name: user ? user.name : email
};
this.deleteDialog = true;
},
confirmDeleteAction() {
if (this.userToDelete) {
const emailToDelete = this.userToDelete.email;
axios.post('/api/business/delete/user', {
'bid': localStorage.getItem('activeBid'),
'email': emailToDelete
}).then((response) => {
if (response.data.result == 1) {
this.users = this.users.filter(user => user.email !== emailToDelete);
this.showSnackbar('کاربر با موفقیت حذف شد.');
} else {
this.showSnackbar('خطا در حذف کاربر', 'error');
}
this.deleteDialog = false;
this.userToDelete = null;
}).catch((error) => {
this.showSnackbar('خطا در حذف کاربر', 'error');
this.deleteDialog = false;
this.userToDelete = null;
});
}
else {
axios.post("/api/business/add/user",{'bid':localStorage.getItem('activeBid'),'email': this.newEmail}).then((response)=>{
if(response.data.result == 0){
Swal.fire({
text: 'کاربری با این پست الکترونیکی یافت نشد.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
else if(response.data.result == 1){
Swal.fire({
text: 'قبلا این کاربر به کسب و کار افزوده شده است.',
icon: 'error',
confirmButtonText: 'قبول'
});
}
else{
let temp = {
'name':response.data.data.name,
'email':response.data.data.email,
'owner':response.data.data.owner
};
this.users.push(temp);
Swal.fire({
text: 'کاربر با موفقیت عضو کسب و کار شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
this.newEmail = '';
});
},
submitData() {
if (this.newEmail == '') {
this.showSnackbar('پست الکترونیکی را وارد کنید.', 'error');
return;
}
axios.post("/api/business/add/user", {
'bid': localStorage.getItem('activeBid'),
'email': this.newEmail
}).then((response) => {
if (response.data.result == 0) {
this.showSnackbar('کاربری با این پست الکترونیکی یافت نشد.', 'error');
} else if (response.data.result == 1) {
this.showSnackbar('قبلا این کاربر به کسب و کار افزوده شده است.', 'error');
} else {
this.users.push({
'name': response.data.data.name,
'email': response.data.data.email,
'owner': response.data.data.owner,
'mobile': response.data.data.mobile || null
});
this.showSnackbar('کاربر با موفقیت عضو کسب و کار شد.');
}
this.newEmail = '';
}).catch(() => {
this.showSnackbar('خطا در افزودن کاربر', 'error');
});
}
},
mounted() {
axios.post('/api/user/get/users/of/business/' + localStorage.getItem('activeBid')).then((response)=>{
this.users = response.data;
})
axios.post('/api/user/get/users/of/business/' + localStorage.getItem('activeBid')).then((response) => {
this.users = Array.isArray(response.data) ? response.data : [];
}).catch(() => {
this.showSnackbar('خطا در دریافت لیست کاربران', 'error');
});
}
}
</script>
<style scoped>
.v-table {
width: 100%;
}
.email-field :deep(.v-field) {
height: 56px;
}
.submit-btn {
height: 56px;
}
.v-data-table :deep(th),
.v-data-table :deep(td) {
text-align: center !important;
vertical-align: middle;
}
.v-data-table :deep(.v-chip) {
margin: 0 auto;
}
</style>

View file

@ -36,12 +36,9 @@
<v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
<Hdatepicker
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
@ -176,6 +173,7 @@
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
interface TransferType {
id: number;

View file

@ -36,12 +36,9 @@
<v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
<Hdatepicker
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
@ -176,6 +173,7 @@
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
interface TransferType {
id: number;
@ -302,12 +300,51 @@ const submit = async () => {
}
const response = await axios.post('/api/storeroom/ticket/insert', {
doc: doc.value,
ticket: {
...ticket.value,
senderTel: ticket.value.person.mobile || ''
doc: {
id: doc.value.id,
dateSubmit: doc.value.dateSubmit,
date: doc.value.date,
type: doc.value.type,
code: doc.value.code,
des: doc.value.des
},
items: items.value
ticket: {
type: ticket.value.type,
typeString: ticket.value.typeString,
date: ticket.value.date,
des: ticket.value.des,
transfer: ticket.value.transfer,
receiver: ticket.value.receiver,
code: ticket.value.code,
store: {
id: ticket.value.store.id,
name: ticket.value.store.name,
manager: ticket.value.store.manager,
des: ticket.value.store.des
},
person: {
id: ticket.value.person.id,
code: ticket.value.person.code,
nikename: ticket.value.person.nikename,
name: ticket.value.person.name,
tel: ticket.value.person.tel,
mobile: ticket.value.person.mobile,
address: ticket.value.person.address
},
transferType: ticket.value.transferType,
referral: ticket.value.referral,
senderTel: ticket.value.person.mobile || '',
sms: false
},
items: items.value.map(item => ({
id: item.id,
bs: item.bs,
bd: item.bd,
ticketCount: item.ticketCount,
des: item.des,
referral: item.referral,
type: item.type
}))
})
if (response.data.result === 0) {

View file

@ -36,12 +36,9 @@
<v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
<Hdatepicker
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
@ -176,6 +173,7 @@
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
interface TransferType {
id: number;

View file

@ -49,12 +49,9 @@
<v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
<Hdatepicker
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
@ -198,6 +195,7 @@
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
interface TransferType {
id: number;

View file

@ -1,66 +1,114 @@
<template>
<v-toolbar title="جادوگر">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
</v-toolbar>
<v-toolbar color="toolbar" title="هوش مصنوعی حسابیکس">
</v-toolbar>
<div class="page-container">
<div class="content-container">
<v-card class="chat-container" elevation="0">
<div class="chat-box">
<div class="messages-container" ref="messagesContainer">
<!-- پیام هوش مصنوعی -->
<div class="message ai-message" v-if="displayWelcome">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayWelcome }}</div>
</div>
</div>
<v-container class="mt-8 px-2">
<v-row justify="center" class="wizard-content">
<v-col cols="12" md="10">
<v-card class="elevation-0 main-card">
<v-card-title class="text-h4 text-center mb-4">
<v-icon color="primary" size="40" class="mr-2">mdi-wizard-hat</v-icon>
جادوگر هوش مصنوعی
</v-card-title>
<v-card-text class="text-body-1 text-center">
<p class="mb-4">
به زودی با جادوگر هوش مصنوعی آشنا خواهید شد! این ابزار قدرتمند به شما امکان میدهد تا با استفاده از هوش مصنوعی، ماژولهای جدید را به صورت پویا به نرمافزار اضافه کنید. دوران انحصار نرمافزارهای حسابداری به پایان رسیده است و شما میتوانید نرمافزار خود را بهروز کرده و آن را با نیازهای خاص کسبوکار خود تطبیق دهید، بدون نیاز به انتظار برای بهروزرسانیهای طولانی مدت یا پرداخت هزینههای گزاف برای سفارشیسازی.
</p>
</v-card-text>
<div class="message ai-message" v-if="displayThanks">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayThanks }}</div>
</div>
</div>
<v-row class="mt-4">
<v-col cols="12" md="4">
<v-card class="elevation-0 pa-4 text-center feature-card">
<v-icon color="success" size="40" class="mb-2">mdi-robot</v-icon>
<h3 class="text-h6 mb-2">هوش مصنوعی پیشرفته</h3>
<p class="text-body-2">استفاده از آخرین تکنولوژیهای هوش مصنوعی برای ایجاد ماژولهای سفارشی</p>
</v-card>
</v-col>
<div class="message ai-message" v-if="displayCapabilities">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayCapabilities }}</div>
</div>
</div>
<v-col cols="12" md="4">
<v-card class="elevation-0 pa-4 text-center feature-card">
<v-icon color="info" size="40" class="mb-2">mdi-puzzle</v-icon>
<h3 class="text-h6 mb-2">ماژولهای پویا</h3>
<p class="text-body-2">ایجاد و اضافه کردن ماژولهای جدید بدون نیاز به کدنویسی</p>
</v-card>
</v-col>
<div class="message ai-message" v-for="(capability, index) in displayCapabilitiesList" :key="index">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">
<v-icon color="#1a237e" size="16" class="mr-2">mdi-check-circle</v-icon>
{{ capability }}
</div>
</div>
</div>
<v-col cols="12" md="4">
<v-card class="elevation-0 pa-4 text-center feature-card">
<v-icon color="warning" size="40" class="mb-2">mdi-lightning-bolt</v-icon>
<h3 class="text-h6 mb-2">سرعت بالا</h3>
<p class="text-body-2">توسعه سریع و کارآمد با استفاده از ابزارهای هوشمند</p>
</v-card>
</v-col>
</v-row>
<div class="message ai-message" v-if="displayPrompt">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text typing-text">{{ displayPrompt }}</div>
</div>
</div>
<v-card-actions class="justify-center mt-6">
<v-btn color="primary" size="large" prepend-icon="mdi-clock-outline" disabled>
به زودی...
</v-btn>
</v-card-actions>
<!-- پیامهای کاربر و پاسخهای هوش مصنوعی -->
<template v-for="(message, index) in userMessages" :key="index">
<!-- پیام کاربر -->
<div class="message user-message" v-if="typeof message === 'string'">
<div class="message-content">
<div class="message-text">{{ message }}</div>
</div>
<v-avatar color="grey lighten-2" size="36" class="ml-2">
<v-icon color="grey darken-1" size="20">mdi-account</v-icon>
</v-avatar>
</div>
<!-- پیام هوش مصنوعی -->
<div class="message ai-message" v-else-if="message && message.isAI">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content" :class="{ 'details-message': message.isDetails }">
<div class="message-text" v-html="message.text.replace(/\n/g, '<br>')"></div>
</div>
</div>
</template>
<!-- نشانگر تایپ -->
<div class="message ai-message" v-if="isTyping">
<v-avatar color="#1a237e" size="36" class="mr-2">
<v-icon color="white" size="20">mdi-robot</v-icon>
</v-avatar>
<div class="message-content">
<div class="message-text">
<span class="typing-indicator">
<span></span>
<span></span>
<span></span>
</span>
</div>
</div>
</div>
</div>
<!-- باکس ورودی پیام -->
<div class="input-container">
<v-textarea v-model="userMessage" placeholder="پیام خود را اینجا بنویسید..." rows="1" auto-grow hide-details
variant="plain" class="message-input" @keydown.enter.prevent="sendMessage"></v-textarea>
<v-btn color="#1a237e" icon :loading="isLoading" @click="sendMessage" class="send-button"
:disabled="!userMessage.trim()">
<v-icon>mdi-send</v-icon>
</v-btn>
</div>
</div>
</v-card>
</v-col>
</v-row>
</v-container>
</div>
</div>
</template>
<script>
@ -68,66 +116,363 @@ export default {
name: 'WizardHome',
data() {
return {
userMessage: '',
isLoading: false,
isTyping: true,
userMessages: [],
aiResponses: [
{
message: 'با عرض پوزش، در حال حاضر سخت‌افزار پردازش داده متصل نشده است. لطفاً با پشتیبانی فنی تماس بگیرید تا در اسرع وقت مشکل را برطرف کنیم.',
details: 'برای اتصال سخت‌افزار پردازش داده، نیاز به تنظیمات خاصی است که باید توسط تیم فنی انجام شود. این تنظیمات شامل:\n- اتصال به سرور پردازش\n- تنظیم پارامترهای امنیتی\n- راه‌اندازی ماژول‌های پردازشی\nمیباشد.'
},
{
message: 'متأسفانه در حال حاضر سیستم پردازش داده در دسترس نیست. این مشکل موقت است و به زودی برطرف خواهد شد.',
details: 'برای فعال‌سازی کامل سیستم، نیاز به انجام مراحل زیر است:\n- تأیید اتصال به سرور مرکزی\n- راه‌اندازی ماژول‌های پردازشی\n- تنظیم پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'با کمال تأسف، سخت‌افزار پردازش داده هنوز آماده بهره‌برداری نیست. این مشکل به زودی برطرف خواهد شد.',
details: 'برای راه‌اندازی کامل سیستم، تیم فنی در حال انجام مراحل زیر است:\n- نصب و پیکربندی سرور پردازش\n- تنظیم پارامترهای امنیتی\n- راه‌اندازی ماژول‌های پردازشی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'در حال حاضر سیستم پردازش داده در حالت تعمیر و نگهداری است. به زودی سرویس‌دهی از سر گرفته خواهد شد.',
details: 'برای فعال‌سازی مجدد سیستم، نیاز به انجام مراحل زیر است:\n- بررسی اتصال به سرور مرکزی\n- به‌روزرسانی ماژول‌های پردازشی\n- تنظیم مجدد پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
},
{
message: 'با عرض پوزش، سخت‌افزار پردازش داده در حال حاضر غیرفعال است. تیم فنی در حال بررسی و رفع مشکل است.',
details: 'برای فعال‌سازی سیستم، نیاز به انجام مراحل زیر است:\n- تأیید اتصال به سرور پردازش\n- راه‌اندازی ماژول‌های پردازشی\n- تنظیم پارامترهای امنیتی\nلطفاً با پشتیبانی فنی تماس بگیرید.'
}
],
welcomePatterns: [
{
welcome: 'سلام! 👋',
thanks: 'از اینکه از هوش مصنوعی حسابیکس استفاده می‌کنید، بسیار خوشحالم! من یک هوش مصنوعی مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و نیازی به سرویس‌های خارجی ندارم.'
},
{
welcome: 'درود! 🌟',
thanks: 'به هوش مصنوعی حسابیکس خوش آمدید! من یک دستیار هوشمند مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و آماده خدمت‌رسانی به شما هستم.'
},
{
welcome: 'سلام و وقت بخیر! ✨',
thanks: 'خوشحالم که از هوش مصنوعی حسابیکس استفاده می‌کنید. من یک دستیار هوشمند مستقل هستم که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم و می‌توانم در زمینه‌های مختلف به شما کمک کنم.'
},
{
welcome: 'به حسابیکس خوش آمدید! 🚀',
thanks: 'من هوش مصنوعی مستقل حسابیکس هستم که به صورت کامل در سرورهای داخلی میزبانی می‌شوم. خوشحالم که می‌توانم به شما در استفاده از این نرم‌افزار کمک کنم.'
},
{
welcome: 'سلام! من دستیار هوشمند شما هستم 🤖',
thanks: 'به عنوان یک هوش مصنوعی مستقل که به صورت کامل در سرورهای داخلی حسابیکس میزبانی می‌شوم، آماده‌ام تا در هر زمینه‌ای که نیاز دارید به شما کمک کنم.'
}
],
selectedPattern: null,
capabilities: 'من می‌توانم به شما در موارد زیر کمک کنم:',
capabilitiesList: [
'ساخت گزارش‌های سفارشی با استفاده از هوش مصنوعی داخلی',
'ایجاد ماژول‌های جدید بدون نیاز به کدنویسی',
'پاسخ به سؤالات شما درباره نرم‌افزار با استفاده از دانش داخلی',
'راهنمایی در استفاده از امکانات مختلف با هوش مصنوعی مستقل',
'تجزیه و تحلیل داده‌های مالی با استفاده از الگوریتم‌های داخلی',
'پیش‌بینی روندهای مالی با استفاده از هوش مصنوعی اختصاصی'
],
prompt: 'لطفاً سؤال یا درخواست خود را در باکس زیر بنویسید. من با استفاده از هوش مصنوعی مستقل خود، به شما کمک خواهم کرد.',
displayWelcome: '',
displayThanks: '',
displayCapabilities: '',
displayCapabilitiesList: [],
displayPrompt: ''
}
},
async mounted() {
this.selectRandomPattern()
await this.startTypingAnimation()
},
watch: {
userMessages: {
handler() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
deep: true
},
displayWelcome() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayThanks() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayCapabilities() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
displayCapabilitiesList: {
handler() {
this.$nextTick(() => {
this.scrollToBottom()
})
},
deep: true
},
displayPrompt() {
this.$nextTick(() => {
this.scrollToBottom()
})
}
},
methods: {
selectRandomPattern() {
const randomIndex = Math.floor(Math.random() * this.welcomePatterns.length)
this.selectedPattern = this.welcomePatterns[randomIndex]
this.welcome = this.selectedPattern.welcome
this.thanks = this.selectedPattern.thanks
},
async startTypingAnimation() {
// تایپ پیام خوشآمدگویی
await this.typeText(this.welcome, (text) => {
this.displayWelcome = text
}, 15)
await this.delay(100)
// تایپ پیام تشکر
await this.typeText(this.thanks, (text) => {
this.displayThanks = text
}, 15)
await this.delay(100)
// تایپ معرفی قابلیتها
await this.typeText(this.capabilities, (text) => {
this.displayCapabilities = text
}, 15)
await this.delay(100)
// تایپ لیست قابلیتها
for (const capability of this.capabilitiesList) {
this.displayCapabilitiesList.push('')
await this.typeText(capability, (text) => {
this.displayCapabilitiesList[this.displayCapabilitiesList.length - 1] = text
}, 15)
await this.delay(50)
}
// تایپ پیام نهایی
await this.typeText(this.prompt, (text) => {
this.displayPrompt = text
}, 15)
this.isTyping = false
},
async typeText(text, callback, speed = 50) {
let currentText = ''
for (let i = 0; i < text.length; i++) {
currentText += text[i]
callback(currentText)
await this.delay(speed)
}
},
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
},
async sendMessage() {
if (!this.userMessage.trim()) return
const message = this.userMessage.trim()
this.userMessages.push(message)
this.userMessage = ''
this.isLoading = true
// انتخاب پاسخ رندوم
const randomResponse = this.aiResponses[Math.floor(Math.random() * this.aiResponses.length)]
// نمایش پاسخ اصلی
await this.delay(1000)
this.userMessages.push({
text: randomResponse.message,
isAI: true
})
// نمایش جزئیات
await this.delay(500)
this.userMessages.push({
text: randomResponse.details,
isAI: true,
isDetails: true
})
this.isLoading = false
this.$nextTick(() => {
this.scrollToBottom()
})
},
scrollToBottom() {
const container = this.$refs.messagesContainer
if (container) {
container.scrollTo({
top: container.scrollHeight,
behavior: 'smooth'
})
}
}
}
}
</script>
<style scoped>
.fixed-toolbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2000;
background-color: #ffffff !important;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important;
.page-container {
height: 100vh;
display: flex;
flex-direction: column;
}
.wizard-content {
position: relative;
z-index: 1;
.content-container {
flex: 1;
height: 100vh;
}
.main-card {
background-color: #ffffff !important;
border-radius: 16px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05) !important;
.chat-container {
height: 100%;
background-color: #f5f5f5;
}
.feature-card {
background-color: #f8f9fa !important;
.chat-box {
height: 100%;
display: flex;
flex-direction: column;
}
.messages-container {
flex: 1;
overflow-y: auto;
padding: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}
.message {
display: flex;
align-items: flex-start;
max-width: 80%;
}
.ai-message {
align-self: flex-start;
}
.user-message {
align-self: flex-end;
flex-direction: row-reverse;
}
.message-content {
background-color: white;
padding: 12px 16px;
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.ai-message .message-content {
background-color: #e3f2fd;
border-bottom-right-radius: 4px;
}
.user-message .message-content {
background-color: #1a237e;
color: white;
border-bottom-left-radius: 4px;
}
.message-text {
font-size: 1rem;
line-height: 1.5;
}
.input-container {
padding: 16px;
background-color: white;
border-top: 1px solid rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 12px;
}
.message-input {
background-color: #f5f5f5 !important;
border-radius: 24px;
padding: 8px 16px !important;
}
.message-input :deep(.v-field__input) {
padding: 8px !important;
font-size: 1rem;
color: #424242;
}
.send-button {
transition: all 0.3s ease;
}
.feature-card:hover {
background-color: #ffffff !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1) !important;
transform: translateY(-4px);
}
.v-icon {
transition: transform 0.3s ease;
}
.feature-card:hover .v-icon {
.send-button:hover {
transform: scale(1.1);
}
.v-card-title {
color: #2c3e50 !important;
.typing-indicator {
display: flex;
gap: 4px;
align-items: center;
height: 24px;
}
.v-card-text {
color: #34495e !important;
.typing-indicator span {
width: 8px;
height: 8px;
background-color: #1a237e;
border-radius: 50%;
animation: typing 1s infinite ease-in-out;
}
.text-body-2 {
color: #7f8c8d !important;
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes typing {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-4px); }
}
.details-message {
background-color: #f5f5f5 !important;
border: 1px solid #e0e0e0;
font-size: 0.9rem;
color: #616161;
}
.details-message .message-text {
white-space: pre-line;
}
.messages-container::-webkit-scrollbar {
width: 6px;
}
.messages-container::-webkit-scrollbar-track {
background: transparent;
}
.messages-container::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
border-radius: 3px;
}
.messages-container::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.3);
}
</style>