progress in income and pays and some bug fix
This commit is contained in:
parent
cdbe6e3ae9
commit
72e2065544
|
@ -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);
|
||||
|
|
230
hesabixCore/src/Controller/Componenets/DocsearchController.php
Normal file
230
hesabixCore/src/Controller/Componenets/DocsearchController.php
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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']));
|
||||
}
|
||||
|
||||
|
|
419
hesabixCore/src/Controller/Plugins/PlugGhestaController.php
Normal file
419
hesabixCore/src/Controller/Plugins/PlugGhestaController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
217
hesabixCore/src/Entity/PlugGhestaDoc.php
Normal file
217
hesabixCore/src/Entity/PlugGhestaDoc.php
Normal 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;
|
||||
}
|
||||
}
|
96
hesabixCore/src/Entity/PlugGhestaItem.php
Normal file
96
hesabixCore/src/Entity/PlugGhestaItem.php
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
43
hesabixCore/src/Repository/PlugGhestaDocRepository.php
Normal file
43
hesabixCore/src/Repository/PlugGhestaDocRepository.php
Normal 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()
|
||||
// ;
|
||||
// }
|
||||
}
|
43
hesabixCore/src/Repository/PlugGhestaItemRepository.php
Normal file
43
hesabixCore/src/Repository/PlugGhestaItemRepository.php
Normal 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()
|
||||
// ;
|
||||
// }
|
||||
}
|
BIN
webUI/public/img/plugins/ghesta.png
Normal file
BIN
webUI/public/img/plugins/ghesta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 MiB |
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
346
webUI/src/components/forms/Hdocsearch.vue
Normal file
346
webUI/src/components/forms/Hdocsearch.vue
Normal 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>
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
71
webUI/src/utils/date.js
Normal 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
19
webUI/src/utils/number.js
Normal 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}`
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
134
webUI/src/views/acc/plugins/ghesta/intro.vue
Normal file
134
webUI/src/views/acc/plugins/ghesta/intro.vue
Normal 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>
|
396
webUI/src/views/acc/plugins/ghesta/list.vue
Normal file
396
webUI/src/views/acc/plugins/ghesta/list.vue
Normal 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>
|
1184
webUI/src/views/acc/plugins/ghesta/mod.vue
Normal file
1184
webUI/src/views/acc/plugins/ghesta/mod.vue
Normal file
File diff suppressed because it is too large
Load diff
768
webUI/src/views/acc/plugins/ghesta/view.vue
Normal file
768
webUI/src/views/acc/plugins/ghesta/view.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue