From fb251af7131b551e193d5393aed7f03d66c76727 Mon Sep 17 00:00:00 2001 From: Babak Alizadeh Date: Wed, 3 Sep 2025 00:48:42 +0330 Subject: [PATCH] redesign rfsell part --- .../src/Controller/RfsellController.php | 423 +++++-- .../templates/pdf/printers/rfsell.html.twig | 4 +- webUI/src/i18n/fa_lang.ts | 1 + webUI/src/views/acc/rfsell/list.vue | 1012 ++++++++++------- 4 files changed, 950 insertions(+), 490 deletions(-) diff --git a/hesabixCore/src/Controller/RfsellController.php b/hesabixCore/src/Controller/RfsellController.php index dade173..4d9cdf8 100644 --- a/hesabixCore/src/Controller/RfsellController.php +++ b/hesabixCore/src/Controller/RfsellController.php @@ -18,6 +18,7 @@ use App\Entity\CustomInvoiceTemplate; use App\Service\CustomInvoice\TemplateRenderer; use App\Entity\StoreroomTicket; use App\Service\Printers; +use App\Service\Jdate; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -279,70 +280,271 @@ class RfsellController extends AbstractController return $this->json($extractor->operationSuccess()); } - #[Route('/api/rfsell/docs/search', name: 'app_rfsell_docs_search')] - public function app_rfsell_docs_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse - { + #[Route('/api/rfsell/docs/search', name: 'app_rfsell_docs_search', methods: ['POST'])] + public function searchRfsellDocs( + Provider $provider, + Request $request, + Access $access, + Log $log, + EntityManagerInterface $entityManager, + Jdate $jdate + ): JsonResponse { $acc = $access->hasRole('plugAccproRfsell'); - if (!$acc) + if (!$acc) { throw $this->createAccessDeniedException(); - - $params = []; - if ($content = $request->getContent()) { - $params = json_decode($content, true); } - $data = $entityManager->getRepository(HesabdariDoc::class)->findBy([ - 'bid' => $acc['bid'], - 'year' => $acc['year'], - 'type' => 'rfsell', - 'money'=> $acc['money'] - ], [ - 'id' => 'DESC' - ]); - $dataTemp = []; - foreach ($data as $item) { - $temp = [ - 'id' => $item->getId(), - 'dateSubmit' => $item->getDateSubmit(), - 'date' => $item->getDate(), - 'type' => $item->getType(), - 'code' => $item->getCode(), - 'des' => $item->getDes(), - 'amount' => $item->getAmount(), - 'submitter' => $item->getSubmitter()->getFullName(), - ]; - $mainRow = $entityManager->getRepository(HesabdariRow::class)->getNotEqual($item, 'person'); - $temp['person'] = ''; - if ($mainRow) - $temp['person'] = Explore::ExplorePerson($mainRow->getPerson()); + $params = json_decode($request->getContent(), true) ?? []; + $searchTerm = $params['search'] ?? ''; + $page = max(1, $params['page'] ?? 1); + $perPage = max(1, min(100, $params['perPage'] ?? 10)); + $types = $params['types'] ?? []; + $dateFilter = $params['dateFilter'] ?? 'all'; + $sortBy = $params['sortBy'] ?? []; - $temp['label'] = null; - if ($item->getInvoiceLabel()) { - $temp['label'] = [ - 'code' => $item->getInvoiceLabel()->getCode(), - 'label' => $item->getInvoiceLabel()->getLabel() - ]; - } + $queryBuilder = $entityManager->createQueryBuilder() + ->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount') + ->addSelect('d.isPreview, d.isApproved') + ->addSelect('u.fullName as submitter') + ->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail') + ->addSelect('l.code as labelCode, l.label as labelLabel') + ->from(HesabdariDoc::class, 'd') + ->leftJoin('d.submitter', 'u') + ->leftJoin('d.approvedBy', 'approver') + ->leftJoin('d.InvoiceLabel', 'l') + ->leftJoin('d.hesabdariRows', 'r') + ->where('d.bid = :bid') + ->andWhere('d.year = :year') + ->andWhere('d.type = :type') + ->andWhere('d.money = :money') + ->setParameter('bid', $acc['bid']) + ->setParameter('year', $acc['year']) + ->setParameter('type', 'rfsell') + ->setParameter('money', $acc['money']); - $temp['relatedDocsCount'] = count($item->getRelatedDocs()); - $pays = 0; - foreach ($item->getRelatedDocs() as $relatedDoc) { - $pays += $relatedDoc->getAmount(); - } - $temp['relatedDocsPays'] = $pays; + // اعمال فیلترهای تاریخ + $today = $jdate->jdate('Y/m/d', time()); + if ($dateFilter === 'today') { + $queryBuilder->andWhere('d.date = :today') + ->setParameter('today', $today); + } elseif ($dateFilter === 'week') { + $weekStart = $jdate->jdate('Y/m/d', strtotime('-6 days')); + $queryBuilder->andWhere('d.date BETWEEN :weekStart AND :today') + ->setParameter('weekStart', $weekStart) + ->setParameter('today', $today); + } elseif ($dateFilter === 'month') { + $monthStart = $jdate->jdate('Y/m/01', time()); + $queryBuilder->andWhere('d.date BETWEEN :monthStart AND :today') + ->setParameter('monthStart', $monthStart) + ->setParameter('today', $today); + } - foreach ($item->getHesabdariRows() as $item) { - if ($item->getRef()->getCode() == '104') { - $temp['discountAll'] = $item->getBd(); - } elseif ($item->getRef()->getCode() == '90') { - $temp['transferCost'] = $item->getBs(); + if ($searchTerm) { + $queryBuilder->leftJoin('r.person', 'p') + ->andWhere( + $queryBuilder->expr()->orX( + 'd.code LIKE :search', + 'd.des LIKE :search', + 'd.date LIKE :search', + 'd.amount LIKE :search', + 'p.nikename LIKE :search', + 'p.mobile LIKE :search' + ) + ) + ->setParameter('search', "%$searchTerm%"); + } + + if (!empty($types)) { + $queryBuilder->andWhere('l.code IN (:types)') + ->setParameter('types', $types); + } + + // فیلدهای معتبر برای مرتب‌سازی توی دیتابیس + $validDbFields = [ + 'id' => 'd.id', + 'dateSubmit' => 'd.dateSubmit', + 'date' => 'd.date', + 'type' => 'd.type', + 'code' => 'd.code', + 'des' => 'd.des', + 'amount' => 'd.amount', + 'mdate' => 'd.mdate', + 'plugin' => 'd.plugin', + 'refData' => 'd.refData', + 'shortlink' => 'd.shortlink', + 'isPreview' => 'd.isPreview', + 'isApproved' => 'd.isApproved', + 'approvedBy' => 'd.approvedBy', + 'submitter' => 'u.fullName', + 'label' => 'l.label', + ]; + + // اعمال مرتب‌سازی توی دیتابیس + if (!empty($sortBy)) { + foreach ($sortBy as $sort) { + $key = $sort['key'] ?? 'id'; + $direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? 'DESC' : 'ASC'; + if ($key === 'receivedAmount') { + continue; // این توی PHP مرتب می‌شه + } elseif (isset($validDbFields[$key])) { + $queryBuilder->addOrderBy($validDbFields[$key], $direction); } } - if(!array_key_exists('discountAll',$temp)) $temp['discountAll'] = 0; - if(!array_key_exists('transferCost',$temp)) $temp['transferCost'] = 0; - $dataTemp[] = $temp; + } else { + $queryBuilder->orderBy('d.id', 'DESC'); } - return $this->json($dataTemp); + + $totalItemsQuery = clone $queryBuilder; + $totalItems = $totalItemsQuery->select('COUNT(DISTINCT d.id)') + ->getQuery() + ->getSingleScalarResult(); + + $queryBuilder->setFirstResult(($page - 1) * $perPage) + ->setMaxResults($perPage); + + $docs = $queryBuilder->getQuery()->getArrayResult(); + + $dataTemp = []; + foreach ($docs as $doc) { + $item = [ + 'id' => $doc['id'], + 'dateSubmit' => $doc['dateSubmit'], + 'date' => $doc['date'], + 'type' => $doc['type'], + 'code' => $doc['code'], + 'des' => $doc['des'], + 'amount' => $doc['amount'], + 'submitter' => $doc['submitter'], + 'label' => $doc['labelCode'] ? [ + 'code' => $doc['labelCode'], + 'label' => $doc['labelLabel'] + ] : null, + 'isPreview' => $doc['isPreview'], + 'isApproved' => $doc['isApproved'], + 'approvedBy' => $doc['approvedByName'] ? [ + 'fullName' => $doc['approvedByName'], + 'id' => $doc['approvedById'], + 'email' => $doc['approvedByEmail'] + ] : null, + ]; + + $mainRow = $entityManager->getRepository(HesabdariRow::class) + ->createQueryBuilder('r') + ->where('r.doc = :docId') + ->andWhere('r.person IS NOT NULL') + ->setParameter('docId', $doc['id']) + ->setMaxResults(1) + ->getQuery() + ->getOneOrNullResult(); + $item['person'] = $mainRow && $mainRow->getPerson() ? [ + 'id' => $mainRow->getPerson()->getId(), + 'nikename' => $mainRow->getPerson()->getNikename(), + 'code' => $mainRow->getPerson()->getCode() + ] : null; + + // استفاده از SQL خام برای محاسبه پرداختی‌ها + $sql = " + SELECT SUM(rd.amount) as total_pays, COUNT(rd.id) as count_docs + FROM hesabdari_doc rd + JOIN hesabdari_doc_hesabdari_doc rel ON rel.hesabdari_doc_target = rd.id + WHERE rel.hesabdari_doc_source = :sourceDocId + AND rd.bid_id = :bidId + "; + $stmt = $entityManager->getConnection()->prepare($sql); + $stmt->bindValue('sourceDocId', $doc['id']); + $stmt->bindValue('bidId', $acc['bid']->getId()); + $result = $stmt->executeQuery()->fetchAssociative(); + + $relatedDocsPays = $result['total_pays'] ?? 0; + $relatedDocsCount = $result['count_docs'] ?? 0; + + $item['relatedDocsCount'] = (int) $relatedDocsCount; + $item['relatedDocsPays'] = $relatedDocsPays; + $item['discountAll'] = 0; + $item['transferCost'] = 0; + + $rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]); + foreach ($rows as $row) { + if ($row->getRef()->getCode() == '104') { + $item['discountAll'] = $row->getBd(); + } elseif ($row->getRef()->getCode() == '90') { + $item['transferCost'] = $row->getBd(); + } + } + + $dataTemp[] = $item; + } + + // مرتب‌سازی توی PHP برای receivedAmount + if (!empty($sortBy)) { + foreach ($sortBy as $sort) { + $key = $sort['key'] ?? 'id'; + $direction = isset($sort['order']) && strtoupper($sort['order']) === 'DESC' ? SORT_DESC : SORT_ASC; + if ($key === 'receivedAmount') { + usort($dataTemp, function ($a, $b) use ($direction) { + return $direction === SORT_ASC ? $a['relatedDocsPays'] - $b['relatedDocsPays'] : $b['relatedDocsPays'] - $a['relatedDocsPays']; + }); + } + } + } + + return $this->json([ + 'items' => $dataTemp, + 'total' => (int) $totalItems, + 'page' => $page, + 'perPage' => $perPage, + ]); + } + + #[Route('/api/rfsell/rows/{code}', name: 'app_rfsell_rows', methods: ['GET'])] + public function getRfsellRows( + Request $request, + Access $access, + EntityManagerInterface $entityManager, + string $code, + Log $log + ): JsonResponse { + $acc = $access->hasRole('plugAccproRfsell'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ + 'bid' => $acc['bid'], + 'code' => $code, + 'money' => $acc['money'], + ]); + + if (!$doc) { + $log->insert('RfsellController', 'Doc not found for code: ' . $code, $this->getUser(), $acc['bid']->getId()); + throw $this->createNotFoundException(); + } + + $rows = $entityManager->getRepository(HesabdariRow::class)->findBy(['doc' => $doc]); + + $data = array_map(function ($row) use ($log) { + try { + return [ + 'id' => $row->getId(), + 'des' => $row->getDes(), + 'bs' => $row->getBs(), + 'bd' => $row->getBd(), + 'commdityCount' => $row->getCommdityCount(), + 'commodity' => $row->getCommodity() ? [ + 'id' => $row->getCommodity()->getId(), + 'name' => $row->getCommodity()->getName(), + ] : null, + ]; + } catch (\Exception $e) { + $log->insert('RfsellController', 'Error processing row: ' . $e->getMessage(), $this->getUser(), null); + return null; + } + }, $rows); + + // فیلتر کردن موارد null + $data = array_filter($data); + + return $this->json(['rows' => array_values($data)]); } #[Route('/api/rfsell/posprinter/invoice', name: 'app_rfsell_posprinter_invoice')] @@ -412,20 +614,38 @@ class RfsellController extends AbstractController #[Route('/api/rfsell/print/invoice', name: 'app_rfsell_print_invoice')] public function app_rfsell_print_invoice(Printers $printers, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, TemplateRenderer $renderer): JsonResponse { - $params = []; - if ($content = $request->getContent()) { - $params = json_decode($content, true); - } - $acc = $access->hasRole('plugAccproRfsell'); - if (!$acc) throw $this->createAccessDeniedException(); + if (!$acc) + throw $this->createAccessDeniedException(); + + $params = json_decode($request->getContent(), true); + $params['printers'] = $params['printers'] ?? false; + $params['pdf'] = $params['pdf'] ?? true; + $params['posPrint'] = $params['posPrint'] ?? false; + + // دریافت تنظیمات پیش‌فرض از PrintOptions + $printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid' => $acc['bid']]); + + // تنظیم مقادیر پیش‌فرض از تنظیمات ذخیره شده + $defaultOptions = [ + 'note' => $printSettings ? $printSettings->isRfsellNote() : true, + 'bidInfo' => $printSettings ? $printSettings->isRfsellBidInfo() : true, + 'taxInfo' => $printSettings ? $printSettings->isRfsellTaxInfo() : true, + 'discountInfo' => $printSettings ? $printSettings->isRfsellDiscountInfo() : true, + 'pays' => $printSettings ? $printSettings->isRfsellPays() : true, + 'paper' => $printSettings ? $printSettings->getRfsellPaper() : 'A4-L', + ]; + + // اولویت با پارامترهای ارسالی است + $printOptions = array_merge($defaultOptions, $params['printOptions'] ?? []); $doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([ 'bid' => $acc['bid'], 'code' => $params['code'], - 'money'=> $acc['money'] + 'money' => $acc['money'] ]); - if (!$doc) throw $this->createNotFoundException(); + if (!$doc) + throw $this->createNotFoundException(); $person = null; $discount = 0; $transfer = 0; @@ -439,50 +659,28 @@ class RfsellController extends AbstractController } } $pdfPid = 0; - if ($params['pdf']) { - $printOptions = [ - 'bidInfo' => true, - 'pays' =>true, - 'taxInfo' =>true, - 'discountInfo' =>true, - 'note' =>true, - 'paper' =>'A4-L' - ]; - if(array_key_exists('printOptions',$params)){ - if(array_key_exists('bidInfo',$params['printOptions'])){ - $printOptions['bidInfo'] = $params['printOptions']['bidInfo']; - } - if(array_key_exists('pays',$params['printOptions'])){ - $printOptions['pays'] = $params['printOptions']['pays']; - } - if(array_key_exists('taxInfo',$params['printOptions'])){ - $printOptions['taxInfo'] = $params['printOptions']['taxInfo']; - } - if(array_key_exists('discountInfo',$params['printOptions'])){ - $printOptions['discountInfo'] = $params['printOptions']['discountInfo']; - } - if(array_key_exists('note',$params['printOptions'])){ - $printOptions['note'] = $params['printOptions']['note']; - } - if(array_key_exists('paper',$params['printOptions'])){ - $printOptions['paper'] = $params['printOptions']['paper']; - } - } + + if ($params['pdf'] == true || $params['printers'] == true) { $note = ''; - $printSettings = $entityManager->getRepository(PrintOptions::class)->findOneBy(['bid'=>$acc['bid']]); - if($printSettings){$note = $printSettings->getRfsellNoteString();} - // Build safe context + if ($printSettings) { + $note = $printSettings->getRfsellNoteString(); + } + + // Build safe context data for rendering $rowsArr = array_map(function ($row) { return [ 'commodity' => $row->getCommodity() ? [ 'name' => method_exists($row->getCommodity(), 'getName') ? $row->getCommodity()->getName() : null, 'code' => method_exists($row->getCommodity(), 'getCode') ? $row->getCommodity()->getCode() : null, ] : null, - 'commodityCount' => $row->getCommdityCount(), + 'commdityCount' => $row->getCommdityCount(), 'des' => $row->getDes(), 'bs' => $row->getBs(), + 'bd' => $row->getBd(), 'tax' => $row->getTax(), 'discount' => $row->getDiscount(), + 'showPercentDiscount' => $row->getDiscountType() === 'percent', + 'discountPercent' => $row->getDiscountPercent() ]; }, $doc->getHesabdariRows()->toArray()); @@ -501,50 +699,65 @@ class RfsellController extends AbstractController 'address' => method_exists($biz, 'getAddress') ? $biz->getAddress() : null, 'shenasemeli' => method_exists($biz, 'getShenasemeli') ? $biz->getShenasemeli() : null, 'codeeghtesadi' => method_exists($biz, 'getCodeeghtesadi') ? $biz->getCodeeghtesadi() : null, + 'id' => method_exists($biz, 'getId') ? $biz->getId() : null, ] : null; $context = [ 'business' => $businessArr, + 'bid' => $businessArr, 'doc' => [ 'code' => $doc->getCode(), 'date' => method_exists($doc, 'getDate') ? $doc->getDate() : null, + 'taxPercent' => method_exists($doc, 'getTaxPercent') ? $doc->getTaxPercent() : null, + 'discountPercent' => $doc->getDiscountPercent(), + 'discountType' => $doc->getDiscountType(), + 'amount' => $doc->getAmount(), + 'money' => [ + 'shortName' => method_exists($doc, 'getMoney') && $doc->getMoney() && method_exists($doc->getMoney(), 'getShortName') ? $doc->getMoney()->getShortName() : null, + ], ], 'rows' => $rowsArr, 'person' => $personArr, 'discount' => $discount, 'transfer' => $transfer, - 'printOptions'=> $printOptions, - 'note'=> $note + 'printOptions' => $printOptions, + 'note' => $note, ]; + // Decide template: custom or default $html = null; $selectedTemplate = $printSettings ? $printSettings->getRfsellTemplate() : null; + if ($selectedTemplate instanceof CustomInvoiceTemplate) { $html = $renderer->render($selectedTemplate->getCode() ?? '', $context); } + if ($html === null) { + // fallback to default Twig template $html = $this->renderView('pdf/printers/rfsell.html.twig', [ 'bid' => $acc['bid'], 'doc' => $doc, 'rows' => array_map(function ($row) { return [ 'commodity' => $row->getCommodity(), - 'commodityCount' => $row->getCommdityCount(), 'commdityCount' => $row->getCommdityCount(), 'des' => $row->getDes(), 'bs' => $row->getBs(), + 'bd' => $row->getBd(), 'tax' => $row->getTax(), 'discount' => $row->getDiscount(), 'showPercentDiscount' => $row->getDiscountType() === 'percent', - 'discountPercent' => $row->getDiscountPercent(), + 'discountPercent' => $row->getDiscountPercent() ]; }, $doc->getHesabdariRows()->toArray()), 'person' => $person, 'printInvoice' => $params['printers'], 'discount' => $discount, 'transfer' => $transfer, - 'printOptions'=> $printOptions, - 'note'=> $note + 'printOptions' => $printOptions, + 'note' => $note, + 'showPercentDiscount' => $doc->getDiscountType() === 'percent', + 'discountPercent' => $doc->getDiscountPercent() ]); } @@ -556,7 +769,7 @@ class RfsellController extends AbstractController $printOptions['paper'] ); } - if ($params['printers'] == true) { + if ($params['posPrint'] == true) { $pid = $provider->createPrint( $acc['bid'], $this->getUser(), @@ -566,18 +779,18 @@ class RfsellController extends AbstractController 'rows' => array_map(function ($row) { return [ 'commodity' => $row->getCommodity(), - 'commodityCount' => $row->getCommdityCount(), 'commdityCount' => $row->getCommdityCount(), 'des' => $row->getDes(), 'bs' => $row->getBs(), 'tax' => $row->getTax(), 'discount' => $row->getDiscount(), 'showPercentDiscount' => $row->getDiscountType() === 'percent', - 'discountPercent' => $row->getDiscountPercent(), + 'discountPercent' => $row->getDiscountPercent() ]; }, $doc->getHesabdariRows()->toArray()), 'discount' => $discount, - 'transfer' => $transfer, + 'showPercentDiscount' => $doc->getDiscountType() === 'percent', + 'discountPercent' => $doc->getDiscountPercent() ]), false ); diff --git a/hesabixCore/templates/pdf/printers/rfsell.html.twig b/hesabixCore/templates/pdf/printers/rfsell.html.twig index ea00afb..5b6cbfc 100644 --- a/hesabixCore/templates/pdf/printers/rfsell.html.twig +++ b/hesabixCore/templates/pdf/printers/rfsell.html.twig @@ -214,14 +214,14 @@ {{ item.commdityCount }} {{ item.commodity.unit.name }} - {{ ((item.bs - item.tax + item.discount) / item.commdityCount) | number_format }} + {{ ((item.bd - item.tax + item.discount) / item.commdityCount) | number_format }} {% if printOptions.discountInfo %} {{ item.discount | number_format }} {% endif %} {% if printOptions.taxInfo %} {{ item.tax | number_format}} {% endif %} - {{ item.bs| number_format }} + {{ item.bd| number_format }} {% endif %} {% endfor %} diff --git a/webUI/src/i18n/fa_lang.ts b/webUI/src/i18n/fa_lang.ts index 2ddf20a..16acd25 100755 --- a/webUI/src/i18n/fa_lang.ts +++ b/webUI/src/i18n/fa_lang.ts @@ -164,6 +164,7 @@ const fa_lang = { sell_invoices: " فروش", rfbuy_invoices: " برگشت از خرید", rfsell_invoices: " برگشت از فروش", + return_sell_invoices_long: "فاکتورهای برگشت از فروش", costs: "هزینه‌ها", incomes: "درآمد‌ها", fast_sell: "فاکتور سریع", diff --git a/webUI/src/views/acc/rfsell/list.vue b/webUI/src/views/acc/rfsell/list.vue index fc98844..77541e7 100644 --- a/webUI/src/views/acc/rfsell/list.vue +++ b/webUI/src/views/acc/rfsell/list.vue @@ -1,244 +1,325 @@