progress in income and pays and some bug fix

This commit is contained in:
Hesabix 2025-05-22 01:15:44 +00:00
parent cdbe6e3ae9
commit 72e2065544
34 changed files with 7495 additions and 1710 deletions

View file

@ -543,6 +543,7 @@ class BusinessController extends AbstractController
'plugAccproPresell' => true,
'plugRepservice' => true,
'plugHrmDocs' => true,
'plugGhestaManager' => true,
];
} elseif ($perm) {
$result = [
@ -585,6 +586,7 @@ class BusinessController extends AbstractController
'plugRepservice' => $perm->isPlugRepservice(),
'plugAccproPresell' => $perm->isPlugAccproPresell(),
'plugHrmDocs' => $perm->isPlugHrmDocs(),
'plugGhestaManager' => $perm->isPlugGhestaManager(),
];
}
return $this->json($result);
@ -653,6 +655,7 @@ class BusinessController extends AbstractController
$perm->setPlugAccproAccounting($params['plugAccproAccounting']);
$perm->setPlugRepservice($params['plugRepservice']);
$perm->setPlugHrmDocs($params['plugHrmDocs']);
$perm->setPlugGhestaManager($params['plugGhestaManager']);
$entityManager->persist($perm);
$entityManager->flush();
$log->insert('تنظیمات پایه', 'ویرایش دسترسی‌های کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);

View file

@ -0,0 +1,230 @@
<?php
namespace App\Controller\Componenets;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Service\Access;
use App\Service\Explore;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class DocsearchController extends AbstractController
{
#[Route('/api/componenets/doc/search', name: 'app_componenets_doc_search', methods: ['POST'])]
public function search(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
// بررسی دسترسی کاربر
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// ایجاد کوئری بیس
$qb = $entityManager->createQueryBuilder();
$qb->select('d')
->addSelect('p.name as personName')
->addSelect('p.nikename as personNikename')
->from(HesabdariDoc::class, 'd')
->leftJoin('d.hesabdariRows', 'r')
->leftJoin('r.person', 'p')
->where('d.bid = :bid')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اعمال فیلترهای جستجو
if (isset($params['search']) && !empty($params['search'])) {
$search = $params['search'];
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->like('d.code', ':search'),
$qb->expr()->like('d.des', ':search'),
$qb->expr()->like('p.name', ':search'),
$qb->expr()->like('p.nikename', ':search')
)
)
->setParameter('search', '%' . $search . '%');
}
// فیلتر بر اساس نوع سند
if (isset($params['docType']) && !empty($params['docType'])) {
$qb->andWhere('d.type = :type')
->setParameter('type', $params['docType']);
}
// فیلتر بر اساس تاریخ
if (isset($params['dateFrom']) && !empty($params['dateFrom'])) {
$qb->andWhere('d.date >= :dateFrom')
->setParameter('dateFrom', $params['dateFrom']);
}
if (isset($params['dateTo']) && !empty($params['dateTo'])) {
$qb->andWhere('d.date <= :dateTo')
->setParameter('dateTo', $params['dateTo']);
}
// مرتب‌سازی
$qb->orderBy('d.code', 'DESC');
// صفحه‌بندی
$page = isset($params['page']) ? (int)$params['page'] : 1;
$itemsPerPage = isset($params['itemsPerPage']) ? (int)$params['itemsPerPage'] : 10;
$qb->setFirstResult(($page - 1) * $itemsPerPage)
->setMaxResults($itemsPerPage);
// اجرای کوئری
$results = $qb->getQuery()->getResult();
// آماده‌سازی نتایج
$formattedResults = [];
foreach ($results as $result) {
$doc = $result[0]; // اولین عنصر آرایه، شیء HesabdariDoc است
$temp = [
'id' => $doc->getId(),
'code' => $doc->getCode(),
'date' => $doc->getDate(),
'dateSubmit' => $doc->getDateSubmit(),
'type' => $doc->getType(),
'des' => $doc->getDes(),
'amount' => $doc->getAmount(),
'submitter' => $doc->getSubmitter()->getFullName(),
'status' => 'بدون تراکنش دریافت/پرداخت',
'personName' => $result['personName'] ?? null,
'personNikename' => $result['personNikename'] ?? null,
'relatedDocs' => []
];
// محاسبه وضعیت تسویه و اضافه کردن اسناد مرتبط
$pays = 0;
foreach ($doc->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
$temp['relatedDocs'][] = [
'id' => $relatedDoc->getId(),
'code' => $relatedDoc->getCode(),
'date' => $relatedDoc->getDate(),
'amount' => $relatedDoc->getAmount(),
'type' => $relatedDoc->getType()
];
}
if ($pays > 0) {
if ($doc->getAmount() <= $pays) {
$temp['status'] = 'تسویه شده';
} else {
$temp['status'] = 'تسویه نشده';
}
}
// اضافه کردن اطلاعات مشتری یا کالا
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($doc, 'person');
if ($mainRow && $mainRow->getPerson()) {
$temp['person'] = Explore::ExplorePerson($mainRow->getPerson());
}
// اضافه کردن برچسب فاکتور
if ($doc->getInvoiceLabel()) {
$temp['label'] = [
'code' => $doc->getInvoiceLabel()->getCode(),
'label' => $doc->getInvoiceLabel()->getLabel()
];
}
$formattedResults[] = $temp;
}
// محاسبه تعداد کل نتایج
$countQb = clone $qb;
$countQb->select('COUNT(d.id)');
$totalItems = $countQb->getQuery()->getSingleScalarResult();
return $this->json([
'items' => $formattedResults,
'total' => $totalItems,
'page' => $page,
'itemsPerPage' => $itemsPerPage
]);
}
#[Route('/api/componenets/doc/get/{code}', name: 'app_componenets_doc_get', methods: ['GET'])]
public function getDoc(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
{
// بررسی دسترسی کاربر
$acc = $access->hasRole('join');
if (!$acc) {
throw $this->createAccessDeniedException();
}
// دریافت سند
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'code' => $code,
'bid' => $acc['bid'],
'year' => $acc['year'],
'money' => $acc['money']
]);
if (!$doc) {
throw $this->createNotFoundException('سند مورد نظر یافت نشد');
}
// آماده‌سازی اطلاعات سند
$result = [
'id' => $doc->getId(),
'code' => $doc->getCode(),
'date' => $doc->getDate(),
'dateSubmit' => $doc->getDateSubmit(),
'type' => $doc->getType(),
'des' => $doc->getDes(),
'amount' => $doc->getAmount(),
'submitter' => $doc->getSubmitter()->getFullName(),
'status' => 'بدون تراکنش دریافت/پرداخت',
'relatedDocs' => []
];
// محاسبه وضعیت تسویه و اضافه کردن اسناد مرتبط
$pays = 0;
foreach ($doc->getRelatedDocs() as $relatedDoc) {
$pays += $relatedDoc->getAmount();
$result['relatedDocs'][] = [
'id' => $relatedDoc->getId(),
'code' => $relatedDoc->getCode(),
'date' => $relatedDoc->getDate(),
'amount' => $relatedDoc->getAmount(),
'type' => $relatedDoc->getType()
];
}
if ($pays > 0) {
if ($doc->getAmount() <= $pays) {
$result['status'] = 'تسویه شده';
} else {
$result['status'] = 'تسویه نشده';
}
}
// اضافه کردن اطلاعات مشتری یا کالا
$mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($doc, 'person');
if ($mainRow && $mainRow->getPerson()) {
$result['person'] = Explore::ExplorePerson($mainRow->getPerson());
}
// اضافه کردن برچسب فاکتور
if ($doc->getInvoiceLabel()) {
$result['label'] = [
'code' => $doc->getInvoiceLabel()->getCode(),
'label' => $doc->getInvoiceLabel()->getLabel()
];
}
return $this->json($result);
}
}

View file

@ -9,7 +9,18 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use App\Service\Provider;
use App\Entity\HesabdariDoc;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Salary;
use App\Entity\Person;
use App\Service\Log;
use Doctrine\Common\Collections\ArrayCollection;
use App\Repository\HesabdariTableRepository;
class IncomeController extends AbstractController
{
@ -160,4 +171,476 @@ class IncomeController extends AbstractController
'series' => $series,
]);
}
#[Route('/api/income/list/search', name: 'app_income_list_search', methods: ['POST'])]
public function searchIncomeList(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
HesabdariTableRepository $hesabdariTableRepository,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('income');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$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'] ?? 'income';
// 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')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('year', $acc['year'])
->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('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',
't.name LIKE :search',
't.code LIKE :search'
)
)
->setParameter('search', "%{$searchValue}%");
}
// Income center filter
if (isset($filters['account']) && $filters['account'] !== '66') {
$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']) {
case 'today':
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
break;
case 'week':
$weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days'));
$queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today')
->setParameter('weekStart', $weekStart)
->setParameter('today', $today);
break;
case 'month':
$monthStart = $jdate->jdate('Y/m/01', time());
$queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today')
->setParameter('monthStart', $monthStart)
->setParameter('today', $today);
break;
case 'custom':
if (isset($filters['date']) && isset($filters['date']['from']) && isset($filters['date']['to'])) {
$queryBuilder->andWhere('d.date BETWEEN :dateFrom AND :dateTo')
->setParameter('dateFrom', $filters['date']['from'])
->setParameter('dateTo', $filters['date']['to']);
}
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);
$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'],
];
// Get income center details
$incomeDetails = $entityManager->createQueryBuilder()
->select('t.name as center_name, t.code as center_code, r.bs as amount, r.des as des')
->from('App\Entity\HesabdariRow', 'r')
->join('r.ref', 't')
->where('r.doc = :docId')
->andWhere('r.bs != 0')
->setParameter('docId', $doc['id'])
->getQuery()
->getResult();
$item['incomeCenters'] = array_map(function ($detail) {
return [
'name' => $detail['center_name'],
'code' => $detail['center_code'],
'amount' => (int) $detail['amount'],
'des' => $detail['des'],
];
}, $incomeDetails);
// Get related person info
$personInfo = $entityManager->createQueryBuilder()
->select('p.id, p.nikename, p.code')
->from('App\Entity\HesabdariRow', 'r')
->join('r.person', 'p')
->where('r.doc = :docId')
->andWhere('r.person IS NOT NULL')
->setParameter('docId', $doc['id'])
->setMaxResults(1)
->getQuery()
->getOneOrNullResult();
$item['person'] = $personInfo ? [
'id' => $personInfo['id'],
'nikename' => $personInfo['nikename'],
'code' => $personInfo['code'],
] : null;
$dataTemp[] = $item;
}
return $this->json([
'items' => $dataTemp,
'total' => (int) $totalItems,
'page' => $page,
'limit' => $limit,
]);
}
#[Route('/api/incomes/list/print', name: 'app_incomes_list_print')]
public function app_incomes_list_print(
Provider $provider,
Request $request,
Access $access,
EntityManagerInterface $entityManager
): JsonResponse {
$acc = $access->hasRole('income');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
} else {
$items = [];
foreach ($params['items'] as $param) {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
}
}
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/incomes.html.twig', [
'page_title' => 'فهرست درآمدها',
'bid' => $acc['bid'],
'items' => $items
])
);
return $this->json(['id' => $pid]);
}
#[Route('/api/incomes/list/excel', name: 'app_incomes_list_excel')]
public function app_incomes_list_excel(
Provider $provider,
Request $request,
Access $access,
EntityManagerInterface $entityManager
): BinaryFileResponse {
$acc = $access->hasRole('income');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
// دریافت آیتم‌های انتخاب شده یا همه آیتم‌ها
if (!isset($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
} else {
$items = [];
foreach ($params['items'] as $param) {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'type' => 'income',
'year' => $acc['year'],
'money' => $acc['money']
]);
if ($doc) {
$items[] = $doc;
}
}
}
// ایجاد فایل اکسل
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setRightToLeft(true);
// تنظیم هدرها
$sheet->setCellValue('A1', 'ردیف')
->setCellValue('B1', 'شماره سند')
->setCellValue('C1', 'تاریخ')
->setCellValue('D1', 'شرح')
->setCellValue('E1', 'مرکز درآمد')
->setCellValue('F1', 'مرکز دریافت')
->setCellValue('G1', 'مبلغ (ریال)');
// پر کردن داده‌ها
$rowNumber = 2;
foreach ($items as $index => $item) {
// محاسبه مراکز درآمد
$incomeCenters = [];
foreach ($item->getHesabdariRows() as $row) {
if ($row->getRef()) {
$incomeCenters[] = $row->getRef()->getName();
}
}
$incomeCenterNames = implode('، ', array_unique($incomeCenters));
// محاسبه مرکز دریافت
$receiveCenter = null;
foreach ($item->getHesabdariRows() as $row) {
if (!$receiveCenter) {
if ($row->getBank()) {
$receiveCenter = $row->getBank()->getName();
} elseif ($row->getCashdesk()) {
$receiveCenter = $row->getCashdesk()->getName();
} elseif ($row->getSalary()) {
$receiveCenter = $row->getSalary()->getName();
} elseif ($row->getPerson()) {
$receiveCenter = $row->getPerson()->getNikename();
}
}
}
$sheet->setCellValue('A' . $rowNumber, $index + 1)
->setCellValue('B' . $rowNumber, $item->getCode())
->setCellValue('C' . $rowNumber, $item->getDate())
->setCellValue('D' . $rowNumber, $item->getDes())
->setCellValue('E' . $rowNumber, $incomeCenterNames)
->setCellValue('F' . $rowNumber, $receiveCenter)
->setCellValue('G' . $rowNumber, number_format($item->getAmount()));
$rowNumber++;
}
// ذخیره فایل اکسل
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$filePath = __DIR__ . '/../../var/' . uniqid() . '.xlsx';
$writer->save($filePath);
return new BinaryFileResponse($filePath);
}
#[Route('/api/income/doc/insert', name: 'app_income_doc_insert', methods: ['POST'])]
public function insertIncomeDoc(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Provider $provider,
Log $log,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('income');
if (!$acc) {
throw $this->createAccessDeniedException();
}
$params = json_decode($request->getContent(), true) ?? [];
// بررسی پارامترهای ضروری
if (!isset($params['rows']) || count($params['rows']) < 2) {
return $this->json(['result' => 0, 'message' => 'حداقل دو ردیف برای سند درآمد الزامی است'], 400);
}
if (!isset($params['date']) || !isset($params['des'])) {
return $this->json(['result' => 0, 'message' => 'تاریخ و شرح سند الزامی است'], 400);
}
// تنظیم نوع سند به income
$params['type'] = 'income';
// بررسی وجود سند برای ویرایش
if (isset($params['update']) && $params['update'] != '') {
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'bid' => $acc['bid'],
'year' => $acc['year'],
'code' => $params['update'],
'money' => $acc['money']
]);
if (!$doc) {
return $this->json(['result' => 0, 'message' => 'سند مورد نظر یافت نشد'], 404);
}
}
// ایجاد سند جدید
$doc = new HesabdariDoc();
$doc->setBid($acc['bid']);
$doc->setYear($acc['year']);
$doc->setDes($params['des']);
$doc->setDateSubmit(time());
$doc->setType('income');
$doc->setDate($params['date']);
$doc->setSubmitter($this->getUser());
$doc->setMoney($acc['money']);
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$entityManager->persist($doc);
$entityManager->flush();
// پردازش ردیف‌های سند
$amount = 0;
foreach ($params['rows'] as $row) {
$row['bs'] = str_replace(',', '', $row['bs']);
$row['bd'] = str_replace(',', '', $row['bd']);
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setBid($acc['bid']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setDoc($doc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
// تنظیم مرکز درآمد
$ref = $entityManager->getRepository(HesabdariTable::class)->findOneBy([
'code' => $row['table']
]);
$hesabdariRow->setRef($ref);
// تنظیم مرکز دریافت (بانک، صندوق، تنخواه، شخص)
if ($row['type'] == 'bank') {
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$bank) {
return $this->json(['result' => 0, 'message' => 'حساب بانکی مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setBank($bank);
} elseif ($row['type'] == 'cashdesk') {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['id']);
if (!$cashdesk) {
return $this->json(['result' => 0, 'message' => 'صندوق مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setCashdesk($cashdesk);
} elseif ($row['type'] == 'salary') {
$salary = $entityManager->getRepository(Salary::class)->find($row['id']);
if (!$salary) {
return $this->json(['result' => 0, 'message' => 'تنخواه مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setSalary($salary);
} elseif ($row['type'] == 'person') {
$person = $entityManager->getRepository(Person::class)->findOneBy([
'id' => $row['id'],
'bid' => $acc['bid']
]);
if (!$person) {
return $this->json(['result' => 0, 'message' => 'شخص مورد نظر یافت نشد'], 404);
}
$hesabdariRow->setPerson($person);
}
if (isset($row['des'])) {
$hesabdariRow->setDes($row['des']);
}
$entityManager->persist($hesabdariRow);
$amount += $row['bs'];
}
$doc->setAmount($amount);
$entityManager->persist($doc);
$entityManager->flush();
$log->insert(
'حسابداری',
'سند درآمد شماره ' . $doc->getCode() . ' ثبت شد.',
$this->getUser(),
$acc['bid'],
$doc
);
return $this->json([
'result' => 1,
'doc' => $provider->Entity2Array($doc, 0)
]);
}
}

View file

@ -1280,119 +1280,267 @@ class PersonsController extends AbstractController
$acc = $access->hasRole('getpay');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('items', $params)) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'person_send',
'year' => $acc['year'],
'money' => $acc['money']
]);
} else {
$items = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'type' => 'person_send',
'year' => $acc['year'],
'money' => $acc['money']
]);
if ($prs)
$items[] = $prs;
}
$queryBuilder = $entityManager->getRepository(HesabdariDoc::class)->createQueryBuilder('d')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'person_send')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money']);
// اگر آیتم‌های خاصی درخواست شده‌اند
if (array_key_exists('items', $params)) {
$ids = array_map(function($item) { return $item['id']; }, $params['items']);
$queryBuilder->andWhere('d.id IN (:ids)')
->setParameter('ids', $ids);
}
// دریافت تعداد کل رکوردها
$totalItems = $queryBuilder->select('COUNT(d.id)')
->getQuery()
->getSingleScalarResult();
// اگر درخواست با صفحه‌بندی است
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) {
$personNames = [];
foreach ($item->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$personNames[] = $row->getPerson()->getNikename();
}
}
$item->personNames = implode('، ', array_unique($personNames));
}
$pid = $provider->createPrint(
$acc['bid'],
$this->getUser(),
$this->renderView('pdf/persons_receive.html.twig', [
$this->renderView('pdf/persons_send.html.twig', [
'page_title' => 'لیست پرداخت‌ها',
'bid' => $acc['bid'],
'items' => $items
'items' => $items,
'totalItems' => $totalItems,
'currentPage' => $params['page'] ?? 1,
'totalPages' => array_key_exists('limit', $params) ? ceil($totalItems / $params['limit']) : 1
])
);
return $this->json(['id' => $pid]);
return $this->json([
'id' => $pid,
'totalItems' => $totalItems,
'currentPage' => $params['page'] ?? 1,
'totalPages' => array_key_exists('limit', $params) ? ceil($totalItems / $params['limit']) : 1
]);
}
#[Route('/api/person/send/list/search', name: 'app_persons_send_list_search')]
public function app_persons_send_list_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
#[Route('/api/person/send/list/search', name: 'app_persons_send_list_search', methods: ['POST'])]
public function app_persons_send_list_search(
Request $request,
Access $access,
EntityManagerInterface $entityManager,
Jdate $jdate
): JsonResponse {
$acc = $access->hasRole('getpay');
if (!$acc)
if (!$acc) {
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy(
[
'bid' => $acc['bid'],
'type' => 'person_send',
'year' => $acc['year'],
'money' => $acc['money']
],
['id' => 'DESC']
);
$res = [];
foreach ($items as $item) {
$temp = [
'id' => $item->getId(),
'date' => $item->getDate(),
'code' => $item->getCode(),
'des' => $item->getDes(),
'amount' => $item->getAmount()
];
$persons = [];
foreach ($item->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$persons[] = Explore::ExplorePerson($row->getPerson());
// دریافت پارامترها
$params = json_decode($request->getContent(), true) ?? [];
$page = (int) ($params['page'] ?? 1);
$itemsPerPage = (int) ($params['itemsPerPage'] ?? 10);
$search = $params['search'] ?? '';
$dateFilter = $params['dateFilter'] ?? 'all';
// کوئری پایه برای اسناد
$queryBuilder = $entityManager->getRepository(HesabdariDoc::class)
->createQueryBuilder('d')
->select('DISTINCT d.id, d.date, d.code, d.des, d.amount')
->leftJoin('d.hesabdariRows', 'hr')
->leftJoin('hr.person', 'p')
->where('d.bid = :bid')
->andWhere('d.type = :type')
->andWhere('d.year = :year')
->andWhere('d.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('type', 'person_send')
->setParameter('year', $acc['year'])
->setParameter('money', $acc['money'])
->orderBy('d.id', 'DESC');
// جست‌وجو
if (!empty($search)) {
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(
'd.code LIKE :search',
'd.des LIKE :search',
'p.nikename LIKE :search'
)
)->setParameter('search', "%$search%");
}
// فیلتر تاریخ
$today = $jdate->GetTodayDate();
switch ($dateFilter) {
case 'today':
$queryBuilder->andWhere('d.date = :today')
->setParameter('today', $today);
break;
case 'thisWeek':
$dayOfWeek = (int) $jdate->jdate('w', time());
$startOfWeek = $jdate->shamsiDate(0, 0, -$dayOfWeek);
$endOfWeek = $jdate->shamsiDate(0, 0, 6 - $dayOfWeek);
$queryBuilder->andWhere('d.date BETWEEN :start AND :end')
->setParameter('start', $startOfWeek)
->setParameter('end', $endOfWeek);
break;
case 'thisMonth':
$currentYear = (int) $jdate->jdate('Y', time());
$currentMonth = (int) $jdate->jdate('n', time());
$daysInMonth = (int) $jdate->jdate('t', time());
$startOfMonth = sprintf('%d/%02d/01', $currentYear, $currentMonth);
$endOfMonth = sprintf('%d/%02d/%02d', $currentYear, $currentMonth, $daysInMonth);
$queryBuilder->andWhere('d.date BETWEEN :start AND :end')
->setParameter('start', $startOfMonth)
->setParameter('end', $endOfMonth);
break;
case 'all':
default:
break;
}
// محاسبه تعداد کل
$totalQuery = (clone $queryBuilder)
->select('COUNT(DISTINCT d.id) as total')
->getQuery()
->getSingleResult();
$total = (int) $totalQuery['total'];
// گرفتن اسناد با صفحه‌بندی
$docs = $queryBuilder
->setFirstResult(($page - 1) * $itemsPerPage)
->setMaxResults($itemsPerPage)
->getQuery()
->getArrayResult();
// گرفتن اشخاص مرتبط
$docIds = array_column($docs, 'id');
$persons = [];
if (!empty($docIds)) {
$personQuery = $entityManager->createQueryBuilder()
->select('IDENTITY(hr.doc) as doc_id, p.code as person_code, p.nikename as person_nikename')
->from('App\Entity\HesabdariRow', 'hr')
->leftJoin('hr.person', 'p')
->where('hr.doc IN (:docIds)')
->setParameter('docIds', $docIds)
->getQuery()
->getArrayResult();
foreach ($personQuery as $row) {
if (!empty($row['person_code'])) {
$persons[$row['doc_id']][] = [
'code' => $row['person_code'],
'nikename' => $row['person_nikename'],
];
}
}
$temp['persons'] = $persons;
$res[] = $temp;
}
// ساختاردهی خروجی
$items = [];
foreach ($docs as $doc) {
$items[] = [
'id' => $doc['id'],
'date' => $doc['date'],
'code' => $doc['code'],
'des' => $doc['des'],
'amount' => $doc['amount'],
'persons' => $persons[$doc['id']] ?? [],
];
}
return $this->json($res);
return $this->json([
'items' => $items,
'total' => $total,
]);
}
/**
* @throws Exception
*/
#[Route('/api/person/send/list/excel', name: 'app_persons_send_list_excel')]
public function app_persons_send_list_excel(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): BinaryFileResponse|JsonResponse|StreamedResponse
{
#[Route('/api/person/send/list/excel', name: 'app_persons_send_list_excel', methods: ['POST'])]
public function app_persons_send_list_excel(
Provider $provider,
Request $request,
Access $access,
Log $log,
EntityManagerInterface $entityManager
): BinaryFileResponse {
$acc = $access->hasRole('getpay');
if (!$acc)
if (!$acc) {
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
if (!array_key_exists('items', $params)) {
$params = json_decode($request->getContent(), true) ?? [];
if (!array_key_exists('items', $params) || empty($params['items'])) {
$items = $entityManager->getRepository(HesabdariDoc::class)->findBy([
'bid' => $acc['bid'],
'type' => 'person_send',
'year' => $acc['year'],
'money' => $acc['money']
'money' => $acc['money'],
]);
} else {
$items = [];
foreach ($params['items'] as $param) {
$prs = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
if (!is_array($param) || !isset($param['id'])) {
throw new \InvalidArgumentException('Invalid item format in request');
}
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $param['id'],
'bid' => $acc['bid'],
'type' => 'person_send',
'year' => $acc['year'],
'money' => $acc['money']
'money' => $acc['money'],
]);
if ($prs)
$items[] = $prs;
if ($doc) {
// اضافه کردن اطلاعات اشخاص
$personNames = [];
foreach ($doc->getHesabdariRows() as $row) {
if ($row->getPerson()) {
$personNames[] = $row->getPerson()->getNikename();
}
}
$doc->personNames = implode('، ', array_unique($personNames));
$items[] = $doc;
}
}
}
return new BinaryFileResponse($provider->createExcell($items, ['type', 'dateSubmit']));
}

View file

@ -0,0 +1,419 @@
<?php
namespace App\Controller\Plugins;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\PlugGhestaDoc;
use App\Entity\PlugGhestaItem;
use App\Entity\HesabdariDoc;
use App\Entity\Person;
use App\Service\Access;
use App\Service\Provider;
class PlugGhestaController extends AbstractController
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
#[Route('/api/plugins/ghesta/invoices', name: 'plugin_ghesta_invoices', methods: ['GET'])]
public function plugin_ghesta_invoices(EntityManagerInterface $entityManager, Access $access) : JsonResponse
{
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$invoices = $entityManager->getRepository(PlugGhestaDoc::class)->findBy(['bid' => $acc['bid']]);
$data = [];
foreach($invoices as $invoice){
$data[] = [
'id' => $invoice->getId(),
'code' => $invoice->getMainDoc() ? $invoice->getMainDoc()->getCode() : null,
'dateSubmit' => $invoice->getDateSubmit(),
'count' => $invoice->getCount(),
'profitPercent' => $invoice->getProfitPercent(),
'profitAmount' => $invoice->getProfitAmount(),
'profitType' => $invoice->getProfitType(),
'daysPay' => $invoice->getDaysPay(),
'person' => [
'id' => $invoice->getPerson()->getId(),
'name' => $invoice->getPerson()->getName(),
'nikename' => $invoice->getPerson()->getNikename()
]
];
}
return $this->json($data);
}
#[Route('/api/plugins/ghesta/invoices/{id}', name: 'plugin_ghesta_invoice', methods: ['GET'])]
public function plugin_ghesta_invoice(EntityManagerInterface $entityManager, Access $access, $id) : JsonResponse
{
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$invoice = $entityManager->getRepository(PlugGhestaDoc::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid']
]);
if(!$invoice)
throw $this->createNotFoundException();
$data = [
'id' => $invoice->getId(),
'code' => $invoice->getMainDoc() ? $invoice->getMainDoc()->getCode() : null,
'dateSubmit' => $invoice->getDateSubmit(),
'count' => $invoice->getCount(),
'profitPercent' => $invoice->getProfitPercent(),
'profitAmount' => $invoice->getProfitAmount(),
'profitType' => $invoice->getProfitType(),
'daysPay' => $invoice->getDaysPay(),
'person' => [
'id' => $invoice->getPerson()->getId(),
'name' => $invoice->getPerson()->getName(),
'nikename' => $invoice->getPerson()->getNikename()
],
'items' => []
];
foreach($invoice->getPlugGhestaItems() as $item) {
$data['items'][] = [
'id' => $item->getId(),
'date' => $item->getDate(),
'amount' => $item->getAmount(),
'num' => $item->getNum(),
'hesabdariDoc' => $item->getHesabdariDoc() ? [
'id' => $item->getHesabdariDoc()->getId(),
'code' => $item->getHesabdariDoc()->getCode()
] : null
];
}
return $this->json($data);
}
#[Route('/api/plugins/ghesta/invoices/add', name: 'plugin_ghesta_invoice_add', methods: ['POST'])]
public function plugin_ghesta_invoice_add(Request $request, EntityManagerInterface $entityManager, Access $access, Provider $provider) : JsonResponse
{
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$params = json_decode($request->getContent(), true);
if(!$params)
throw $this->createNotFoundException();
// دریافت سند حسابداری
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $params['hesabdariDocId'],
'bid' => $acc['bid']
]);
if (!$hesabdariDoc) {
throw $this->createNotFoundException('HesabdariDoc not found');
}
// ایجاد سند اقساط
$doc = new PlugGhestaDoc();
$doc->setBid($acc['bid']);
$doc->setSubmitter($this->getUser());
$doc->setDateSubmit(time());
$doc->setCount($params['count']);
$doc->setProfitPercent($params['profitPercent']);
$doc->setProfitAmount($params['profitAmount']);
$doc->setProfitType($params['profitType']);
$doc->setDaysPay(floatval($params['daysPay']));
$doc->setMainDoc($hesabdariDoc);
// دریافت اطلاعات شخص از فاکتور
$person = $entityManager->getRepository(Person::class)->findOneBy([
'id' => $params['personId'],
'bid' => $acc['bid']
]);
if (!$person) {
throw $this->createNotFoundException('Person not found');
}
$doc->setPerson($person);
$entityManager->persist($doc);
$entityManager->flush();
// ایجاد اقساط
foreach($params['items'] as $item) {
$ghestaItem = new PlugGhestaItem();
$ghestaItem->setDoc($doc);
$ghestaItem->setDate($item['date']);
$ghestaItem->setAmount($item['amount']);
$ghestaItem->setNum($item['num']);
if(isset($item['hesabdariDocId'])) {
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
'id' => $item['hesabdariDocId'],
'bid' => $acc['bid']
]);
if($hesabdariDoc) {
$ghestaItem->setHesabdariDoc($hesabdariDoc);
}
}
$entityManager->persist($ghestaItem);
}
$entityManager->flush();
return $this->json(['result' => 1, 'id' => $doc->getId()]);
}
#[Route('/api/plugins/ghesta/invoices/edit/{id}', name: 'plugin_ghesta_invoice_edit', methods: ['POST'])]
public function plugin_ghesta_invoice_edit(Request $request, EntityManagerInterface $entityManager, Access $access, $id) : JsonResponse
{
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(PlugGhestaDoc::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid']
]);
if(!$doc)
throw $this->createNotFoundException();
$params = json_decode($request->getContent(), true);
if(!$params)
throw $this->createNotFoundException();
// به‌روزرسانی اطلاعات سند
$doc->setCount($params['count']);
$doc->setProfitPercent($params['profitPercent']);
$doc->setProfitAmount($params['profitAmount']);
$doc->setProfitType($params['profitType']);
$doc->setDaysPay(floatval($params['daysPay']));
// دریافت اطلاعات شخص از فاکتور
$person = $entityManager->getRepository(Person::class)->find($params['personId']);
if (!$person) {
throw $this->createNotFoundException('Person not found');
}
$doc->setPerson($person);
// حذف اقساط قبلی
foreach($doc->getPlugGhestaItems() as $item) {
$entityManager->remove($item);
}
// ایجاد اقساط جدید
foreach($params['items'] as $item) {
$ghestaItem = new PlugGhestaItem();
$ghestaItem->setDoc($doc);
$ghestaItem->setDate($item['date']);
$ghestaItem->setAmount($item['amount']);
$ghestaItem->setNum($item['num']);
if(isset($item['hesabdariDocId'])) {
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->find($item['hesabdariDocId']);
if($hesabdariDoc) {
$ghestaItem->setHesabdariDoc($hesabdariDoc);
}
}
$entityManager->persist($ghestaItem);
}
$entityManager->flush();
return $this->json(['result' => 1]);
}
#[Route('/api/plugins/ghesta/invoice/{id}', name: 'plugin_ghesta_invoice_delete', methods: ['DELETE'])]
public function plugin_ghesta_invoice_delete(EntityManagerInterface $entityManager, Access $access, $id) : JsonResponse
{
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$doc = $entityManager->getRepository(PlugGhestaDoc::class)->findOneBy([
'id' => $id,
'bid' => $acc['bid']
]);
if(!$doc)
throw $this->createNotFoundException();
// حذف اقساط
foreach($doc->getPlugGhestaItems() as $item) {
$entityManager->remove($item);
}
$entityManager->remove($doc);
$entityManager->flush();
return $this->json(['result' => 1]);
}
#[Route('/api/plugins/ghesta/invoices/search', name: 'plugin_ghesta_invoice_search', methods: ['POST'])]
public function plugin_ghesta_invoice_search(Request $request, Access $access): JsonResponse
{
try {
$acc = $access->hasRole('plugGhestaManager');
if(!$acc)
throw $this->createAccessDeniedException();
$params = json_decode($request->getContent(), true);
$search = $params['search'] ?? '';
$page = (int)($params['page'] ?? 1);
$perPage = (int)($params['perPage'] ?? 10);
$dateFilter = $params['dateFilter'] ?? 'all';
$statusFilter = $params['statusFilter'] ?? 'all';
$sortBy = $params['sortBy'] ?? [];
$qb = $this->entityManager->createQueryBuilder();
$qb->select('d')
->from(PlugGhestaDoc::class, 'd')
->leftJoin('d.person', 'p')
->leftJoin('d.plugGhestaItems', 'i')
->where('d.bid = :bid')
->setParameter('bid', $acc['bid'])
->groupBy('d.id');
// اعمال فیلتر جستجو
if (!empty($search)) {
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()->like('d.id', ':search'),
$qb->expr()->like('p.name', ':search'),
$qb->expr()->like('p.nikename', ':search')
)
)->setParameter('search', '%' . $search . '%');
}
// اعمال فیلتر تاریخ
if ($dateFilter !== 'all') {
$now = new \DateTime();
switch ($dateFilter) {
case 'today':
$qb->andWhere('DATE(d.dateSubmit) = :today')
->setParameter('today', $now->format('Y-m-d'));
break;
case 'week':
$qb->andWhere('d.dateSubmit >= :weekStart')
->setParameter('weekStart', $now->modify('-7 days')->format('Y-m-d'));
break;
case 'month':
$qb->andWhere('d.dateSubmit >= :monthStart')
->setParameter('monthStart', $now->modify('-30 days')->format('Y-m-d'));
break;
}
}
// اعمال فیلتر وضعیت
if ($statusFilter !== 'all') {
switch ($statusFilter) {
case 'paid':
$qb->andWhere('i.hesabdariDoc IS NOT NULL');
break;
case 'unpaid':
$qb->andWhere('i.hesabdariDoc IS NULL');
break;
case 'partial':
$qb->andWhere('EXISTS (SELECT 1 FROM ' . PlugGhestaItem::class . ' i2 WHERE i2.doc = d.id AND i2.hesabdariDoc IS NOT NULL)')
->andWhere('EXISTS (SELECT 1 FROM ' . PlugGhestaItem::class . ' i3 WHERE i3.doc = d.id AND i3.hesabdariDoc IS NULL)');
break;
}
}
// اعمال مرتب‌سازی
if (!empty($sortBy)) {
foreach ($sortBy as $sort) {
$field = $sort['key'];
$direction = $sort['order'] === 'desc' ? 'DESC' : 'ASC';
// تبدیل نام فیلد به نام ستون در دیتابیس
$columnMap = [
'id' => 'd.id',
'code' => 'd.code',
'dateSubmit' => 'd.dateSubmit',
'amount' => 'd.amount',
'profitAmount' => 'd.profitAmount',
'profitPercent' => 'd.profitPercent',
'count' => 'd.count',
'profitType' => 'd.profitType'
];
if (isset($columnMap[$field])) {
$qb->addOrderBy($columnMap[$field], $direction);
}
}
} else {
$qb->orderBy('d.dateSubmit', 'DESC');
}
// محاسبه تعداد کل رکوردها
$countQb = clone $qb;
$countQb->select('COUNT(DISTINCT d.id)');
$total = (int)$countQb->getQuery()->getScalarResult();
// اگر هیچ نتیجه‌ای وجود نداشت، آرایه خالی برگردان
if ($total == 0) {
return $this->json([
'result' => 1,
'items' => [],
'total' => 0
]);
}
// اعمال صفحه‌بندی
$qb->setFirstResult(($page - 1) * $perPage)
->setMaxResults($perPage);
$items = $qb->getQuery()->getResult();
// تبدیل نتایج به آرایه
$result = [];
foreach ($items as $item) {
$firstGhestaDate = null;
$ghestaItems = $item->getPlugGhestaItems();
if (count($ghestaItems) > 0) {
$firstGhestaDate = $ghestaItems[0]->getDate();
}
$result[] = [
'id' => $item->getId(),
'code' => $item->getMainDoc() ? $item->getMainDoc()->getCode() : null,
'firstGhestaDate' => $firstGhestaDate,
'amount' => $item->getProfitAmount(), // مبلغ کل شامل سود
'profitAmount' => $item->getProfitAmount(),
'profitPercent' => $item->getProfitPercent(),
'profitType' => $item->getProfitType(),
'count' => $item->getCount(),
'person' => $item->getPerson() ? [
'id' => $item->getPerson()->getId(),
'name' => $item->getPerson()->getName(),
'nikename' => $item->getPerson()->getNikename()
] : null,
'mainDoc' => $item->getMainDoc() ? [
'id' => $item->getMainDoc()->getId(),
'code' => $item->getMainDoc()->getCode()
] : null
];
}
return $this->json([
'result' => 1,
'items' => $result,
'total' => $total
]);
} catch (\Exception $e) {
return $this->json([
'result' => 0,
'message' => $e->getMessage()
], 500);
}
}
}

View file

@ -291,6 +291,12 @@ class Business
#[ORM\OneToMany(mappedBy: 'bid', targetEntity: AccountingPackageOrder::class, orphanRemoval: true)]
private Collection $accountingPackageOrders;
/**
* @var Collection<int, PlugGhestaDoc>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'bid', orphanRemoval: true)]
private Collection $PlugGhestaDocs;
public function __construct()
{
$this->logs = new ArrayCollection();
@ -332,6 +338,7 @@ class Business
$this->dashboardSettings = new ArrayCollection();
$this->hesabdariTables = new ArrayCollection();
$this->accountingPackageOrders = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
}
public function getId(): ?int
@ -2018,4 +2025,34 @@ class Business
return $this;
}
/**
* @return Collection<int, PlugGhestaDoc>
*/
public function getPlugGhestaDocs(): Collection
{
return $this->PlugGhestaDocs;
}
public function addPlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if (!$this->PlugGhestaDocs->contains($PlugGhestaDoc)) {
$this->PlugGhestaDocs->add($PlugGhestaDoc);
$PlugGhestaDoc->setBid($this);
}
return $this;
}
public function removePlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if ($this->PlugGhestaDocs->removeElement($PlugGhestaDoc)) {
// set the owning side to null (unless already changed)
if ($PlugGhestaDoc->getBid() === $this) {
$PlugGhestaDoc->setBid(null);
}
}
return $this;
}
}

View file

@ -128,6 +128,18 @@ class HesabdariDoc
#[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 2, nullable: true)]
private ?float $discountPercent = null;
/**
* @var Collection<int, PlugGhestaItem>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaItem::class, mappedBy: 'hesabdariDoc')]
private Collection $plugGhestaItems;
/**
* @var Collection<int, PlugGhestaDoc>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'mainDoc', orphanRemoval: true)]
private Collection $plugGhestaDocs;
public function __construct()
{
$this->hesabdariRows = new ArrayCollection();
@ -137,6 +149,8 @@ class HesabdariDoc
$this->logs = new ArrayCollection();
$this->notes = new ArrayCollection();
$this->pairDoc = new ArrayCollection();
$this->plugGhestaItems = new ArrayCollection();
$this->plugGhestaDocs = new ArrayCollection();
}
public function getId(): ?int
@ -612,6 +626,67 @@ class HesabdariDoc
public function setDiscountPercent(?float $discountPercent): static
{
$this->discountPercent = $discountPercent;
return $this;
}
/**
* @return Collection<int, PlugGhestaItem>
*/
public function getPlugGhestaItems(): Collection
{
return $this->plugGhestaItems;
}
public function addPlugGhestaItem(PlugGhestaItem $plugGhestaItem): static
{
if (!$this->plugGhestaItems->contains($plugGhestaItem)) {
$this->plugGhestaItems->add($plugGhestaItem);
$plugGhestaItem->setHesabdariDoc($this);
}
return $this;
}
public function removePlugGhestaItem(PlugGhestaItem $plugGhestaItem): static
{
if ($this->plugGhestaItems->removeElement($plugGhestaItem)) {
// set the owning side to null (unless already changed)
if ($plugGhestaItem->getHesabdariDoc() === $this) {
$plugGhestaItem->setHesabdariDoc(null);
}
}
return $this;
}
/**
* @return Collection<int, PlugGhestaDoc>
*/
public function getPlugGhestaDocs(): Collection
{
return $this->plugGhestaDocs;
}
public function addPlugGhestaDoc(PlugGhestaDoc $plugGhestaDoc): static
{
if (!$this->plugGhestaDocs->contains($plugGhestaDoc)) {
$this->plugGhestaDocs->add($plugGhestaDoc);
$plugGhestaDoc->setMainDoc($this);
}
return $this;
}
public function removePlugGhestaDoc(PlugGhestaDoc $plugGhestaDoc): static
{
if ($this->plugGhestaDocs->removeElement($plugGhestaDoc)) {
// set the owning side to null (unless already changed)
if ($plugGhestaDoc->getMainDoc() === $this) {
$plugGhestaDoc->setMainDoc(null);
}
}
return $this;
}
}

View file

@ -152,6 +152,12 @@ class Person
#[ORM\ManyToOne(inversedBy: 'people')]
private ?PersonPrelabel $prelabel = null;
/**
* @var Collection<int, PlugGhestaDoc>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'person', orphanRemoval: true)]
private Collection $PlugGhestaDocs;
public function __construct()
{
$this->hesabdariRows = new ArrayCollection();
@ -166,6 +172,7 @@ class Person
$this->preInvoiceDocs = new ArrayCollection();
$this->hesabdariDocs = new ArrayCollection();
$this->preinvoiceDocsSalemans = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
}
public function getId(): ?int
@ -862,4 +869,34 @@ class Person
return $this;
}
/**
* @return Collection<int, PlugGhestaDoc>
*/
public function getPlugGhestaDocs(): Collection
{
return $this->PlugGhestaDocs;
}
public function addPlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if (!$this->PlugGhestaDocs->contains($PlugGhestaDoc)) {
$this->PlugGhestaDocs->add($PlugGhestaDoc);
$PlugGhestaDoc->setPerson($this);
}
return $this;
}
public function removePlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if ($this->PlugGhestaDocs->removeElement($PlugGhestaDoc)) {
// set the owning side to null (unless already changed)
if ($PlugGhestaDoc->getPerson() === $this) {
$PlugGhestaDoc->setPerson(null);
}
}
return $this;
}
}

View file

@ -0,0 +1,217 @@
<?php
namespace App\Entity;
use App\Repository\PlugGhestaDocRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PlugGhestaDocRepository::class)]
class PlugGhestaDoc
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
private ?Business $bid = null;
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
private ?User $submitter = null;
#[ORM\Column(length: 25)]
private ?string $dateSubmit = null;
#[ORM\Column(type: Types::BIGINT)]
private ?string $count = null;
#[ORM\Column(type: Types::BIGINT)]
private ?string $profitPercent = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $profitAmount = null;
#[ORM\Column(length: 30, nullable: true)]
private ?string $profitType = null;
#[ORM\Column(type: Types::FLOAT, nullable: true)]
private ?float $daysPay = null;
#[ORM\ManyToOne(inversedBy: 'PlugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
private ?Person $person = null;
/**
* @var Collection<int, PlugGhestaItem>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaItem::class, mappedBy: 'doc', orphanRemoval: true)]
private Collection $plugGhestaItems;
#[ORM\ManyToOne(inversedBy: 'plugGhestaDocs')]
#[ORM\JoinColumn(nullable: false)]
private ?HesabdariDoc $mainDoc = null;
public function __construct()
{
$this->plugGhestaItems = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getBid(): ?Business
{
return $this->bid;
}
public function setBid(?Business $bid): static
{
$this->bid = $bid;
return $this;
}
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 getCount(): ?string
{
return $this->count;
}
public function setCount(string $count): static
{
$this->count = $count;
return $this;
}
public function getProfitPercent(): ?string
{
return $this->profitPercent;
}
public function setProfitPercent(string $profitPercent): static
{
$this->profitPercent = $profitPercent;
return $this;
}
public function getProfitAmount(): ?string
{
return $this->profitAmount;
}
public function setProfitAmount(?string $profitAmount): static
{
$this->profitAmount = $profitAmount;
return $this;
}
public function getProfitType(): ?string
{
return $this->profitType;
}
public function setProfitType(?string $profitType): static
{
$this->profitType = $profitType;
return $this;
}
public function getDaysPay(): ?float
{
return $this->daysPay;
}
public function setDaysPay(?float $daysPay): static
{
$this->daysPay = $daysPay;
return $this;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function setPerson(?Person $person): static
{
$this->person = $person;
return $this;
}
/**
* @return Collection<int, PlugGhestaItem>
*/
public function getPlugGhestaItems(): Collection
{
return $this->plugGhestaItems;
}
public function addPlugGhestaItem(PlugGhestaItem $plugGhestaItem): static
{
if (!$this->plugGhestaItems->contains($plugGhestaItem)) {
$this->plugGhestaItems->add($plugGhestaItem);
$plugGhestaItem->setDoc($this);
}
return $this;
}
public function removePlugGhestaItem(PlugGhestaItem $plugGhestaItem): static
{
if ($this->plugGhestaItems->removeElement($plugGhestaItem)) {
// set the owning side to null (unless already changed)
if ($plugGhestaItem->getDoc() === $this) {
$plugGhestaItem->setDoc(null);
}
}
return $this;
}
public function getMainDoc(): ?HesabdariDoc
{
return $this->mainDoc;
}
public function setMainDoc(?HesabdariDoc $mainDoc): static
{
$this->mainDoc = $mainDoc;
return $this;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace App\Entity;
use App\Repository\PlugGhestaItemRepository;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: PlugGhestaItemRepository::class)]
class PlugGhestaItem
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
#[ORM\JoinColumn(nullable: false)]
private ?PlugGhestaDoc $doc = null;
#[ORM\Column(length: 25)]
private ?string $date = null;
#[ORM\Column(length: 120)]
private ?string $amount = null;
#[ORM\Column]
private ?int $num = null;
#[ORM\ManyToOne(inversedBy: 'plugGhestaItems')]
private ?HesabdariDoc $hesabdariDoc = null;
public function getId(): ?int
{
return $this->id;
}
public function getDoc(): ?PlugGhestaDoc
{
return $this->doc;
}
public function setDoc(?PlugGhestaDoc $doc): static
{
$this->doc = $doc;
return $this;
}
public function getDate(): ?string
{
return $this->date;
}
public function setDate(string $date): static
{
$this->date = $date;
return $this;
}
public function getAmount(): ?string
{
return $this->amount;
}
public function setAmount(string $amount): static
{
$this->amount = $amount;
return $this;
}
public function getNum(): ?int
{
return $this->num;
}
public function setNum(int $num): static
{
$this->num = $num;
return $this;
}
public function getHesabdariDoc(): ?HesabdariDoc
{
return $this->hesabdariDoc;
}
public function setHesabdariDoc(?HesabdariDoc $hesabdariDoc): static
{
$this->hesabdariDoc = $hesabdariDoc;
return $this;
}
}

View file

@ -131,6 +131,12 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
#[ORM\OneToMany(targetEntity: BackBuiltModule::class, mappedBy: 'submitter', orphanRemoval: true)]
private Collection $backBuiltModules;
/**
* @var Collection<int, PlugGhestaDoc>
*/
#[ORM\OneToMany(targetEntity: PlugGhestaDoc::class, mappedBy: 'submitter')]
private Collection $PlugGhestaDocs;
public function __construct()
{
$this->userTokens = new ArrayCollection();
@ -155,6 +161,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
$this->dashboardSettings = new ArrayCollection();
$this->accountingPackageOrders = new ArrayCollection();
$this->backBuiltModules = new ArrayCollection();
$this->PlugGhestaDocs = new ArrayCollection();
}
public function getId(): ?int
@ -960,4 +967,34 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
return $this;
}
/**
* @return Collection<int, PlugGhestaDoc>
*/
public function getPlugGhestaDocs(): Collection
{
return $this->PlugGhestaDocs;
}
public function addPlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if (!$this->PlugGhestaDocs->contains($PlugGhestaDoc)) {
$this->PlugGhestaDocs->add($PlugGhestaDoc);
$PlugGhestaDoc->setSubmitter($this);
}
return $this;
}
public function removePlugGhestaDoc(PlugGhestaDoc $PlugGhestaDoc): static
{
if ($this->PlugGhestaDocs->removeElement($PlugGhestaDoc)) {
// set the owning side to null (unless already changed)
if ($PlugGhestaDoc->getSubmitter() === $this) {
$PlugGhestaDoc->setSubmitter(null);
}
}
return $this;
}
}

View file

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

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -1,136 +1,136 @@
<script>
import { RouterView } from 'vue-router'
import axios from 'axios';
import { ref } from 'vue';
import "./assets/site.css";
export default {
data: () => {
return {
loading:false,
dialog: false,
theme: ref('light'),
hesabix: {
version: '',
lastUpdateDate: '',
lastUpdateDes: '',
},
}
},
methods: {
update() {
this.dialog = false;
localStorage.setItem('hesabixVersion', this.hesabix.version);
window.location.reload();
},
gethesabix() {
this.loading = true;
this.dialog = false;
axios.post('/api/general/stat').then((response) => {
this.hesabix = response.data;
let currentVersion = window.localStorage.getItem('hesabixVersion');
if (currentVersion == undefined) {
window.localStorage.setItem('hesabixVersion', this.hesabix.version);
}
else if (currentVersion != this.hesabix.version) {
//set version Number
this.dialog = true;
this.loading = false;
}
});
}
},
mounted() {
this.gethesabix();
}
}
</script>
<template>
<v-app :theme="theme">
<v-dialog v-model="dialog" max-width="600" persistent class="elevation-4">
<v-card class="rounded-lg">
<!-- نوار ابزار بهعنوان هدر -->
<v-toolbar color="primary" dark flat class="rounded-t-lg">
<v-toolbar-title class="d-flex align-center">
<v-icon start>mdi-update</v-icon>
{{ $t('dialog.update') }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<!-- محتوای کارت -->
<v-card-subtitle class="py-2 text-grey-darken-1">
{{ hesabix.lastUpdateDate }}
</v-card-subtitle>
<v-card-text class="pa-5">
<div class="text-primary" v-html="hesabix.lastUpdateDes"></div>
</v-card-text>
<!-- اکشنها -->
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="flat"
:text="$t('dialog.update')"
@click="update()"
/>
</v-card-actions>
</v-card>
</v-dialog>
<RouterView />
</v-app>
</template>
<style>
.customize-table {
--easy-table-header-font-color: #e1e1e1;
--easy-table-header-background-color: #055bbb;
}
/* هدف قرار دادن اسکرول‌بار در v-navigation-drawer */
.v-navigation-drawer ::-webkit-scrollbar {
width: 4px;
/* عرض اسکرول‌بار را کاهش می‌دهد */
}
.v-navigation-drawer ::-webkit-scrollbar-track {
background: transparent;
/* پس‌زمینه اسکرول‌بار شفاف */
}
.v-navigation-drawer ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
/* رنگ دسته اسکرول‌بار */
border-radius: 4px;
/* گوشه‌های گرد */
}
.v-navigation-drawer ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.4);
/* رنگ هنگام هاور */
}
.v-data-table {
overflow-x: auto;
}
.expanded-row {
background-color: #f5f5f5 !important;
padding: 8px;
}
.custom-header {
background-color: #213e8b !important;
color: #ffffff !important;
text-align: center !important;
}
.v-data-table, .v-data-table-server, .v-data-table-header__content {
margin: 0 auto;
width: fit-content;
text-align: center !important;
}
</style>
<script>
import { RouterView } from 'vue-router'
import axios from 'axios';
import { ref } from 'vue';
import "./assets/site.css";
export default {
data: () => {
return {
loading:false,
dialog: false,
theme: ref('light'),
hesabix: {
version: '',
lastUpdateDate: '',
lastUpdateDes: '',
},
}
},
methods: {
update() {
this.dialog = false;
localStorage.setItem('hesabixVersion', this.hesabix.version);
window.location.reload();
},
gethesabix() {
this.loading = true;
this.dialog = false;
axios.post('/api/general/stat').then((response) => {
this.hesabix = response.data;
let currentVersion = window.localStorage.getItem('hesabixVersion');
if (currentVersion == undefined) {
window.localStorage.setItem('hesabixVersion', this.hesabix.version);
}
else if (currentVersion != this.hesabix.version) {
//set version Number
this.dialog = true;
this.loading = false;
}
});
}
},
mounted() {
this.gethesabix();
}
}
</script>
<template>
<v-app :theme="theme">
<v-dialog v-model="dialog" max-width="600" persistent class="elevation-4">
<v-card class="rounded-lg">
<!-- نوار ابزار بهعنوان هدر -->
<v-toolbar color="primary" dark flat class="rounded-t-lg">
<v-toolbar-title class="d-flex align-center">
<v-icon start>mdi-update</v-icon>
{{ $t('dialog.update') }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-toolbar>
<!-- محتوای کارت -->
<v-card-subtitle class="py-2 text-grey-darken-1">
{{ hesabix.lastUpdateDate }}
</v-card-subtitle>
<v-card-text class="pa-5">
<div class="text-primary" v-html="hesabix.lastUpdateDes"></div>
</v-card-text>
<!-- اکشنها -->
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="primary"
variant="flat"
:text="$t('dialog.update')"
@click="update()"
/>
</v-card-actions>
</v-card>
</v-dialog>
<RouterView />
</v-app>
</template>
<style>
.customize-table {
--easy-table-header-font-color: #e1e1e1;
--easy-table-header-background-color: #055bbb;
}
/* هدف قرار دادن اسکرول‌بار در v-navigation-drawer */
.v-navigation-drawer ::-webkit-scrollbar {
width: 4px;
/* عرض اسکرول‌بار را کاهش می‌دهد */
}
.v-navigation-drawer ::-webkit-scrollbar-track {
background: transparent;
/* پس‌زمینه اسکرول‌بار شفاف */
}
.v-navigation-drawer ::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.2);
/* رنگ دسته اسکرول‌بار */
border-radius: 4px;
/* گوشه‌های گرد */
}
.v-navigation-drawer ::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.4);
/* رنگ هنگام هاور */
}
.v-data-table {
overflow-x: auto;
}
.expanded-row {
background-color: #f5f5f5 !important;
padding: 8px;
}
.custom-header {
background-color: #213e8b !important;
color: #ffffff !important;
text-align: center !important;
}
.v-data-table, .v-data-table-server, .v-data-table-header__content {
margin: 0 auto;
width: fit-content;
text-align: center !important;
}
</style>

View file

@ -315,7 +315,7 @@ export default {
if (Math.floor(this.score) > this.bestScore) {
this.bestScore = Math.floor(this.score);
localStorage.setItem('bestScore', this.bestScore);
lunch }
}
},
resetGame() {
this.gameOver();

View file

@ -1,8 +1,8 @@
<template>
<div>
<v-menu location="bottom end" :close-on-content-click="false">
<v-menu location="bottom end" :close-on-content-click="false" class="d-none d-md-block">
<template v-slot:activator="{ props }">
<v-btn class="" stacked v-bind="props">
<v-btn class="d-none d-md-block" stacked v-bind="props">
<v-icon>mdi-apps</v-icon>
</v-btn>
</template>
@ -305,7 +305,11 @@ export default {
loadShortcuts() {
const savedShortcuts = localStorage.getItem('customShortcuts')
if (savedShortcuts) {
this.customShortcuts = JSON.parse(savedShortcuts)
const shortcuts = JSON.parse(savedShortcuts)
this.customShortcuts = shortcuts.filter(shortcut => shortcut.path && shortcut.path.trim() !== '')
if (shortcuts.length !== this.customShortcuts.length) {
this.saveShortcuts()
}
}
},
isInternalPath(path) {

View file

@ -0,0 +1,346 @@
<template>
<div>
<v-menu v-model="menu" :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-text-field
v-bind="props"
v-model="displayValue"
variant="outlined"
:error-messages="errorMessages"
:rules="combinedRules"
:label="label"
class="my-0"
prepend-inner-icon="mdi-file-document"
clearable
@click:clear="clearSelection"
:loading="loading"
@keydown.enter="handleEnter"
hide-details
density="compact"
style="font-size: 0.7rem;"
>
<template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template>
</v-text-field>
</template>
<v-card min-width="300" max-width="500" class="search-card">
<v-card-text class="pa-2">
<template v-if="!loading">
<v-list density="compact" class="list-container">
<template v-if="filteredItems.length > 0">
<v-list-item
v-for="item in filteredItems"
:key="item.code"
@click="selectItem(item)"
class="search-item"
>
<div class="d-flex flex-column w-100">
<div class="d-flex justify-space-between align-center mb-1">
<span class="text-grey-darken-1 text-caption">{{ item.code }}</span>
<span class="text-warning text-caption">{{ item.date }}</span>
</div>
<div class="text-subtitle-1 font-weight-medium mb-1">{{ item.des }}</div>
<div class="d-flex justify-space-between align-center">
<span class="text-success text-caption">مبلغ: {{ formatPrice(item.amount) }} ریال</span>
<span class="text-primary text-caption">وضعیت: {{ item.status }}</span>
</div>
<div v-if="item.personName || item.personNikename" class="text-caption text-grey-darken-1 mt-1">
مشتری: {{ item.personName }} {{ item.personNikename ? `(${item.personNikename})` : '' }}
</div>
</div>
</v-list-item>
</template>
<template v-else>
<v-list-item>
<v-list-item-title class="text-center text-grey">
نتیجهای یافت نشد
</v-list-item-title>
</v-list-item>
</template>
</v-list>
</template>
<v-progress-circular
v-else
indeterminate
color="primary"
class="d-flex mx-auto my-4"
></v-progress-circular>
</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>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
interface SearchItem {
id: number;
code: string;
date: string;
dateSubmit: string;
type: string;
des: string;
amount: number;
submitter: string;
status: string;
personName?: string;
personNikename?: string;
person?: {
id: number;
name: string;
nikename: string;
};
label?: {
code: string;
label: string;
};
}
export default defineComponent({
name: 'Hdocsearch',
props: {
modelValue: {
type: [Object, String],
default: null
},
label: {
type: String,
default: 'جستجوی فاکتور'
},
docType: {
type: String,
required: true,
validator: (value: string) => ['invoice', 'receipt', 'order', 'sell'].includes(value)
},
returnObject: {
type: Boolean,
default: false
},
rules: {
type: Array as () => any[],
default: () => []
}
},
data() {
return {
selectedItem: null as SearchItem | null,
items: [] as SearchItem[],
loading: false,
menu: false,
searchQuery: '',
totalItems: 0,
currentPage: 1,
itemsPerPage: 10,
searchTimeout: null as number | null,
snackbar: {
show: false,
text: '',
color: 'success'
},
errorMessages: [] as string[]
}
},
computed: {
filteredItems() {
return Array.isArray(this.items) ? this.items : []
},
displayValue: {
get() {
if (this.menu) {
return this.searchQuery
}
if (this.selectedItem) {
return `${this.selectedItem.code} - ${this.formatPrice(this.selectedItem.amount)} ریال`
}
return this.searchQuery
},
set(value: string) {
this.searchQuery = value
if (!value) {
this.clearSelection()
} else if (value !== this.selectedItem?.des) {
this.menu = true
}
}
},
combinedRules() {
return [
(v: any) => !!v || 'انتخاب فاکتور الزامی است',
...this.rules
]
}
},
watch: {
modelValue: {
handler(newVal: SearchItem | string | null) {
if (newVal) {
if (this.returnObject) {
this.selectedItem = newVal as SearchItem
} else {
const code = typeof newVal === 'string' ? newVal : (newVal as SearchItem).code
this.selectedItem = this.items.find(item => item.code === code) || null
if (!this.selectedItem) {
this.fetchSingleDocument(code)
}
}
} else {
this.selectedItem = null
}
},
immediate: true
},
searchQuery: {
handler(newVal: string) {
this.currentPage = 1
if (this.searchTimeout) {
clearTimeout(this.searchTimeout)
}
this.searchTimeout = setTimeout(() => {
this.fetchData()
if (newVal && newVal !== this.selectedItem?.des) {
this.menu = true
}
}, 500)
}
},
menu: {
handler(newVal: boolean) {
if (!newVal) {
this.searchQuery = this.selectedItem?.des || ''
}
}
}
},
methods: {
showMessage(text: string, color = 'success') {
this.snackbar.text = text
this.snackbar.color = color
this.snackbar.show = true
},
async fetchData() {
this.loading = true
try {
const response = await axios.post('/api/componenets/doc/search', {
page: this.currentPage,
itemsPerPage: this.itemsPerPage,
search: this.searchQuery,
docType: this.docType,
sortBy: null
})
if (response.data && Array.isArray(response.data)) {
this.items = response.data
this.totalItems = response.data.length
} else if (response.data && response.data.items) {
this.items = response.data.items
this.totalItems = response.data.total || response.data.items.length
} else {
this.items = []
this.totalItems = 0
}
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue
} else {
this.selectedItem = this.items.find(item => item.code === this.modelValue)
if (!this.selectedItem) {
await this.fetchSingleDocument(this.modelValue)
}
}
}
} catch (error) {
this.showMessage('خطا در بارگذاری داده‌ها', 'error')
this.items = []
this.totalItems = 0
} finally {
this.loading = false
}
},
async fetchSingleDocument(code: string) {
try {
const response = await axios.get(`/api/componenets/doc/get/${code}`)
if (response.data && response.data.code) {
this.items.push(response.data)
this.selectedItem = response.data
this.searchQuery = response.data.des
this.$emit('update:modelValue', this.returnObject ? response.data : response.data.code)
}
} catch (error) {
this.showMessage('خطا در بارگذاری فاکتور', 'error')
}
},
selectItem(item: SearchItem) {
this.selectedItem = item
this.searchQuery = item.des
this.$emit('update:modelValue', this.returnObject ? item : item.code)
this.menu = false
this.errorMessages = []
},
clearSelection() {
this.selectedItem = null
this.searchQuery = ''
this.$emit('update:modelValue', null)
this.errorMessages = ['انتخاب فاکتور الزامی است']
this.menu = false
},
handleEnter() {
if (!this.loading && this.filteredItems.length === 0) {
this.showMessage('نتیجه‌ای یافت نشد', 'warning')
}
},
formatPrice(price: number): string {
return new Intl.NumberFormat('fa-IR').format(price || 0)
}
},
created() {
this.fetchData()
}
})
</script>
<style scoped>
.list-container {
max-height: 400px;
overflow-y: auto;
}
.search-card {
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.search-item {
border-radius: 4px;
transition: all 0.2s ease;
border: 1px solid transparent;
padding: 0px 0px;
margin-bottom: 4px;
}
.search-item:hover {
background-color: rgba(var(--v-theme-primary), 0.05);
border-color: rgba(var(--v-theme-primary), 0.1);
}
</style>

View file

@ -30,6 +30,10 @@ export default {
rules: {
type: Array,
default: () => []
},
allowDecimal: {
type: Boolean,
default: false
}
},
@ -43,7 +47,7 @@ export default {
computed: {
combinedRules() {
return [
v => !v || /^\d+$/.test(v.replace(/[^0-9]/g, '')) || this.$t('numberinput.invalid_number'),
v => !v || (this.allowDecimal ? /^\d*\.?\d*$/.test(v.replace(/[^0-9.]/g, '')) : /^\d+$/.test(v.replace(/[^0-9]/g, ''))) || this.$t('numberinput.invalid_number'),
...this.rules
]
}
@ -54,8 +58,8 @@ export default {
immediate: true,
handler(newVal) {
if (newVal !== null && newVal !== undefined) {
const cleaned = String(newVal).replace(/[^0-9]/g, '')
this.inputValue = cleaned ? Number(cleaned).toLocaleString('en-US') : ''
const cleaned = String(newVal).replace(this.allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '')
this.inputValue = cleaned ? (this.allowDecimal ? cleaned : Number(cleaned).toLocaleString('en-US')) : ''
} else {
this.inputValue = ''
}
@ -66,9 +70,9 @@ export default {
this.$emit('update:modelValue', 0)
this.errorMessages = []
} else {
const cleaned = String(newVal).replace(/[^0-9]/g, '')
if (/^\d+$/.test(cleaned)) {
const numericValue = cleaned ? Number(cleaned) : 0
const cleaned = String(newVal).replace(this.allowDecimal ? /[^0-9.]/g : /[^0-9]/g, '')
if (this.allowDecimal ? /^\d*\.?\d*$/.test(cleaned) : /^\d+$/.test(cleaned)) {
const numericValue = cleaned ? (this.allowDecimal ? parseFloat(cleaned) : Number(cleaned)) : 0
this.$emit('update:modelValue', numericValue)
this.errorMessages = []
} else {
@ -81,8 +85,20 @@ export default {
methods: {
restrictToNumbers(event) {
const charCode = event.charCode
if (charCode < 48 || charCode > 57) {
event.preventDefault()
if (this.allowDecimal) {
// اجازه ورود اعداد و نقطه اعشاری
if ((charCode < 48 || charCode > 57) && charCode !== 46) {
event.preventDefault()
}
// جلوگیری از ورود بیش از یک نقطه اعشاری
if (charCode === 46 && this.inputValue.includes('.')) {
event.preventDefault()
}
} else {
// فقط اجازه ورود اعداد
if (charCode < 48 || charCode > 57) {
event.preventDefault()
}
}
}
}

View file

@ -94,6 +94,8 @@ const fa_lang = {
}
},
drawer: {
ghesta: "فروش اقساطی",
ghesta_invoices: "فاکتورها",
wallets: "کیف پول‌ها",
ultimate_package: 'بسته‌های نامحدود',
sell_chart: "فروش هفته گذشته",
@ -402,6 +404,7 @@ const fa_lang = {
result: "نتیجه",
title: "پاسخ",
view: "مشاهده",
payment: "ثبت پرداخت",
exit: "خروج از حساب کاربری",
complete_all: "موارد الزامی را تکمیل کنید",
back: "صفحه قبل",

File diff suppressed because it is too large Load diff

71
webUI/src/utils/date.js Normal file
View file

@ -0,0 +1,71 @@
/**
* تبدیل timestamp به تاریخ شمسی
* @param {number|string} timestamp - timestamp مورد نظر
* @returns {string} - تاریخ شمسی
*/
export const formatDate = (timestamp) => {
if (!timestamp) return ''
try {
// اگر timestamp به صورت رشته است، آن را به عدد تبدیل می‌کنیم
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
// اگر timestamp به ثانیه است، آن را به میلی‌ثانیه تبدیل می‌کنیم
const date = new Date(ts * 1000)
// بررسی معتبر بودن تاریخ
if (isNaN(date.getTime())) {
console.warn('Invalid date:', timestamp)
return ''
}
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
calendar: 'persian'
}
return new Intl.DateTimeFormat('fa-IR', options).format(date)
} catch (error) {
console.error('Error formatting date:', error)
return ''
}
}
/**
* تبدیل timestamp به تاریخ و زمان شمسی
* @param {number|string} timestamp - timestamp مورد نظر
* @returns {string} - تاریخ و زمان شمسی
*/
export const formatDateTime = (timestamp) => {
if (!timestamp) return ''
try {
// اگر timestamp به صورت رشته است، آن را به عدد تبدیل می‌کنیم
const ts = typeof timestamp === 'string' ? parseInt(timestamp) : timestamp
// اگر timestamp به ثانیه است، آن را به میلی‌ثانیه تبدیل می‌کنیم
const date = new Date(ts * 1000)
// بررسی معتبر بودن تاریخ
if (isNaN(date.getTime())) {
console.warn('Invalid date:', timestamp)
return ''
}
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
calendar: 'persian'
}
return new Intl.DateTimeFormat('fa-IR', options).format(date)
} catch (error) {
console.error('Error formatting date:', error)
return ''
}
}

19
webUI/src/utils/number.js Normal file
View file

@ -0,0 +1,19 @@
/**
* فرمتبندی اعداد به فرمت فارسی
* @param {number} number - عدد مورد نظر
* @returns {string} - عدد فرمت شده
*/
export const formatNumber = (number) => {
if (!number) return '0'
return new Intl.NumberFormat('fa-IR').format(number)
}
/**
* تبدیل عدد به فرمت پول
* @param {number} number - عدد مورد نظر
* @param {string} currency - واحد پول
* @returns {string} - مبلغ فرمت شده
*/
export const formatCurrency = (number, currency = 'ریال') => {
return `${formatNumber(number)} ${currency}`
}

View file

@ -178,6 +178,7 @@ export default {
{ path: '/acc/plugin-center/my', key: '\\', label: this.$t('drawer.my_plugins'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/plugin-center/invoice', key: '`', label: this.$t('drawer.plugins_invoices'), ctrl: true, shift: true, permission: () => this.permissions.owner },
{ path: '/acc/hrm/docs/list', key: 'H', label: this.$t('drawer.hrm_docs'), ctrl: true, shift: true, permission: () => this.isPluginActive('hrm') && this.permissions.plugHrmDocs },
{ path: '/acc/plugins/ghesta/list', key: 'G', label: this.$t('drawer.ghesta_invoices'), ctrl: true, shift: true, permission: () => this.isPluginActive('ghesta') && this.permissions.plugGhestaManager },
];
},
restorePermissions(shortcuts) {
@ -807,6 +808,26 @@ export default {
</template>
</v-list-item>
</v-list-group>
<v-list-group v-show="isPluginActive('ghesta') && permissions.plugGhestaManager">
<template v-slot:activator="{ props }">
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.ghesta')">
<template v-slot:prepend><v-icon icon="mdi-cash-multiple" color="primary"></v-icon></template>
</v-list-item>
</template>
<v-list-item to="/acc/plugins/ghesta/list">
<v-list-item-title>
{{ $t('drawer.ghesta_invoices') }}
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/ghesta/list') }}</span>
</v-list-item-title>
<template v-slot:append>
<v-tooltip :text="$t('dialog.add_new')" location="end">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" to="/acc/plugins/ghesta/mod/" />
</template>
</v-tooltip>
</template>
</v-list-item>
</v-list-group>
<v-list-item class="text-dark" v-if="permissions.owner" to="/acc/sms/panel">
<template v-slot:prepend><v-icon icon="mdi-message-cog" color="primary"></v-icon></template>
<v-list-item-title>

View file

@ -450,10 +450,42 @@ export default {
axios.post('/api/openbalance/get').then((Response) => {
if (Response.data && Response.data.data) {
this.data = Response.data.data;
// اطمینان از وجود آرایه commodities
if (!this.data.commodities) {
this.data.commodities = [];
this.data = Response.data.data;
// محاسبه مجموع بانکها
if (this.data.banks) {
this.data.banks.forEach(item => {
if (item.openbalance) {
this.sums.banks += parseFloat(item.openbalance);
}
});
}
// محاسبه مجموع صندوقها
if (this.data.cashdesks) {
this.data.cashdesks.forEach(item => {
if (item.openbalance) {
this.sums.cashdesks += parseFloat(item.openbalance);
}
});
}
// محاسبه مجموع تنخواهگردانها
if (this.data.salarys) {
this.data.salarys.forEach(item => {
if (item.openbalance) {
this.sums.salarys += parseFloat(item.openbalance);
}
});
}
// محاسبه مجموع سهامداران
if (this.data.shareholders) {
this.data.shareholders.forEach(item => {
if (item.openbalance) {
this.sums.shareholders += parseFloat(item.openbalance);
}
});
}
// محاسبه مجموع موجودی کالا
@ -465,8 +497,8 @@ export default {
});
}
this.sums.degSum = parseFloat(this.sums.banks) + parseFloat(this.sums.cashdesks) + parseFloat(this.sums.salarys) + parseFloat(this.sums.inventory);
this.sums.shareSum = parseFloat(this.sums.shareholders);
this.sums.degSum = this.sums.banks + this.sums.cashdesks + this.sums.salarys + this.sums.inventory;
this.sums.shareSum = this.sums.shareholders;
}
}).catch(error => {
console.error('Error loading data:', error);

View file

@ -1,177 +1,747 @@
<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">
<i class="mx-2 fa fa-cash-register"></i>
درآمدها
</h3>
<div class="block-options">
<router-link to="/acc/incomes/mod/" class="block-options-item">
<span class="fa fa-plus fw-bolder"></span>
</router-link>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
</div>
</div>
<EasyDataTable table-class-name="customize-table" v-model:items-selected="itemsSelected" show-index
alternating :search-value="searchValue" :headers="headers" :items="items" theme-color="#1d90ff"
header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading">
<template #item-operation="{ code }">
<div class="dropdown-center">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link" data-bs-toggle="dropdown"
id="dropdown-align-center-alt-primary" type="button">
<i class="fa-solid fa-ellipsis"></i>
</button>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end"
style="">
<router-link class="dropdown-item" :to="'/acc/accounting/view/' + code">
<i class="fa fa-file pe-2 text-primary"></i>
سند حسابداری
</router-link>
<router-link class="dropdown-item" :to="{ name: 'incomes_mod', params: { id: code } }">
<i class="fa fa-eye pe-2 text-success"></i>
مشاهده
</router-link>
<router-link class="dropdown-item" :to="{ name: 'incomes_mod', params: { id: code } }">
<i class="fa fa-edit pe-2"></i>
ویرایش
</router-link>
<button type="button" @click="deleteItem(code)" class="dropdown-item text-danger">
<i class="fa fa-trash pe-2"></i>
حذف
</button>
</div>
</div>
</template>
</EasyDataTable>
<v-container fluid class="pa-0 ma-0 my-3">
<v-card class="rounded border-start border-success border-3" elevation="2" link href="javascript:void(0)">
<v-card-text class="bg-body-light pa-4">
<v-row>
<v-col cols="12" sm="6">
<span class="text-dark">
<v-icon icon="mdi-format-list-bulleted" size="small" class="me-1" />
مبلغ کل:
</span>
<span class="text-primary">
{{ $filters.formatNumber(sumTotal) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</v-col>
<v-toolbar color="toolbar" :title="$t('drawer.incomes')">
<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-col cols="12" sm="6">
<span class="text-dark">
<v-icon icon="mdi-format-list-checks" size="small" class="me-1" />
جمع مبلغ موارد انتخابی:
</span>
<span class="text-primary">
{{ $filters.formatNumber(sumSelected) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/incomes/mod/" />
</template>
</v-tooltip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="red">
<v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
<v-icon icon="mdi-file-pdf-box" />
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')"
@click="exportPDF(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check" />
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportPDF(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all" />
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="" color="green">
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
<v-icon icon="mdi-file-excel-box" />
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
<v-list-item :disabled="!hasSelected" class="text-dark" :title="$t('dialog.selected')"
@click="exportExcel(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check" />
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.all')" @click="exportExcel(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all" />
</template>
</v-list-item>
</v-list>
</v-menu>
<v-tooltip :text="$t('dialog.delete')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-trash-can" color="danger" @click="deleteGroup" :disabled="!hasSelected" />
</template>
</v-tooltip>
</v-toolbar>
<v-text-field :rounded="false" :loading="loading" color="green" class="mb-0 pt-0 rounded-0" hide-details="auto" density="compact"
:placeholder="$t('dialog.search_txt')" v-model="searchQuery" type="text" clearable>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template>
</v-tooltip>
</template>
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" size="sm" color="primary">
<v-icon>mdi-filter</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.filters')" location="bottom" />
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<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 !== '56'"
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: '56', name: 'درآمدها' }"
@select="handleAccountSelect"
@account-selected="handleAccountSelected"
/>
</v-list-item>
<v-divider class="my-2"></v-divider>
<!-- فیلتر بازه زمانی -->
<v-list-item>
<v-list-item-title class="text-dark mb-2">
</v-list-item-title>
<v-row>
<v-col cols="12">
<v-checkbox
v-model="timeFilters.find(f => f.value === 'custom').checked"
label="بازه زمانی"
@change="handleCustomDateFilterChange"
hide-details
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.from"
label="از تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
<v-col cols="12" v-if="timeFilters.find(f => f.value === 'custom').checked">
<Hdatepicker
v-model="dateRange.to"
label="تا تاریخ"
@update:model-value="handleDateRangeChange"
/>
</v-col>
</v-row>
</v-list-item>
<!-- فیلترهای زمانی -->
<v-list-item v-for="(filter, index) in timeFilters.filter(f => f.value !== 'custom')" :key="index" class="text-dark">
<template v-slot:title>
<v-checkbox v-model="filter.checked" :label="filter.label" @change="applyTimeFilter(filter.value)"
hide-details />
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table-server :headers="headers" :items="items" :loading="loading" :items-length="totalItems"
v-model:options="serverOptions" v-model:expanded="expanded" @update:options="fetchData" item-value="code"
class="elevation-1 data-table-wrapper" :header-props="{ class: 'custom-header' }" show-expand>
<template #header.checkbox>
<v-checkbox :model-value="isAllSelected" @change="toggleSelectAll" hide-details density="compact" />
</template>
<template #item.checkbox="{ item }">
<v-checkbox :model-value="selectedItems.has(item.code)" @change="toggleSelection(item.code)" hide-details
density="compact" />
</template>
<template #item.operation="{ item }">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<v-list>
<v-list-item class="text-dark" :title="$t('dialog.view')" :to="'/acc/accounting/view/' + item.code">
<template v-slot:prepend>
<v-icon icon="mdi-file" color="primary" />
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.edit')" :to="'/acc/incomes/mod/' + item.code">
<template v-slot:prepend>
<v-icon icon="mdi-pencil" />
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.delete')" @click="deleteItem(item.code)">
<template v-slot:prepend>
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can" />
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
<template #item.amount="{ item }">
{{ $filters.formatNumber(item.amount) }}
</template>
<template #expanded-row="{ columns, item }">
<tr>
<td :colspan="columns.length" class="expanded-row">
<v-container class="pa-0 ma-0">
<v-row>
<v-col cols="12">
<v-list density="compact" class="pa-0 ma-0">
<v-list-item :border="true" v-for="(center, index) in item.incomeCenters" :key="index">
<v-list-item-title>
{{ center.name }} ({{ center.code }})
{{ $t('dialog.acc_price') }} : {{ $filters.formatNumber(center.amount) }}
{{ $t('dialog.des') }} : {{ center.des }}
</v-list-item-title>
</v-list-item>
<v-list-item v-if="!item.incomeCenters || item.incomeCenters.length === 0">
<v-list-item-title></v-list-item-title>
</v-list-item>
</v-list>
</v-col>
</v-row>
</v-container>
</div>
</div>
</div>
</div>
</td>
</tr>
</template>
</v-data-table-server>
<v-container fluid class="pa-0 ma-0 my-3">
<v-card class="rounded border-start border-success border-3" elevation="2" link href="javascript:void(0)">
<v-card-text class="bg-body-light pa-4">
<v-row>
<v-col cols="12" sm="6">
<span class="text-dark">
<v-icon icon="mdi-format-list-bulleted" size="small" class="me-1" />
مبلغ کل:
</span>
<span class="text-primary">
{{ $filters.formatNumber(totalIncome) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</v-col>
<v-col cols="12" sm="6">
<span class="text-dark">
<v-icon icon="mdi-format-list-checks" size="small" class="me-1" />
جمع مبلغ موارد انتخابی:
</span>
<span class="text-primary">
{{ $filters.formatNumber(selectedIncome) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
</template>
<script>
import { ref } from "vue";
import axios from "axios";
import Swal from "sweetalert2";
<script setup>
import { ref, onMounted, computed, watch } from 'vue';
import axios from 'axios';
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';
import Hdatepicker from '@/components/forms/Hdatepicker.vue';
export default {
name: "list",
data: () => {
return {
sumSelected: 0,
sumTotal: 0,
itemsSelected: [],
searchValue: '',
loading: ref(true),
items: [],
headers: [
{ text: "عملیات", value: "operation", width: "120" },
{ text: "کد", value: "code", width: "80" },
{ text: "تاریخ", value: "date" },
{ text: "شرح", value: "des" },
{ text: "مبلغ", value: "amount" },
]
}
},
methods: {
loadData() {
axios.post('/api/accounting/search', {
type: 'income'
})
.then((response) => {
this.items = response.data;
this.items.forEach((item) => {
item.amount = this.$filters.formatNumber(item.amount);
this.sumTotal += parseInt(item.amount.replaceAll(",", ''));
})
this.loading = false;
})
},
deleteItem(code) {
const apiUrl = getApiUrl();
axios.defaults.baseURL = apiUrl;
// Refs
const loading = ref(false);
const items = ref([]);
const selectedItems = ref(new Set());
const totalItems = ref(0);
const searchQuery = ref('');
const timeFilter = ref('all');
const expanded = ref([]);
const selectedAccountId = ref('66');
const dateRange = ref({
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
});
// فیلترهای زمانی
const timeFilters = ref([
{ label: 'امروز', value: 'today', checked: false },
{ label: 'این هفته', value: 'week', checked: false },
{ label: 'این ماه', value: 'month', checked: false },
{ label: 'بازه زمانی', value: 'custom', checked: false },
{ label: 'همه', value: 'all', checked: true },
]);
// تعریف ستونهای جدول
const headers = ref([
{ title: '', key: 'checkbox', sortable: false, width: '50', align: 'center' },
{ title: 'ردیف', key: 'index', align: 'center', sortable: false, width: '70' },
{ title: 'عملیات', key: 'operation', align: 'center', sortable: false, width: '100' },
{ title: 'کد', key: 'code', align: 'center', sortable: true },
{ title: 'مبلغ', key: 'amount', align: 'center', sortable: true },
{ title: 'تاریخ', key: 'date', align: 'center', sortable: true },
{ title: 'شرح', key: 'des', align: 'center', sortable: true },
]);
// تنظیمات سرور
const serverOptions = ref({
page: 1,
itemsPerPage: 10,
sortBy: [],
sortDesc: [],
});
// Computed properties
const hasSelected = computed(() => selectedItems.value.size > 0);
const isAllSelected = computed(() => selectedItems.value.size === items.value.length);
const totalIncome = computed(() => {
return items.value.reduce((sum, item) => sum + Number(item.amount || 0), 0);
});
const selectedIncome = computed(() => {
return items.value
.filter((item) => selectedItems.value.has(item.code))
.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 = '66';
fetchData();
};
// دیبونس برای جستجو
const debouncedSearch = debounce(() => fetchData(), 500);
// دیبونس برای تغییر تاریخ
const debouncedFetchData = debounce(() => {
fetchData();
}, 500);
// اصلاح متد handleDateRangeChange
const handleDateRangeChange = () => {
if (dateRange.value.from && dateRange.value.to) {
// تبدیل تاریخهای شمسی به آبجکت moment
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa');
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa');
// بررسی اعتبار بازه زمانی
if (fromDate.isAfter(toDate)) {
Swal.fire({
text: 'آیا برای این سند مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: `خیر`,
}).then((result) => {
/* Read more about isConfirmed, isDenied below */
if (result.isConfirmed) {
axios.post('/api/accounting/remove', {
'code': code
}
).then((response) => {
if (response.data.result == 1) {
let index = 0;
for (let z = 0; z < this.items.length; z++) {
index++;
if (this.items[z]['code'] == code) {
this.items.splice(index - 1, 1);
}
}
Swal.fire({
text: 'سند با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
})
}
})
text: 'تاریخ شروع نمی‌تواند بعد از تاریخ پایان باشد',
icon: 'error',
confirmButtonText: 'قبول'
});
// پاک کردن تاریخها
dateRange.value = {
from: null,
to: null
};
return;
}
},
beforeMount() {
this.loadData();
},
watch: {
itemsSelected: {
handler: function (val, oldVal) {
this.sumSelected = 0;
this.itemsSelected.forEach((item) => {
this.sumSelected += parseInt(item.amount.replaceAll(",", ""))
});
// ارسال درخواست با تاخیر
debouncedFetchData();
}
};
// اصلاح متد handleCustomDateFilterChange
const handleCustomDateFilterChange = (checked) => {
if (checked) {
// غیرفعال کردن سایر فیلترها
timeFilters.value.forEach(filter => {
if (filter.value !== 'custom') {
filter.checked = false;
}
});
timeFilter.value = 'custom';
// تنظیم تاریخهای پیشفرض
dateRange.value = {
from: moment().locale('fa').subtract(1, 'days').format('YYYY/MM/DD'),
to: moment().locale('fa').format('YYYY/MM/DD')
};
// ارسال درخواست با تاریخهای پیشفرض
debouncedFetchData();
} else {
// اگر چکباکس بازه زمانی غیرفعال شد، فیلتر "همه" را فعال کن
timeFilters.value.forEach(filter => {
filter.checked = filter.value === 'all';
});
timeFilter.value = 'all';
// پاک کردن تاریخها
dateRange.value = {
from: null,
to: null
};
debouncedFetchData();
}
};
// اصلاح متد applyTimeFilter
const applyTimeFilter = (value) => {
timeFilters.value.forEach((filter) => {
filter.checked = filter.value === value;
});
timeFilter.value = value;
// اگر فیلتر بازه زمانی غیرفعال شد، تاریخها را پاک کن
if (value !== 'custom') {
dateRange.value = {
from: null,
to: null
};
}
fetchData();
};
// فچ کردن دادهها از سرور
const fetchData = async () => {
try {
loading.value = true;
const filters = {};
if (searchQuery.value.trim()) {
filters.search = { value: searchQuery.value.trim() };
}
if (timeFilter.value) {
filters.timeFilter = timeFilter.value;
if (timeFilter.value === 'custom' && dateRange.value.from && dateRange.value.to) {
// تبدیل تاریخهای شمسی به فرمت مورد نیاز
const fromDate = moment(dateRange.value.from, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD');
const toDate = moment(dateRange.value.to, 'jYYYY/jMM/jDD').locale('fa').format('YYYY/MM/DD');
filters.date = {
from: fromDate,
to: toDate
};
} else {
const today = moment().locale('fa').format('YYYY/MM/DD');
switch (timeFilter.value) {
case 'today':
filters.date = {
from: today,
to: today
};
break;
case 'week':
filters.date = {
from: moment().locale('fa').subtract(6, 'days').format('YYYY/MM/DD'),
to: today
};
break;
case 'month':
filters.date = {
from: moment().locale('fa').startOf('jMonth').format('YYYY/MM/DD'),
to: today
};
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 : [];
const sortBy = sortByArray.length > 0 ? sortByArray[0].key : 'code';
const sortDesc = sortDescArray.length > 0 ? sortDescArray[0] : true;
const payload = {
filters,
pagination: {
page: serverOptions.value.page,
limit: serverOptions.value.itemsPerPage,
},
deep: true
sort: {
sortBy,
sortDesc,
},
};
console.log('Request payload:', payload); // برای دیباگ
const response = await axios.post('/api/income/list/search', {
type: 'income',
...payload,
});
if (response.data?.items) {
const startIndex = (serverOptions.value.page - 1) * serverOptions.value.itemsPerPage;
items.value = response.data.items.map((item, index) => ({
...item,
index: startIndex + index + 1,
}));
totalItems.value = response.data.total;
} else {
items.value = [];
totalItems.value = 0;
}
} catch (error) {
console.error('Error fetching data:', error);
Swal.fire({
text: 'خطا در بارگذاری داده‌ها: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
});
} finally {
loading.value = false;
}
};
// حذف یک آیتم
const deleteItem = async (code) => {
const result = await Swal.fire({
text: 'آیا از حذف این آیتم اطمینان دارید؟',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر',
});
if (result.isConfirmed) {
try {
loading.value = true;
const response = await axios.post('/api/accounting/remove', { code });
if (response.data.result === 1) {
Swal.fire({
text: 'آیتم با موفقیت حذف شد',
icon: 'success',
confirmButtonText: 'قبول',
});
fetchData();
}
} catch (error) {
console.error('Error deleting item:', error);
Swal.fire({
text: 'خطا در حذف آیتم: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
});
} finally {
loading.value = false;
}
}
}
};
// انتخاب و لغو انتخاب
const toggleSelection = (code) => {
if (selectedItems.value.has(code)) {
selectedItems.value.delete(code);
} else {
selectedItems.value.add(code);
}
};
const toggleSelectAll = () => {
if (selectedItems.value.size === items.value.length) {
selectedItems.value.clear();
} else {
items.value.forEach((item) => selectedItems.value.add(item.code));
}
};
// خروجی PDF
const exportPDF = async (all = false) => {
try {
loading.value = true;
if (!all && selectedItems.value.size === 0) {
Swal.fire({
text: 'هیچ آیتمی برای خروجی انتخاب نشده است',
icon: 'warning',
confirmButtonText: 'قبول',
});
return;
}
const selectedItemsArray = all
? items.value
: items.value.filter((item) => selectedItems.value.has(item.code));
const payload = all ? { all: true } : { items: selectedItemsArray };
const response = await axios.post('/api/incomes/list/print', payload);
const printId = response.data.id;
window.open(`${apiUrl}/front/print/${printId}`, '_blank');
} catch (error) {
console.error('Error exporting PDF:', error);
Swal.fire({
text: 'خطا در خروجی PDF: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
});
} finally {
loading.value = false;
}
};
// خروجی Excel
const exportExcel = async (all = false) => {
try {
loading.value = true;
if (!all && selectedItems.value.size === 0) {
Swal.fire({
text: 'هیچ آیتمی برای خروجی انتخاب نشده است',
icon: 'warning',
confirmButtonText: 'قبول',
});
return;
}
const selectedItemsArray = all
? items.value
: items.value.filter((item) => selectedItems.value.has(item.code));
const payload = all ? { all: true } : { items: selectedItemsArray };
const response = await axios.post('/api/incomes/list/excel', payload, {
responseType: 'blob',
});
const url = window.URL.createObjectURL(
new Blob([response.data], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
})
);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'incomes.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
} catch (error) {
console.error('Error exporting Excel:', error);
Swal.fire({
text: 'خطا در خروجی Excel: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
});
} finally {
loading.value = false;
}
};
// حذف گروهی
const deleteGroup = async () => {
if (selectedItems.value.size === 0) {
Swal.fire({
text: 'هیچ آیتمی برای حذف انتخاب نشده است',
icon: 'warning',
confirmButtonText: 'قبول',
});
return;
}
const result = await Swal.fire({
text: 'آیا از حذف آیتم‌های انتخاب‌شده اطمینان دارید؟',
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر',
});
if (result.isConfirmed) {
try {
loading.value = true;
const selectedCodes = Array.from(selectedItems.value);
const promises = selectedCodes.map((code) =>
axios.post('/api/accounting/remove', { code })
);
await Promise.all(promises);
Swal.fire({
text: 'آیتم‌ها با موفقیت حذف شدند',
icon: 'success',
confirmButtonText: 'قبول',
});
selectedItems.value.clear();
fetchData();
} catch (error) {
console.error('Error deleting group:', error);
Swal.fire({
text: 'خطا در حذف گروهی: ' + (error.response?.data?.detail || error.message),
icon: 'error',
confirmButtonText: 'قبول',
});
} finally {
loading.value = false;
}
}
};
// اضافه کردن watch برای تغییرات تاریخها
watch([() => dateRange.value.from, () => dateRange.value.to], () => {
if (timeFilter.value === 'custom') {
handleDateRangeChange();
}
}, { deep: true });
// Watchers
watch(() => serverOptions.value.page, () => {
selectedItems.value.clear();
});
watch(searchQuery, () => debouncedSearch());
// OnMounted
onMounted(() => {
fetchData();
});
</script>
<style scoped></style>
<style scoped>
.v-data-table ::v-deep .v-data-table__checkbox {
margin-right: 0;
margin-left: 0;
}
.expanded-row {
background-color: #f5f5f5;
padding: 10px;
}
</style>

View file

@ -92,12 +92,13 @@
</v-dialog>
<!-- محتوای اصلی -->
<v-container fluid class="pa-2">
<v-container fluid class="pa-4">
<v-row dense>
<v-col cols="12" md="12">
<v-autocomplete v-model="selectedPerson" :items="listPersons" item-title="nikename" item-value="code"
return-object :label="$t('dialog.user_info')" dense hide-details prepend-inner-icon="mdi-account"
:loading="loading" @update:search="debouncedSearchPerson" @update:model-value="updateRoute">
:loading="loading" @update:search="debouncedSearchPerson" @update:model-value="updateRoute"
class="rounded-lg elevation-2">
<template v-slot:no-data>
{{ $t('pages.person_card.no_results') }}
</template>
@ -128,8 +129,8 @@
</v-autocomplete>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined>
<v-toolbar color="primary-dark" dense flat>
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
{{ $t('pages.person_card.account_card') }}
<small class="text-info-light" v-if="selectedPerson">{{ selectedPerson.nikename }}</small>
@ -154,8 +155,8 @@
</v-card>
</v-col>
<v-col cols="12" md="6">
<v-card flat outlined>
<v-toolbar color="primary-dark" dense flat>
<v-card flat outlined class="rounded-lg elevation-2">
<v-toolbar color="primary-dark" dense flat class="rounded-t-lg">
<v-toolbar-title class="text-white">
{{ $t('pages.person_card.account_status') }}
<small class="text-info-light" v-if="selectedPerson">{{ selectedPerson.nikename }}</small>
@ -187,9 +188,9 @@
<v-row dense>
<v-col cols="12">
<v-data-table v-model="itemsSelected" :headers="headers" :items="items" :search="searchValue" :loading="loading"
show-select dense :items-per-page="25" class="elevation-1" :header-props="{ class: 'custom-header' }">
show-select dense :items-per-page="25" class="elevation-2 rounded-lg" :header-props="{ class: 'custom-header' }">
<template v-slot:top>
<v-toolbar flat dense color="grey-lighten-4">
<v-toolbar flat dense color="grey-lighten-4" class="rounded-t-lg">
<v-toolbar-title class="text-subtitle-1">{{ $t('pages.person_card.transactions') }}</v-toolbar-title>
<v-spacer></v-spacer>
<v-text-field v-model="searchValue" dense hide-details
@ -209,6 +210,12 @@
{{ getTypeLabel(item.type) }}
</v-btn>
</template>
<template v-slot:item.bd="{ item }">
{{ $filters.formatNumber(item.bd) }}
</template>
<template v-slot:item.bs="{ item }">
{{ $filters.formatNumber(item.bs) }}
</template>
<template v-slot:no-data>
{{ $t('pages.person_card.no_data') }}
</template>
@ -404,5 +411,39 @@ export default {
</script>
<style scoped>
/* استایل‌های اضافی حذف شده چون Vuetify بیشتر نیازها رو پوشش می‌ده */
.custom-header {
background-color: #f5f5f5 !important;
font-weight: bold !important;
}
.v-data-table {
border-radius: 8px;
overflow: hidden;
}
.v-card {
transition: all 0.3s ease;
}
.v-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0,0,0,0.1) !important;
}
.v-autocomplete {
background-color: white;
border-radius: 8px;
}
.v-toolbar {
border-bottom: 1px solid rgba(0,0,0,0.1);
}
.v-list-item {
transition: background-color 0.2s ease;
}
.v-list-item:hover {
background-color: #f5f5f5;
}
</style>

View file

@ -1,270 +1,531 @@
<template>
<div class="block block-content-full ">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1">
<h3 class="block-title text-primary-dark">
<button @click="$router.back()" type="button"
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
پرداختها
</h3>
<div class="block-options">
<router-link to="/acc/persons/send/mod/" class="btn btn-sm btn-primary ms-2">
<span class="fa fa-plus fw-bolder"></span>
<v-container fluid class="pa-0">
<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-spacer></v-spacer>
<v-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/persons/send/mod/"></v-btn>
</template>
</v-tooltip>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="error">
<v-icon>mdi-file-pdf-box</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.export_pdf')" location="bottom" />
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_pdf') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="print(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="print(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-menu>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon color="success">
<v-icon>mdi-file-excel-box</v-icon>
<v-tooltip activator="parent" :text="$t('dialog.export_excel')" location="bottom" />
</v-btn>
</template>
<v-list>
<v-list-subheader color="primary">{{ $t('dialog.export_excel') }}</v-list-subheader>
<v-list-item class="text-dark" :title="$t('dialog.selected')" @click="excellOutput(false)">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-check"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.selected_all')" @click="excellOutput(true)">
<template v-slot:prepend>
<v-icon color="indigo-darken-4" icon="mdi-expand-all"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
<v-tooltip :text="'انتخاب ستون‌ها'" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-text-field
hide-details
color="green"
class="pt-0 rounded-0 mb-0"
density="compact"
:placeholder="$t('dialog.search_txt')"
v-model="searchValue"
type="text"
clearable
>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify"></v-icon>
</template>
</v-tooltip>
</template>
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon size="sm" v-bind="props" icon="" color="primary">
<v-tooltip activator="parent" variant="plain" :text="$t('dialog.filters')" location="bottom" />
<v-icon icon="mdi-filter"></v-icon>
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<v-icon icon="mdi-filter"></v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<v-list-item>
<v-select
class="py-2 my-2"
v-model="dateFilter"
:items="dateFilterOptions"
label="فیلتر تاریخ"
@update:model-value="loadData"
dense
/>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table-server
v-model:items-per-page="itemsPerPage"
v-model:page="page"
:headers="visibleHeaders"
:items="items"
:items-length="totalItems"
:loading="loading"
item-value="code"
class="elevation-1"
:header-props="{ class: 'custom-header' }"
:items-per-page-options="[5, 10, 20, 50]"
items-per-page-text="تعداد سطر در هر صفحه"
@update:options="loadData"
>
<template v-slot:header.select>
<v-checkbox
v-model="selectAll"
@change="toggleSelectAll"
hide-details
density="compact"
></v-checkbox>
</template>
<template v-slot:item.select="{ item }">
<v-checkbox
:model-value="isSelected(item.code)"
@change="toggleSelect(item)"
hide-details
density="compact"
></v-checkbox>
</template>
<template v-slot:item.operation="{ item }">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" icon="mdi-menu" v-bind="props" />
</template>
<v-list>
<v-list-item :to="'/acc/accounting/view/' + item.code">
<template v-slot:prepend>
<v-icon color="green-darken-4" icon="mdi-eye"></v-icon>
</template>
<v-list-item-title>سند حسابداری</v-list-item-title>
</v-list-item>
<v-list-item :to="{ name: 'person_send_mod', params: { id: item.code }}">
<template v-slot:prepend>
<v-icon icon="mdi-pencil"></v-icon>
</template>
<v-list-item-title>ویرایش</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteItem(item.code)">
<template v-slot:prepend>
<v-icon color="error" icon="mdi-delete"></v-icon>
</template>
<v-list-item-title class="text-error">حذف</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</template>
<template v-slot:item.persons="{ item }">
<router-link
v-for="person in item.persons"
:key="person.code"
class="me-2"
:to="'/acc/persons/card/view/' + person.code"
>
{{ person.nikename }}
</router-link>
<div class="dropdown">
<a class="btn btn-sm btn-danger ms-2 dropdown-toggle text-end" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-file-pdf"></i>
</a>
<ul class="dropdown-menu">
<li><a @click.prevent="print(false)" class="dropdown-item" href="#">انتخاب شدهها</a></li>
<li><a @click.prevent="print(true)" class="dropdown-item" href="#">همه موارد</a></li>
</ul>
</div>
<div class="dropdown">
<a class="btn btn-sm btn-success ms-2 dropdown-toggle text-end" href="#" role="button"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="fa fa-file-excel"></i>
</a>
<ul class="dropdown-menu">
<li><a @click.prevent="excellOutput(false)" class="dropdown-item" href="#">انتخاب شدهها</a></li>
<li><a @click.prevent="excellOutput(true)" class="dropdown-item" href="#">همه موارد</a></li>
</ul>
</div>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-12 m-0 p-0">
<div class="mb-1">
<div class="input-group input-group-sm">
<span class="input-group-text"><i class="fa fa-search"></i></span>
<input v-model="searchValue" class="form-control" type="text" placeholder="جست و جو ...">
</div>
</div>
<EasyDataTable table-class-name="customize-table" v-model:items-selected="itemsSelected" show-index
alternating :search-value="searchValue" :headers="headers" :items="items" theme-color="#1d90ff"
header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading">
<template #item-operation="{ code }">
<div class="dropdown-center">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link" data-bs-toggle="dropdown"
id="dropdown-align-center-alt-primary" type="button">
<i class="fa-solid fa-ellipsis"></i>
</button>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end"
style="">
<router-link class="dropdown-item" :to="'/acc/accounting/view/' + code">
<i class="fa fa-file text-success pe-2"></i>
سند حسابداری
</router-link>
<router-link :to="{ name: 'person_send_mod', params: { id: code } }" class="dropdown-item">
<i class="fa fa-edit pe-2"></i>
ویرایش
</router-link>
<button type="button" @click="deleteItem(code)" class="dropdown-item text-danger">
<i class="fa fa-trash pe-2"></i>
حذف
</button>
</div>
</template>
<template v-slot:item.code="{ item }">
<span class="text-left">{{ $filters.formatNumber(item.code) }}</span>
</template>
<template v-slot:item.amount="{ item }">
<span class="text-left">{{ $filters.formatNumber(item.amount) }}</span>
</template>
</v-data-table-server>
<v-card variant="" class="my-4">
<v-card-text>
<v-row>
<v-col cols="12" sm="6">
<div class="d-flex align-center">
<v-icon class="me-2">mdi-format-list-bulleted</v-icon>
<span>مبلغ کل صفحه:</span>
<span class="ms-2">
{{ $filters.formatNumber(sumTotal) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</div>
</template>
<template #item-persons="{ persons }">
<router-link class="me-2" v-for="person in persons" :to="'/acc/persons/card/view/' + person.code">
{{ person.nikename }}
</router-link>
</template>
</EasyDataTable>
<div class="container-fluid p-0 mx-0 my-3">
<a class="block block-rounded block-link-shadow border-start border-success border-3"
href="javascript:void(0)">
<div class="block-content block-content-full block-content-sm bg-body-light">
<div class="row">
<div class="col-sm-6 com-md-6">
<span class="text-dark">
<i class="fa fa-list-dots"></i>
مبلغ کل:
</span>
<span class="text-primary">
{{ $filters.formatNumber(this.sumTotal) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</div>
<div class="col-sm-6 com-md-6">
<span class="text-dark">
<i class="fa fa-list-check"></i>
جمع مبلغ موارد انتخابی:
</span>
<span class="text-primary">
{{ $filters.formatNumber(this.sumSelected) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</div>
</div>
</v-col>
<v-col cols="12" sm="6">
<div class="d-flex align-center">
<v-icon class="me-2">mdi-format-list-checks</v-icon>
<span>جمع مبلغ موارد انتخابی:</span>
<span class="ms-2">
{{ $filters.formatNumber(sumSelected) }}
{{ $filters.getActiveMoney().shortName }}
</span>
</div>
</a>
</div>
</div>
</div>
</div>
</div>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-dialog v-model="showColumnDialog" max-width="400px">
<v-card>
<v-toolbar color="toolbar" title="انتخاب ستون‌‌ها" density="compact">
<v-spacer></v-spacer>
<v-btn
variant="text"
@click="showColumnDialog = false"
type="icon">
<v-icon>mdi-close</v-icon>
<v-tooltip activator="parent" location="bottom">بستن</v-tooltip>
</v-btn>
</v-toolbar>
<v-card-text class="pt-4">
<v-row dense>
<v-col cols="12" v-for="header in customizableHeaders" :key="header.key">
<v-checkbox
v-model="header.visible"
:label="header.title"
@update:model-value="updateColumnVisibility"
hide-details
density="comfortable"
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
</v-container>
</template>
<script>
import axios from "axios";
import Swal from "sweetalert2";
import { ref } from "vue";
import HelpBtn from "../../component/helpBtn.vue";
export default {
name: "list",
components: { HelpBtn },
data: () => {
return {
sumSelected: 0,
sumTotal: 0,
itemsSelected: [],
searchValue: '',
loading: ref(true),
items: [],
headers: [
{ text: "عملیات", value: "operation" },
{ text: "کد", value: "code", sortable: true },
{ text: "اشخاص", value: "persons", sortable: true },
{ text: "تاریخ", value: "date", sortable: true },
{ text: "شرح", value: "des" },
{ text: "مبلغ", value: "amount", sortable: true },
]
}
},
methods: {
loadData() {
axios.post('/api/person/send/list/search')
.then((response) => {
this.items = response.data;
this.items.forEach((item) => {
item.amount = this.$filters.formatNumber(item.amount);
this.sumTotal += parseInt(item.amount.replaceAll(",", ""));
})
this.loading = false;
})
},
deleteItem(code) {
Swal.fire({
text: 'آیا برای این سند مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: `خیر`,
}).then((result) => {
/* Read more about isConfirmed, isDenied below */
if (result.isConfirmed) {
axios.post('/api/accounting/remove', {
'code': code
}
).then((response) => {
if (response.data.result == 1) {
let index = 0;
for (let z = 0; z < this.items.length; z++) {
index++;
if (this.items[z]['code'] == code) {
this.items.splice(index - 1, 1);
}
}
Swal.fire({
text: 'سند با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
});
}
})
}
})
},
excellOutput(AllItems = true) {
if (AllItems) {
axios({
method: 'get',
url: '/api/person/send/list/excel',
responseType: 'arraybuffer',
}).then((response) => {
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
<script setup>
import { ref, onMounted, watch, computed, reactive } from 'vue';
import { useRouter } from 'vue-router';
import axios from 'axios';
import Swal from 'sweetalert2';
import { getApiUrl } from '@/hesabixConfig';
fileLink.href = fileURL;
fileLink.setAttribute('download', 'persons-send-list.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
}
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
axios({
method: 'post',
url: '/api/person/send/list/excel',
responseType: 'arraybuffer',
data: { items: this.itemsSelected }
}).then((response) => {
var FILE = window.URL.createObjectURL(new Blob([response.data]));
var fileURL = window.URL.createObjectURL(new Blob([response.data]));
var fileLink = document.createElement('a');
const router = useRouter();
const loading = ref(false);
const searchValue = ref('');
const page = ref(1);
const itemsPerPage = ref(10);
const totalItems = ref(0);
const items = ref([]);
const dateFilter = ref('all');
const showColumnDialog = ref(false);
const selectAll = ref(false);
const selectedItems = ref([]);
const sumTotal = ref(0);
const sumSelected = ref(0);
fileLink.href = fileURL;
fileLink.setAttribute('download', 'persons-send-list.xlsx');
document.body.appendChild(fileLink);
fileLink.click();
})
}
}
},
print(AllItems = true) {
if (AllItems) {
axios.post('/api/person/send/list/print').then((response) => {
this.printID = response.data.id;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
else {
if (this.itemsSelected.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
}
else {
axios.post('/api/person/send/list/print', { items: this.itemsSelected }).then((response) => {
this.printID = response.data.id;
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer');
})
}
const allHeaders = reactive([
{ title: '', key: 'select', sortable: false, visible: true, customizable: false },
{ title: 'عملیات', key: 'operation', sortable: false, visible: true },
{ title: 'کد', key: 'code', visible: true },
{ title: 'اشخاص', key: 'persons', sortable: true, visible: true },
{ title: 'تاریخ', key: 'date', visible: true },
{ title: 'شرح', key: 'des', visible: true },
{ title: 'مبلغ', key: 'amount', visible: true },
]);
const customizableHeaders = computed(() =>
allHeaders.filter(h => h.customizable !== false && h.key !== 'operation')
);
const visibleHeaders = computed(() =>
allHeaders.filter(h => h.customizable === false || h.visible)
);
const dateFilterOptions = [
{ title: 'همه', value: 'all' },
{ title: 'امروز', value: 'today' },
{ title: 'این هفته', value: 'thisWeek' },
{ title: 'این ماه', value: 'thisMonth' },
];
const loadData = async (options = null) => {
if (loading.value) return;
loading.value = true;
try {
const params = {
page: options?.page || page.value,
itemsPerPage: options?.itemsPerPage || itemsPerPage.value,
search: searchValue.value,
dateFilter: dateFilter.value,
};
const response = await axios.post('/api/person/send/list/search', params);
if (response.data) {
if (Array.isArray(response.data)) {
items.value = response.data;
totalItems.value = response.data.length;
} else if (response.data.items && Array.isArray(response.data.items)) {
items.value = response.data.items;
totalItems.value = response.data.total || response.data.items.length;
}
items.value = items.value.map(item => ({
...item,
code: parseInt(item.code),
amount: parseFloat(item.amount)
}));
sumTotal.value = items.value.reduce((acc, item) => acc + parseFloat(item.amount), 0);
}
},
beforeMount() {
this.loadData();
},
watch: {
itemsSelected: {
handler: function (val, oldVal) {
this.sumSelected = 0;
this.itemsSelected.forEach((item) => {
this.sumSelected += parseInt(item.amount.replaceAll(",", ""))
} catch (error) {
console.error('Error loading data:', error);
items.value = [];
totalItems.value = 0;
} finally {
loading.value = false;
}
};
const deleteItem = async (code) => {
const result = await Swal.fire({
text: 'آیا برای این سند مطمئن هستید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر',
});
if (result.isConfirmed) {
try {
const response = await axios.post('/api/accounting/remove', { code });
if (response.data.result === 1) {
items.value = items.value.filter(item => item.code !== code);
Swal.fire({
text: 'سند با موفقیت حذف شد.',
icon: 'success',
confirmButtonText: 'قبول',
});
},
deep: true
}
} catch (error) {
console.error('Error deleting item:', error);
}
}
}
};
const isSelected = (code) => {
return selectedItems.value.some(item => item.code === code);
};
const toggleSelect = (item) => {
const index = selectedItems.value.findIndex(selected => selected.code === item.code);
if (index === -1) {
selectedItems.value.push(item);
} else {
selectedItems.value.splice(index, 1);
}
selectAll.value = items.value.length > 0 && selectedItems.value.length === items.value.length;
updateSelectedSum();
};
const toggleSelectAll = () => {
if (selectAll.value) {
selectedItems.value = [...items.value];
} else {
selectedItems.value = [];
}
updateSelectedSum();
};
const updateSelectedSum = () => {
if (selectedItems.value.length > 0) {
sumSelected.value = selectedItems.value.reduce((acc, item) =>
acc + parseFloat(item.amount), 0);
} else {
sumSelected.value = 0;
}
};
const print = async (allItems = true) => {
if (!allItems && selectedItems.value.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
return;
}
try {
let response;
if (allItems) {
response = await axios.post('/api/person/send/list/print');
} else {
response = await axios.post('/api/person/send/list/print', {
items: selectedItems.value
});
}
const printID = response.data.id;
window.open(`${getApiUrl()}/front/print/${printID}`, '_blank', 'noreferrer');
} catch (error) {
console.error('Error printing:', error);
Swal.fire({
text: 'خطا در تولید PDF',
icon: 'error',
confirmButtonText: 'قبول'
});
}
};
const excellOutput = async (allItems = true) => {
if (!allItems && selectedItems.value.length === 0) {
Swal.fire({
text: 'هیچ آیتمی انتخاب نشده است.',
icon: 'info',
confirmButtonText: 'قبول'
});
return;
}
try {
let response;
if (allItems) {
response = await axios({
method: 'post',
url: '/api/person/send/list/excel',
responseType: 'arraybuffer'
});
} else {
response = await axios({
method: 'post',
url: '/api/person/send/list/excel',
responseType: 'arraybuffer',
data: { items: selectedItems.value }
});
}
const blob = new Blob([response.data]);
const url = window.URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', 'persons-send-list.xlsx');
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (error) {
console.error('Error generating excel:', error);
Swal.fire({
text: 'خطا در تولید فایل Excel',
icon: 'error',
confirmButtonText: 'قبول'
});
}
};
const updateColumnVisibility = () => {
localStorage.setItem('sendTableColumns', JSON.stringify(allHeaders));
};
const loadColumnSettings = () => {
const savedColumns = localStorage.getItem('sendTableColumns');
if (savedColumns) {
const parsedColumns = JSON.parse(savedColumns);
parsedColumns.forEach(savedHeader => {
const header = allHeaders.find(h => h.key === savedHeader.key);
if (header) {
header.visible = savedHeader.visible;
}
});
}
};
watch([searchValue, page, itemsPerPage, dateFilter], () => {
loadData();
});
watch(page, () => {
selectedItems.value = [];
selectAll.value = false;
});
watch([page, itemsPerPage], () => {
loadData();
}, { deep: true });
onMounted(() => {
loadColumnSettings();
loadData();
});
</script>
<style scoped></style>
<style scoped>
.custom-header {
background-color: #f5f5f5;
}
.v-data-table {
direction: rtl;
}
.text-left {
text-align: left !important;
}
.v-data-table :deep(th) {
font-weight: bold !important;
white-space: nowrap;
}
.v-data-table :deep(td) {
padding: 8px 16px !important;
}
.v-dialog .v-toolbar-title {
font-size: 1rem;
}
.v-dialog .v-card-text {
max-height: 400px;
overflow-y: auto;
}
.v-btn .v-icon {
font-size: 20px;
}
</style>

View file

@ -0,0 +1,134 @@
<script lang="ts">
import {defineComponent} from 'vue'
import {getApiUrl, getSiteName} from '@/hesabixConfig'
export default defineComponent({
name: "intro",
data:()=>{return{
siteName:''
}},
async created(){
this.siteName = await getSiteName();
}
})
</script>
<template>
<main id="main-container pt-0 mt-o">
<!-- Hero -->
<div class="bg-image" style="background-image: url('/u/img/plugins/ghesta/ghesta.jpg');">
<div class="bg-black-75">
<div class="content content-top content-full text-center">
<h1 class="text-white"><i class="fa fa-money-bill-wave"></i></h1>
<h1 class="fw-bold text-white mt-5 mb-3"> افزونه فروش اقساطی </h1>
<h2 class="h3 fw-normal text-white-75 mb-5">مدیریت هوشمند فروش اقساطی و وامهای قسطی با یک افزونه قدرتمند</h2>
<RouterLink to="/acc/plugin-center/view-end/ghesta">
<span class="badge rounded-pill bg-primary fs-base px-3 py-2 me-2 m-1">
<i class="fa fa-shopping-cart me-1"></i> خرید نسخه آزمایشی </span>
</RouterLink>
</div>
</div>
</div>
<!-- END Hero -->
<!-- Page Content -->
<div class="content content-full">
<div class="row justify-content-center">
<div class="col-sm-11 py-2">
<!-- Story -->
<article class="story justify-content-between">
<div class="alert alert-info">
<i class="fa fa-info-circle me-2"></i>
این افزونه در حال توسعه و آزمایشی است. در نسخه اولیه، امکان ثبت و مدیریت فروشهای اقساطی فراهم شده است. خرید شما باعث سرعت بیشتر در توسعه و بهبود قابلیتهای آن میشود.
</div>
<p class="justify-content-between">
فروش اقساطی یکی از روشهای مهم افزایش فروش و جذب مشتری است که نیاز به مدیریت دقیق و منظم دارد. افزونه فروش اقساطی حسابیکس با ارائه قابلیتهای متنوع، مدیریت فروشهای اقساطی، محاسبه اقساط و پیگیری پرداختها را به سادهترین شکل ممکن فراهم میکند.
</p>
<p>
<strong>قابلیتهای نسخه اولیه:</strong>
</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">
<i class="fa fa-check text-success me-2"></i>
ثبت و مدیریت فروشهای اقساطی
</li>
<li class="list-group-item">
<i class="fa fa-check text-success me-2"></i>
محاسبه خودکار اقساط با در نظر گرفتن سود
</li>
<li class="list-group-item">
<i class="fa fa-check text-success me-2"></i>
صدور فاکتور و چکهای اقساطی
</li>
</ul>
<p>
<strong>قابلیتهای در دست توسعه:</strong>
</p>
<ul class="list-group list-group-flush mb-4">
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
سیستم امتیازدهی و اعتبارسنجی مشتریان
</li>
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
مدیریت وامهای قسطی
</li>
<li class="list-group-item">
<i class="fa fa-clock text-warning me-2"></i>
گزارشهای تحلیلی پیشرفته
</li>
</ul>
<p>
این افزونه به صورت کامل با سیستم حسابداری حسابیکس یکپارچه شده و تمامی عملیات مالی مربوط به فروش اقساطی را به صورت خودکار در سیستم حسابداری ثبت میکند. همچنین امکان صدور فاکتور، چک و گزارشهای مالی متنوع را فراهم میکند.
</p>
<p>
با استفاده از این افزونه، مدیریت فروشهای اقساطی به سادهترین شکل ممکن انجام میشود. تمامی اطلاعات در یک سیستم یکپارچه ذخیره میشود و دسترسی به آنها از هر مکان و در هر زمان امکانپذیر است.
</p>
</article>
<!-- END Story -->
</div>
</div>
</div>
<!-- END Page Content -->
</main>
</template>
<style scoped>
.bg-image {
background-size: cover;
background-position: center;
min-height: 400px;
position: relative;
}
.bg-black-75 {
background-color: rgba(0, 0, 0, 0.75);
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
align-items: center;
}
.story {
line-height: 1.8;
font-size: 1.1rem;
}
.alert {
border-radius: 8px;
margin-bottom: 2rem;
}
.list-group-item {
border: none;
padding: 0.75rem 1.25rem;
background-color: transparent;
}
.list-group-item i {
width: 20px;
text-align: center;
}
</style>

View file

@ -0,0 +1,396 @@
<template>
<div class="sticky-container">
<v-toolbar color="toolbar" :title="$t('drawer.ghesta_invoices')">
<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-tooltip :text="$t('dialog.add_new')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/plugins/ghesta/mod/"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-text-field
hide-details
color="green"
class="pt-0 rounded-0 mb-0"
density="compact"
:placeholder="$t('dialog.search_txt')"
v-model="search"
type="text"
clearable
@update:model-value="onSearch"
>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" :text="$t('dialog.search')">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify"></v-icon>
</template>
</v-tooltip>
</template>
<template v-slot:append-inner>
<v-menu :close-on-content-click="false">
<template v-slot:activator="{ props }">
<v-icon size="sm" v-bind="props" icon="" color="primary">
<v-tooltip activator="parent" variant="plain" :text="$t('dialog.filters')" location="bottom" />
<v-icon icon="mdi-filter"></v-icon>
</v-icon>
</template>
<v-list>
<v-list-subheader color="primary">
<v-icon icon="mdi-filter"></v-icon>
{{ $t('dialog.filters') }}
</v-list-subheader>
<v-list-item>
<v-select
class="py-2 my-2"
v-model="dateFilter"
:items="dateFilterOptions"
label="فیلتر تاریخ"
@update:model-value="onSearch"
density="compact"
/>
</v-list-item>
<v-list-item>
<v-select
class="py-2 my-2"
v-model="statusFilter"
:items="statusFilterOptions"
label="وضعیت پرداخت"
@update:model-value="onSearch"
density="compact"
/>
</v-list-item>
</v-list>
</v-menu>
</template>
</v-text-field>
<v-data-table-server
v-model:items-per-page="serverOptions.rowsPerPage"
v-model:page="serverOptions.page"
:headers="headers"
:items="items"
:items-length="total"
:loading="loading"
:no-data-text="$t('table.no_data')"
@update:options="updateServerOptions"
class="elevation-1 data-table-wrapper"
item-value="id"
:max-height="tableHeight"
:header-props="{ class: 'custom-header' }"
multi-sort
>
<!-- ستون ردیف -->
<template v-slot:item.row="{ index }">
{{ (serverOptions.page - 1) * serverOptions.rowsPerPage + index + 1 }}
</template>
<!-- ستون عملیات -->
<template v-slot:item.actions="{ item }">
<v-menu>
<template v-slot:activator="{ props }">
<v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</template>
<v-list>
<v-list-item class="text-dark" :title="$t('dialog.view')" @click="onView(item)">
<template v-slot:prepend>
<v-icon icon="mdi-eye"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.edit')" @click="onEdit(item)">
<template v-slot:prepend>
<v-icon icon="mdi-file-edit"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.payment')" @click="onPayment(item)">
<template v-slot:prepend>
<v-icon icon="mdi-cash"></v-icon>
</template>
</v-list-item>
<v-list-item class="text-dark" :title="$t('dialog.delete')" @click="onDelete(item)">
<template v-slot:prepend>
<v-icon color="deep-orange-accent-4" icon="mdi-trash-can"></v-icon>
</template>
</v-list-item>
</v-list>
</v-menu>
</template>
<!-- ستون شماره فاکتور -->
<template v-slot:item.code="{ item }">
{{ item.code || '-' }}
</template>
<!-- ستون تاریخ اولین قسط -->
<template v-slot:item.firstGhestaDate="{ item }">
{{ item.firstGhestaDate }}
</template>
<!-- ستون مبلغ -->
<template v-slot:item.amount="{ item }">
<span class="text-dark">
{{ formatNumber(item.amount) }}
</span>
</template>
<!-- ستون سود -->
<template v-slot:item.profitAmount="{ item }">
<span class="text-dark">
{{ formatNumber(item.profitAmount) }}
</span>
</template>
<!-- ستون درصد سود -->
<template v-slot:item.profitPercent="{ item }">
{{ item.profitPercent }}%
</template>
<!-- ستون تعداد اقساط -->
<template v-slot:item.count="{ item }">
{{ item.count }}
</template>
<!-- ستون نوع سود -->
<template v-slot:item.profitType="{ item }">
{{ getProfitTypeLabel(item.profitType) }}
</template>
<!-- ستون مشتری -->
<template v-slot:item.person="{ item }">
<router-link v-if="item.person" :to="'/acc/persons/card/view/' + item.person.id">
{{ item.person.nikename }}
</router-link>
<span v-else>-</span>
</template>
</v-data-table-server>
</div>
</template>
<script>
import { defineComponent, ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import axios from 'axios'
import Swal from 'sweetalert2'
import debounce from 'lodash/debounce'
export default defineComponent({
name: 'GhestaList',
setup() {
const router = useRouter()
const loading = ref(false)
const items = ref([])
const total = ref(0)
const search = ref('')
const dateFilter = ref('all')
const statusFilter = ref('all')
const serverOptions = ref({
page: 1,
rowsPerPage: 10,
sortBy: []
})
const headers = [
{ title: 'ردیف', key: 'row', align: 'center', sortable: false },
{ title: 'عملیات', key: 'actions', align: 'center', sortable: false },
{ title: 'فاکتور', key: 'code', align: 'center', sortable: true },
{ title: 'اولین قسط', key: 'firstGhestaDate', align: 'center', sortable: true },
{ title: 'مشتری', key: 'person', align: 'center', sortable: true },
{ title: 'مبلغ', key: 'amount', align: 'center', sortable: true },
{ title: 'سود', key: 'profitAmount', align: 'center', sortable: true },
{ title: 'درصد سود', key: 'profitPercent', align: 'center', sortable: true },
{ title: 'تعداد اقساط', key: 'count', align: 'center', sortable: true },
{ title: 'نوع سود', key: 'profitType', align: 'center' }
]
const dateFilterOptions = [
{ title: 'همه', value: 'all' },
{ title: 'امروز', value: 'today' },
{ title: 'هفته جاری', value: 'week' },
{ title: 'ماه جاری', value: 'month' }
]
const statusFilterOptions = [
{ title: 'همه', value: 'all' },
{ title: 'پرداخت شده', value: 'paid' },
{ title: 'پرداخت نشده', value: 'unpaid' },
{ title: 'نیمه پرداخت', value: 'partial' }
]
const tableHeight = computed(() => window.innerHeight - 200)
const getProfitTypeLabel = (type) => {
const types = {
'simple': 'سود ساده',
'compound': 'سود مرکب',
'yearly': 'سود سالانه',
'monthly': 'سود ماهانه'
}
return types[type] || type
}
const formatDate = (date) => {
// تبدیل تاریخ به فرمت فارسی
return new Date(date).toLocaleDateString('fa-IR')
}
const formatNumber = (number) => {
// تبدیل اعداد به فرمت فارسی
return new Intl.NumberFormat('fa-IR').format(number)
}
const loadData = async () => {
try {
loading.value = true
const response = await axios.post('/api/plugins/ghesta/invoices/search', {
search: search.value,
page: serverOptions.value.page,
perPage: serverOptions.value.rowsPerPage,
dateFilter: dateFilter.value,
statusFilter: statusFilter.value,
sortBy: serverOptions.value.sortBy
})
if (response.data.result === 1) {
items.value = response.data.items
total.value = response.data.total
} else {
Swal.fire({
text: 'خطا در دریافت اطلاعات',
icon: 'error',
confirmButtonText: 'قبول'
})
}
} catch (error) {
console.error('Error loading data:', error)
Swal.fire({
text: 'خطا در دریافت اطلاعات: ' + error.message,
icon: 'error',
confirmButtonText: 'قبول'
})
} finally {
loading.value = false
}
}
const updateServerOptions = (options) => {
serverOptions.value = {
page: options.page,
rowsPerPage: options.itemsPerPage,
sortBy: options.sortBy || []
}
loadData()
}
const onSearch = debounce(() => {
serverOptions.value.page = 1
loadData()
}, 300)
const onEdit = (item) => {
router.push(`/acc/plugins/ghesta/mod/${item.id}`)
}
const onDelete = (item) => {
Swal.fire({
text: 'آیا از حذف این فاکتور اطمینان دارید؟',
showCancelButton: true,
confirmButtonText: 'بله',
cancelButtonText: 'خیر',
icon: 'warning'
}).then((result) => {
if (result.isConfirmed) {
loading.value = true
axios.delete(`/api/plugins/ghesta/invoice/${item.id}`)
.then((response) => {
if (response.data.result === 1) {
Swal.fire({
text: 'فاکتور با موفقیت حذف شد',
icon: 'success',
confirmButtonText: 'قبول'
})
loadData()
} else {
Swal.fire({
text: 'خطا در حذف فاکتور',
icon: 'error',
confirmButtonText: 'قبول'
})
}
})
.catch((error) => {
console.error('Error:', error)
Swal.fire({
text: 'خطا در حذف فاکتور',
icon: 'error',
confirmButtonText: 'قبول'
})
})
.finally(() => {
loading.value = false
})
}
})
}
const onView = (item) => {
router.push(`/acc/plugins/ghesta/view/${item.id}`)
}
const onPayment = (item) => {
router.push(`/acc/plugins/ghesta/payment/${item.id}`)
}
onMounted(() => {
loadData()
})
return {
loading,
items,
total,
search,
dateFilter,
statusFilter,
serverOptions,
headers,
dateFilterOptions,
statusFilterOptions,
tableHeight,
getProfitTypeLabel,
formatDate,
formatNumber,
updateServerOptions,
onSearch,
onEdit,
onDelete,
onView,
onPayment
}
}
})
</script>
<style>
.sticky-container {
height: 100%;
display: flex;
flex-direction: column;
}
.data-table-wrapper {
flex: 1;
overflow: auto;
}
.custom-header {
background-color: #f5f5f5;
font-weight: bold;
}
</style>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,768 @@
<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-spacer></v-spacer>
</v-toolbar>
<div class="pa-0">
<v-card>
<v-card-text v-if="loading">
<v-progress-linear indeterminate color="primary"></v-progress-linear>
</v-card-text>
<v-card-text v-else-if="error">
<v-alert type="error" variant="tonal" class="mb-0">
<template v-slot:prepend>
<v-icon icon="mdi-alert-circle"></v-icon>
</template>
{{ error }}
</v-alert>
</v-card-text>
<v-card-text v-else>
<v-row>
<v-col cols="12" md="4">
<v-card variant="outlined" class="mb-4">
<v-card-title class="text-subtitle-1 font-weight-bold bg-grey-lighten-4">
<v-icon icon="mdi-information-outline" class="ml-2"></v-icon>
اطلاعات اصلی
</v-card-title>
<v-card-text>
<v-list density="compact" class="pa-0">
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-receipt" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">شماره فاکتور</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ invoice.code }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-account" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">مشتری</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ invoice.person?.nikename }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-calendar-clock" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">تعداد اقساط</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ invoice.count }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
<v-card variant="outlined">
<v-card-title class="text-subtitle-1 font-weight-bold bg-grey-lighten-4">
<v-icon icon="mdi-cash-multiple" class="ml-2"></v-icon>
اطلاعات مالی
</v-card-title>
<v-card-text>
<v-list density="compact" class="pa-0">
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-percent" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">درصد سود</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ invoice.profitPercent }}%</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-currency-usd" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">مبلغ سود</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ formatCurrency(invoice.profitAmount) }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-tag-multiple" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">نوع سود</v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ getProfitTypeText(invoice.profitType) }}</v-list-item-subtitle>
</v-list-item>
<v-list-item>
<template v-slot:prepend>
<v-icon icon="mdi-calendar-range" color="primary"></v-icon>
</template>
<v-list-item-title class="text-subtitle-2">جریمه روزانه </v-list-item-title>
<v-list-item-subtitle class="text-body-1 font-weight-medium">{{ invoice.daysPay }}</v-list-item-subtitle>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
<v-col cols="12" md="8">
<v-card variant="outlined">
<v-card-title class="text-subtitle-1 font-weight-bold bg-grey-lighten-4">
<v-icon icon="mdi-format-list-bulleted" class="ml-2"></v-icon>
لیست اقساط
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="invoice.items"
:items-per-page="-1"
class="elevation-0"
density="compact"
hide-default-footer
>
<template v-slot:item.date="{ item }">
<v-chip
size="small"
color="primary"
variant="outlined"
class="font-weight-medium"
>
{{ formatDate(item.date) }}
</v-chip>
</template>
<template v-slot:item.amount="{ item }">
<span class="font-weight-medium">{{ formatCurrency(item.amount) }}</span>
</template>
<template v-slot:item.hesabdariDoc="{ item }">
<v-chip
v-if="item.hesabdariDoc"
color="success"
size="small"
variant="tonal"
class="font-weight-medium"
>
<v-icon start icon="mdi-check-circle" size="small"></v-icon>
{{ item.hesabdariDoc.code }}
</v-chip>
<v-chip
v-else
color="warning"
size="small"
variant="tonal"
class="font-weight-medium"
>
<v-icon start icon="mdi-clock-outline" size="small"></v-icon>
پرداخت نشده
</v-chip>
</template>
<template v-slot:item.actions="{ item }">
<v-btn
v-if="!item.hesabdariDoc"
color="primary"
size="small"
variant="tonal"
@click="openPaymentDialog(item)"
>
<v-icon start icon="mdi-cash" size="small"></v-icon>
ثبت قسط
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</div>
<!-- دیالوگ ثبت قسط -->
<v-dialog
v-model="paymentDialog"
:fullscreen="$vuetify.display.mobile"
:max-width="$vuetify.display.mobile ? '' : '600'"
persistent
:class="$vuetify.display.mobile ? 'mobile-dialog' : ''"
>
<v-card class="d-flex flex-column dialog-card">
<v-toolbar color="toolbar" flat density="compact">
<v-toolbar-title class="text-subtitle-1">
<v-icon color="primary" left>mdi-cash</v-icon>
ثبت قسط
</v-toolbar-title>
<v-spacer></v-spacer>
<v-menu bottom>
<template v-slot:activator="{ props }">
<v-btn icon color="success" v-bind="props" :disabled="loading" density="comfortable">
<v-icon>mdi-plus</v-icon>
<v-tooltip activator="parent" location="bottom">افزودن دریافت</v-tooltip>
</v-btn>
</template>
<v-list density="compact">
<v-list-item @click="addPaymentItem('bank')">
<v-list-item-title>حساب بانکی</v-list-item-title>
</v-list-item>
<v-list-item @click="addPaymentItem('cashdesk')">
<v-list-item-title>صندوق</v-list-item-title>
</v-list-item>
<v-list-item @click="addPaymentItem('salary')">
<v-list-item-title>تنخواه گردان</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<v-btn icon color="primary" @click="submitPayment" :disabled="loading" density="comfortable">
<v-icon>mdi-content-save</v-icon>
<v-tooltip activator="parent" location="bottom">ثبت</v-tooltip>
</v-btn>
<v-btn icon @click="paymentDialog = false" :disabled="loading" density="comfortable">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text class="flex-grow-1 overflow-y-auto pa-2">
<v-container class="pa-0">
<v-row dense>
<v-col cols="12" md="5">
<Hdatepicker v-model="paymentDate" label="تاریخ" density="compact" />
</v-col>
<v-col cols="12" md="7">
<v-text-field v-model="paymentDes" label="شرح" outlined clearable density="compact" class="mb-2"></v-text-field>
</v-col>
</v-row>
<v-row dense>
<v-col cols="12" md="6">
<v-text-field v-model="formattedTotalPays" label="مجموع" readonly outlined density="compact"></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field v-model="formattedRemainingAmount" label="باقی مانده" readonly outlined density="compact"></v-text-field>
</v-col>
</v-row>
<div v-if="paymentItems.length === 0" class="text-center pa-2">
هیچ دریافتی ثبت نشده است.
</div>
<v-row dense>
<v-col v-for="(item, index) in paymentItems" :key="index" cols="12">
<v-card :class="{
'bank-card': item.type === 'bank',
'cashdesk-card': item.type === 'cashdesk',
'salary-card': item.type === 'salary',
'cheque-card': item.type === 'cheque'
}" border="sm" class="mb-2">
<v-card-item class="py-1">
<template v-slot:prepend>
<v-icon :color="item.type === 'bank' ? 'blue' :
item.type === 'cashdesk' ? 'green' :
item.type === 'salary' ? 'orange' : 'purple'">
{{ item.type === 'bank' ? 'mdi-bank' :
item.type === 'cashdesk' ? 'mdi-cash-register' :
item.type === 'salary' ? 'mdi-wallet' : 'mdi-checkbook' }}
</v-icon>
</template>
<template v-slot:append>
<v-btn-group density="compact">
<v-btn variant="text" color="primary" density="comfortable" @click="fillWithTotal(item)">
<v-icon>mdi-cash-100</v-icon>
<v-tooltip activator="parent" location="bottom">کل قسط</v-tooltip>
</v-btn>
<v-btn variant="text" color="error" density="comfortable" @click="deletePaymentItem(index)">
<v-icon>mdi-delete</v-icon>
<v-tooltip activator="parent" location="bottom">حذف</v-tooltip>
</v-btn>
</v-btn-group>
</template>
</v-card-item>
<v-card-text class="pa-2">
<v-row dense>
<v-col cols="12" sm="6">
<v-select
v-if="item.type === 'bank'"
v-model="item.bank"
:items="listBanks"
item-title="name"
return-object
label="بانک"
variant="outlined"
density="compact"
></v-select>
<v-select
v-if="item.type === 'cashdesk'"
v-model="item.cashdesk"
:items="listCashdesks"
item-title="name"
return-object
label="صندوق"
variant="outlined"
density="compact"
></v-select>
<v-select
v-if="item.type === 'salary'"
v-model="item.salary"
:items="listSalarys"
item-title="name"
return-object
label="تنخواه گردان"
variant="outlined"
density="compact"
></v-select>
</v-col>
<v-col cols="12" sm="6">
<Hnumberinput
v-model="item.bd"
label="مبلغ"
placeholder="0"
@update:modelValue="calcPayment"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="item.referral"
label="ارجاع"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field
v-model="item.des"
label="شرح"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-col>
</v-row>
<v-overlay :model-value="loading" contained class="align-center justify-center">
<v-progress-circular indeterminate size="64" color="primary"></v-progress-circular>
</v-overlay>
</v-container>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="successDialog" max-width="400">
<v-card color="success">
<v-card-text class="text-center pa-4">
<v-icon size="large" color="white" class="mb-4">mdi-check-circle</v-icon>
<div class="text-h6 text-white mb-2">ثبت موفق</div>
<div class="text-white">{{ successMessage }}</div>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="errorDialog" max-width="400">
<v-card color="error">
<v-card-text class="text-center pa-4">
<v-icon size="large" color="white" class="mb-4">mdi-alert-circle</v-icon>
<div class="text-h6 text-white mb-2">خطا</div>
<div class="text-white" style="white-space: pre-line">{{ errorMessage }}</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="white" variant="text" @click="errorDialog = false">بستن</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script>
import { formatNumber, formatCurrency } from '@/utils/number'
import { formatDate } from '@/utils/date'
import axios from 'axios'
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
import Hnumberinput from '@/components/forms/Hnumberinput.vue'
export default {
name: 'GhestaView',
components: {
Hdatepicker,
Hnumberinput
},
data() {
return {
loading: true,
error: null,
invoice: {
items: []
},
headers: [
{
title: 'شماره قسط',
key: 'num',
align: 'center',
sortable: true
},
{
title: 'تاریخ',
key: 'date',
align: 'center',
sortable: true
},
{
title: 'مبلغ',
key: 'amount',
align: 'center',
sortable: true
},
{
title: 'سند پرداخت',
key: 'hesabdariDoc',
align: 'center',
sortable: false
},
{
title: 'عملیات',
key: 'actions',
align: 'center',
sortable: false,
width: '120'
}
],
// متغیرهای مربوط به دیالوگ ثبت قسط
paymentDialog: false,
paymentDate: '',
paymentDes: '',
paymentItems: [],
listBanks: [],
listSalarys: [],
listCashdesks: [],
totalPays: 0,
successDialog: false,
successMessage: '',
errorDialog: false,
errorMessage: '',
selectedItem: null
}
},
computed: {
formattedTotalPays() {
return this.formatNumber(this.totalPays) || '۰';
},
formattedRemainingAmount() {
return this.formatNumber(this.selectedItem ? this.selectedItem.amount - this.totalPays : 0) || '۰';
}
},
methods: {
formatNumber,
formatCurrency,
formatDate,
getProfitTypeText(type) {
const types = {
'yearly': 'سالانه',
'monthly': 'ماهانه',
'daily': 'روزانه'
}
return types[type] || type
},
async loadInvoice() {
try {
this.loading = true
this.error = null
const response = await axios.get(`/api/plugins/ghesta/invoices/${this.$route.params.id}`)
this.invoice = response.data
} catch (error) {
this.error = 'خطا در دریافت اطلاعات'
console.error(error)
} finally {
this.loading = false
}
},
// متدهای مربوط به دیالوگ ثبت قسط
async openPaymentDialog(item) {
this.selectedItem = item
this.paymentItems = []
this.totalPays = 0
this.paymentDes = `پرداخت قسط ${item.num} فاکتور ${this.invoice.code} - ${this.invoice.person?.nikename}`
await this.loadPaymentData()
const response = await axios.get('/api/year/get')
this.paymentDate = response.data.now
this.paymentDialog = true
},
async loadPaymentData() {
try {
const [banks, salarys, cashdesks] = await Promise.all([
axios.post('/api/bank/list'),
axios.post('/api/salary/list'),
axios.post('/api/cashdesk/list')
])
this.listBanks = banks.data
this.listSalarys = salarys.data
this.listCashdesks = cashdesks.data
} catch (error) {
this.errorMessage = 'خطا در بارگذاری اطلاعات'
this.errorDialog = true
}
},
addPaymentItem(type) {
let obj = {}
let canAdd = true
const uniqueId = Date.now() + Math.random().toString(36).substr(2, 9)
if (type === 'bank') {
if (this.listBanks.length === 0) {
this.errorMessage = 'ابتدا یک حساب بانکی ایجاد کنید.'
canAdd = false
} else {
obj = { uniqueId, id: '', type: 'bank', bank: null, cashdesk: {}, salary: {}, bs: 0, bd: 0, des: '', table: 5, referral: '' }
}
} else if (type === 'cashdesk') {
if (this.listCashdesks.length === 0) {
this.errorMessage = 'ابتدا یک صندوق ایجاد کنید.'
canAdd = false
} else {
obj = { uniqueId, id: '', type: 'cashdesk', bank: {}, cashdesk: null, salary: {}, bs: 0, bd: 0, des: '', table: 121, referral: '' }
}
} else if (type === 'salary') {
if (this.listSalarys.length === 0) {
this.errorMessage = 'ابتدا یک تنخواه گردان ایجاد کنید.'
canAdd = false
} else {
obj = { uniqueId, id: '', type: 'salary', bank: {}, cashdesk: {}, salary: null, bs: 0, bd: 0, des: '', table: 122, referral: '' }
}
}
if (canAdd) {
this.paymentItems.push(obj)
this.errorMessage = ''
} else {
this.errorDialog = true
}
},
deletePaymentItem(index) {
this.paymentItems.splice(index, 1)
this.calcPayment()
},
fillWithTotal(item) {
item.bd = this.selectedItem.amount - this.totalPays
this.calcPayment()
},
calcPayment() {
this.totalPays = this.paymentItems.reduce((sum, item) => {
const bd = item.bd !== null && item.bd !== undefined ? Number(item.bd) : 0
return sum + bd
}, 0)
},
async submitPayment() {
let errors = []
if (this.paymentItems.length === 0) {
this.errorMessage = 'هیچ دریافتی ثبت نشده است.';
this.errorDialog = true;
return;
}
if (this.selectedItem.amount < this.totalPays) {
errors.push('مبالغ وارد شده بیشتر از مبلغ قسط است.')
}
this.paymentItems.forEach((element, index) => {
if (element.bd === 0 || element.bd === null || element.bd === undefined) {
errors.push(`مبلغ صفر در ردیف ${index + 1} نا معتبر است.`)
}
if (element.type === 'bank' && (!element.bank || !Object.keys(element.bank).length)) {
errors.push(`بانک در ردیف ${index + 1} انتخاب نشده است.`)
}
if (element.type === 'salary' && (!element.salary || !Object.keys(element.salary).length)) {
errors.push(`تنخواه گردان در ردیف ${index + 1} انتخاب نشده است.`)
}
if (element.type === 'cashdesk' && (!element.cashdesk || !Object.keys(element.cashdesk).length)) {
errors.push(`صندوق در ردیف ${index + 1} انتخاب نشده است.`)
}
})
if (errors.length > 0) {
this.errorMessage = errors.join('\n')
this.errorDialog = true
return
}
this.loading = true
this.errorMessage = ''
const rows = [...this.paymentItems].map(element => {
if (element.type === 'bank') element.id = element.bank.id
else if (element.type === 'salary') element.id = element.salary.id
else if (element.type === 'cashdesk') element.id = element.cashdesk.id
element.des = element.des || this.paymentDes
return element
})
const personRow = {
id: this.invoice.person.id,
type: 'person',
bd: 0,
bs: this.totalPays,
table: 3,
des: this.paymentDes
}
rows.push(personRow)
try {
const response = await axios.post('/api/accounting/insert', {
date: this.paymentDate,
des: this.paymentDes,
type: 'sell_receive',
update: null,
rows,
related: this.invoice.code
})
if (response.data.result === 1) {
this.successMessage = 'پرداخت قسط با موفقیت ثبت شد'
this.successDialog = true
setTimeout(() => {
this.successDialog = false
this.paymentDialog = false
this.loadInvoice() // بارگذاری مجدد اطلاعات
}, 2000)
} else {
this.errorMessage = response.data.msg || 'خطا در ثبت سند'
this.errorDialog = true
}
} catch (error) {
this.errorMessage = error.response?.data?.message || 'خطا در ثبت سند'
this.errorDialog = true
} finally {
this.loading = false
}
}
},
created() {
this.loadInvoice()
}
}
</script>
<style scoped>
.v-card {
border-radius: 8px;
}
.v-list-item {
min-height: 48px;
}
.v-data-table {
border-radius: 8px;
overflow: hidden;
}
.v-chip {
min-width: 100px;
justify-content: center;
}
/* استایل‌های مربوط به دیالوگ ثبت قسط */
.v-card.success {
background-color: #4caf50 !important;
}
.v-card.bank-card {
border-right: 4px solid #1976d2;
}
.v-card.cashdesk-card {
border-right: 4px solid #4caf50;
}
.v-card.salary-card {
border-right: 4px solid #ff9800;
}
.v-card.cheque-card {
border-right: 4px solid #9c27b0;
}
.v-card-item {
background-color: rgba(0, 0, 0, 0.02);
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.v-btn-group .v-btn {
margin: 0 2px;
}
.mobile-dialog {
overflow: hidden !important;
}
.mobile-dialog :deep(.v-card) {
height: 100vh !important;
max-height: 100vh !important;
}
.mobile-dialog :deep(.v-card-text) {
padding: 8px !important;
overflow-y: auto !important;
height: calc(100vh - 64px) !important;
}
.mobile-dialog :deep(.v-overlay__content) {
width: 100% !important;
height: 100% !important;
}
:deep(.v-card-text .container) {
max-width: 100% !important;
}
/* استایل‌های جدید */
.dialog-card {
max-height: 85vh;
display: flex;
flex-direction: column;
}
.dialog-card .v-card-text {
flex: 1;
overflow-y: auto;
padding: 8px;
}
.dialog-card .v-card {
margin-bottom: 8px;
}
.dialog-card .v-card:last-child {
margin-bottom: 0;
}
.dialog-card .v-row {
margin: 0;
}
.dialog-card .v-col {
padding: 4px;
}
.dialog-card .v-text-field,
.dialog-card .v-select {
margin-bottom: 0;
}
.dialog-card .v-card-item {
padding: 4px 8px;
}
.dialog-card .v-card-text {
padding: 8px;
}
.dialog-card .v-btn-group {
display: flex;
gap: 2px;
}
</style>

View file

@ -71,7 +71,7 @@
</td>
<td class="text-center px-2">
<div class="d-flex align-center justify-center">
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;"></Hnumberinput>
<Hnumberinput v-model="item.price" density="compact" @update:modelValue="recalculateTotals" class="my-0" style="font-size: 0.8rem;" :allow-decimal="true"></Hnumberinput>
<v-tooltip v-if="item.name && item.price < item.name.priceBuy" text="قیمت فروش کمتر از قیمت خرید است" location="bottom">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="warning" size="small" class="mr-1">mdi-alert</v-icon>

View file

@ -575,6 +575,32 @@
</v-card>
</v-col>
</v-row>
<v-row v-if="isPluginActive('ghesta')" 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.plugGhestaManager"
label="مدیریت فروش اقساطی"
@change="savePerms('plugGhestaManager')"
hide-details
color="success"
density="comfortable"
:loading="loadingSwitches.plugGhestaManager"
:disabled="loadingSwitches.plugGhestaManager"
></v-switch>
</v-list-item>
</v-list>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-container>
@ -652,7 +678,8 @@ export default {
plugNoghreAdmin: false,
plugNoghreSell: false,
plugCCAdmin: false,
plugHrmDocs: false
plugHrmDocs: false,
plugGhestaManager: false
};
axios.post('/api/business/get/user/permissions',