almost finish direc document insert and redesign storeroom part

This commit is contained in:
Hesabix 2025-04-26 15:21:32 +00:00
parent 04550d2171
commit c1dc79da52
28 changed files with 6621 additions and 2578 deletions

View file

@ -59,6 +59,58 @@ class BankController extends AbstractController
return $this->json($provider->ArrayEntity2Array($datas, 0)); return $this->json($provider->ArrayEntity2Array($datas, 0));
} }
#[Route('/api/bank/search', name: 'app_bank_search')]
public function app_bank_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('banks');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$query = $entityManager->createQueryBuilder()
->select('b')
->from(BankAccount::class, 'b')
->where('b.bid = :bid')
->andWhere('b.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money']);
if (isset($params['search']) && !empty($params['search'])) {
$query->andWhere('b.name LIKE :search')
->setParameter('search', '%' . $params['search'] . '%');
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$datas = $query->getQuery()->getResult();
// محاسبه موجودی برای هر حساب
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $data
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$data->setBalance($bd - $bs);
}
return $this->json([
'items' => $provider->ArrayEntity2Array($datas, 0),
'total' => count($datas)
]);
}
#[Route('/api/bank/info/{code}', name: 'app_bank_info')] #[Route('/api/bank/info/{code}', name: 'app_bank_info')]
public function app_bank_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse public function app_bank_info($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{ {
@ -147,6 +199,88 @@ class BankController extends AbstractController
return $this->json(['result' => 1]); return $this->json(['result' => 1]);
} }
#[Route('/api/bank/balance/{code}', name: 'app_bank_balance')]
public function app_bank_balance($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('banks');
if (!$acc)
throw $this->createAccessDeniedException();
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$bank)
throw $this->createNotFoundException();
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'bank' => $bank
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
return $this->json([
'balance' => $bd - $bs,
'debit' => $bd,
'credit' => $bs
]);
}
#[Route('/api/bank/transactions/{code}', name: 'app_bank_transactions')]
public function app_bank_transactions($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('banks');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$bank = $entityManager->getRepository(BankAccount::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$bank)
throw $this->createNotFoundException();
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->where('r.bank = :bank')
->andWhere('r.bid = :bid')
->setParameter('bank', $bank)
->setParameter('bid', $acc['bid']);
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
->setParameter('endDate', $params['endDate']);
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$transactions = $query->getQuery()->getResult();
return $this->json([
'items' => $provider->ArrayEntity2Array($transactions, 0),
'total' => count($transactions)
]);
}
/** /**
* @throws Exception * @throws Exception
*/ */

View file

@ -135,4 +135,137 @@ class CashdeskController extends AbstractController
$log->insert('بانکداری', ' صندوق با نام ' . $name . ' حذف شد. ', $this->getUser(), $acc['bid']->getId()); $log->insert('بانکداری', ' صندوق با نام ' . $name . ' حذف شد. ', $this->getUser(), $acc['bid']->getId());
return $this->json(['result' => 1]); return $this->json(['result' => 1]);
} }
#[Route('/api/cashdesk/search', name: 'app_cashdesk_search')]
public function app_cashdesk_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$query = $entityManager->createQueryBuilder()
->select('c')
->from(Cashdesk::class, 'c')
->where('c.bid = :bid')
->andWhere('c.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money']);
if (isset($params['search']) && !empty($params['search'])) {
$query->andWhere('c.name LIKE :search')
->setParameter('search', '%' . $params['search'] . '%');
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$datas = $query->getQuery()->getResult();
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $data
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$data->setBalance($bd - $bs);
}
return $this->json([
'items' => $provider->ArrayEntity2Array($datas, 0),
'total' => count($datas)
]);
}
#[Route('/api/cashdesk/balance/{code}', name: 'app_cashdesk_balance')]
public function app_cashdesk_balance($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$cashdesk)
throw $this->createNotFoundException();
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'cashdesk' => $cashdesk
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
return $this->json([
'balance' => $bd - $bs,
'debit' => $bd,
'credit' => $bs
]);
}
#[Route('/api/cashdesk/transactions/{code}', name: 'app_cashdesk_transactions')]
public function app_cashdesk_transactions($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('cashdesk');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$cashdesk = $entityManager->getRepository(Cashdesk::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$cashdesk)
throw $this->createNotFoundException();
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->where('r.cashdesk = :cashdesk')
->andWhere('r.bid = :bid')
->setParameter('cashdesk', $cashdesk)
->setParameter('bid', $acc['bid']);
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
->setParameter('endDate', $params['endDate']);
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$transactions = $query->getQuery()->getResult();
return $this->json([
'items' => $provider->ArrayEntity2Array($transactions, 0),
'total' => count($transactions)
]);
}
} }

View file

@ -0,0 +1,33 @@
<?php
namespace App\Controller\Componenets;
use App\Entity\BankAccount;
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\Response;
use Symfony\Component\Routing\Annotation\Route;
class BankController extends AbstractController
{
#[Route('/api/componenets/bank/get/{id}', name: 'app_componenets_bank_get')]
public function app_componenets_bank_get(Access $access,EntityManagerInterface $entityManager, $id): JsonResponse
{
$bank = $entityManager->getRepository(BankAccount::class)->find($id);
$acc = $access->hasRole('join');
if (!$acc) {
return new JsonResponse(['message' => 'Access denied'], Response::HTTP_FORBIDDEN);
}
if (!$bank) {
return new JsonResponse(['message' => 'Bank not found'], Response::HTTP_NOT_FOUND);
}
if($bank->getBid() != $acc['bid']){
return new JsonResponse(['message' => 'Access denied'], Response::HTTP_FORBIDDEN);
}
return new JsonResponse(Explore::ExploreBank($bank));
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Controller\Componenets;
use App\Entity\Cashdesk;
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\Response;
use Symfony\Component\Routing\Annotation\Route;
class CashdeskController extends AbstractController
{
#[Route('/api/componenets/cashdesk/get/{id}', name: 'app_componenets_cashdesk_get')]
public function app_componenets_cashdesk_get(Access $access,EntityManagerInterface $entityManager, $id): JsonResponse
{
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($id);
$acc = $access->hasRole('join');
if (!$acc) {
return new JsonResponse(['message' => 'Access denied'], Response::HTTP_FORBIDDEN);
}
if (!$cashdesk) {
return new JsonResponse(['message' => 'Cashdesk not found'], Response::HTTP_NOT_FOUND);
}
if($cashdesk->getBid() != $acc['bid']){
return new JsonResponse(['message' => 'Access denied'], Response::HTTP_FORBIDDEN);
}
return new JsonResponse(Explore::ExploreCashdesk($cashdesk));
}
}

View file

@ -0,0 +1,358 @@
<?php
namespace App\Controller;
use App\Entity\BankAccount;
use App\Entity\Cashdesk;
use App\Entity\Commodity;
use App\Entity\HesabdariRow;
use App\Entity\HesabdariTable;
use App\Entity\HesabdariDoc;
use App\Entity\Person;
use App\Entity\Salary;
use App\Service\Access;
use App\Service\Log;
use App\Service\Provider;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;
use Doctrine\ORM\EntityManagerInterface;
class DirectHesabdariDoc extends AbstractController
{
#[Route('/api/hesabdari/direct/doc/create', name: 'create_hesabdari_doc_insert')]
public function create(Log $log, Access $access, Request $request, Provider $provider, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز'], 403);
}
$prams = $request->getPayload()->all();
$hesabdariDoc = new HesabdariDoc();
$hesabdariDoc->setType('calc');
$hesabdariDoc->setBid($acc['bid']);
$hesabdariDoc->setSubmitter($this->getUser());
$hesabdariDoc->setDes($prams['des']);
$hesabdariDoc->setYear($acc['year']);
$hesabdariDoc->setMoney($acc['money']);
$hesabdariDoc->setDate($prams['date']);
$hesabdariDoc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
$hesabdariDoc->setDateSubmit(time());
//insert rows
if (isset($prams['rows'])) {
if (count($prams['rows']) < 2) {
return new JsonResponse(['success' => false, 'message' => 'حداقل باید دو سطر در سند وجود داشته باشد'], 400);
}
$totalBs = 0;
foreach ($prams['rows'] as $row) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDoc($hesabdariDoc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
$hesabdariRow->setDes($row['des']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setRefData($row['detail']);
$hesabdariRow->setBid($acc['bid']);
$totalBs += floatval($row['bs']);
//get ref
$ref = $entityManager->getRepository(HesabdariTable::class)->find($row['ref']);
if ($ref) {
if ($ref->getBid() == $acc['bid'] || $ref->getBid() == null) {
$hesabdariRow->setRef($ref);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حساب'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حساب مورد نظر یافت نشد'], 404);
}
if ($row['bankAccount']) {
$bankAccount = $entityManager->getRepository(BankAccount::class)->find($row['bankAccount']);
if ($bankAccount) {
if ($bankAccount->getBid() == $acc['bid']) {
$hesabdariRow->setBank($bankAccount);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حساب بانکی'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حساب بانکی مورد نظر یافت نشد'], 404);
}
}
if ($row['cashdesk']) {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['cashdesk']);
if ($cashdesk) {
if ($cashdesk->getBid() == $acc['bid']) {
$hesabdariRow->setCashDesk($cashdesk);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به صندوق'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'صندوق مورد نظر یافت نشد'], 404);
}
}
if ($row['salary']) {
$salary = $entityManager->getRepository(Salary::class)->find($row['salary']);
if ($salary) {
if ($salary->getBid() == $acc['bid']) {
$hesabdariRow->setSalary($salary);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حقوق'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حقوق مورد نظر یافت نشد'], 404);
}
}
if ($row['person']) {
$person = $entityManager->getRepository(Person::class)->find($row['person']);
if ($person) {
if ($person->getBid() == $acc['bid']) {
$hesabdariRow->setPerson($person);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به شخص'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'شخص مورد نظر یافت نشد'], 404);
}
}
if ($row['commodity'] && $row['commodityCount']) {
if (!is_numeric($row['commodityCount']) || $row['commodityCount'] <= 0) {
return new JsonResponse(['success' => false, 'message' => 'تعداد کالا باید عددی مثبت باشد'], 400);
}
$commodity = $entityManager->getRepository(Commodity::class)->find($row['commodity']);
if ($commodity) {
if ($commodity->getBid() == $acc['bid']) {
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($row['commodityCount']);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به کالا'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'کالای مورد نظر یافت نشد'], 404);
}
}
$entityManager->persist($hesabdariRow);
}
$hesabdariDoc->setAmount($totalBs);
}
$entityManager->persist($hesabdariDoc);
$entityManager->flush();
$log->insert('حسابداری', 'ایجاد سند حسابداری شماره ' . $hesabdariDoc->getCode(), $this->getUser(), $acc['bid'], $hesabdariDoc);
return new JsonResponse(['success' => true, 'message' => 'سند با موفقیت ثبت شد', 'data' => ['id' => $hesabdariDoc->getId()]], 200);
}
#[Route('/api/hesabdari/direct/doc/update/{id}', name: 'update_hesabdari_doc_update')]
public function update(Log $log, Access $access, Request $request, int $id, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز'], 403);
}
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->find($id);
if (!$hesabdariDoc) {
return new JsonResponse(['success' => false, 'message' => 'سند مورد نظر یافت نشد'], 404);
}
if ($hesabdariDoc->getBid() !== $acc['bid']) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به سند'], 403);
}
$prams = $request->getPayload()->all();
$hesabdariDoc->setDes($prams['des']);
$hesabdariDoc->setDate($prams['date']);
// حذف ردیف‌های قبلی
foreach ($hesabdariDoc->getHesabdariRows() as $row) {
$entityManager->remove($row);
}
// اضافه کردن ردیف‌های جدید
if (isset($prams['rows'])) {
if (count($prams['rows']) < 2) {
return new JsonResponse(['success' => false, 'message' => 'حداقل باید دو سطر در سند وجود داشته باشد'], 400);
}
$totalBs = 0;
foreach ($prams['rows'] as $row) {
$hesabdariRow = new HesabdariRow();
$hesabdariRow->setDoc($hesabdariDoc);
$hesabdariRow->setBs($row['bs']);
$hesabdariRow->setBd($row['bd']);
$hesabdariRow->setDes($row['des']);
$hesabdariRow->setYear($acc['year']);
$hesabdariRow->setRefData($row['detail']);
$hesabdariRow->setBid($acc['bid']);
$totalBs += floatval($row['bs']);
//get ref
$ref = $entityManager->getRepository(HesabdariTable::class)->find($row['ref']);
if ($ref) {
if ($ref->getBid() == $acc['bid'] || $ref->getBid() == null) {
$hesabdariRow->setRef($ref);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حساب'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حساب مورد نظر یافت نشد'], 404);
}
if ($row['bankAccount']) {
$bankAccount = $entityManager->getRepository(BankAccount::class)->find($row['bankAccount']);
if ($bankAccount) {
if ($bankAccount->getBid() == $acc['bid']) {
$hesabdariRow->setBank($bankAccount);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حساب بانکی'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حساب بانکی مورد نظر یافت نشد'], 404);
}
}
if ($row['cashdesk']) {
$cashdesk = $entityManager->getRepository(Cashdesk::class)->find($row['cashdesk']);
if ($cashdesk) {
if ($cashdesk->getBid() == $acc['bid']) {
$hesabdariRow->setCashDesk($cashdesk);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به صندوق'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'صندوق مورد نظر یافت نشد'], 404);
}
}
if ($row['salary']) {
$salary = $entityManager->getRepository(Salary::class)->find($row['salary']);
if ($salary) {
if ($salary->getBid() == $acc['bid']) {
$hesabdariRow->setSalary($salary);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به حقوق'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'حقوق مورد نظر یافت نشد'], 404);
}
}
if ($row['person']) {
$person = $entityManager->getRepository(Person::class)->find($row['person']);
if ($person) {
if ($person->getBid() == $acc['bid']) {
$hesabdariRow->setPerson($person);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به شخص'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'شخص مورد نظر یافت نشد'], 404);
}
}
if ($row['commodity'] && $row['commodityCount']) {
if (!is_numeric($row['commodityCount']) || $row['commodityCount'] <= 0) {
return new JsonResponse(['success' => false, 'message' => 'تعداد کالا باید عددی مثبت باشد'], 400);
}
$commodity = $entityManager->getRepository(Commodity::class)->find($row['commodity']);
if ($commodity) {
if ($commodity->getBid() == $acc['bid']) {
$hesabdariRow->setCommodity($commodity);
$hesabdariRow->setCommdityCount($row['commodityCount']);
} else {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به کالا'], 403);
}
} else {
return new JsonResponse(['success' => false, 'message' => 'کالای مورد نظر یافت نشد'], 404);
}
}
$entityManager->persist($hesabdariRow);
}
$hesabdariDoc->setAmount($totalBs);
}
$entityManager->flush();
$log->insert('حسابداری', 'ویرایش سند حسابداری شماره ' . $hesabdariDoc->getCode(), $this->getUser(), $acc['bid'], $hesabdariDoc);
return new JsonResponse(['success' => true, 'message' => 'سند با موفقیت ویرایش شد'], 200);
}
#[Route('/api/hesabdari/direct/doc/delete/{id}', name: 'delete_hesabdari_doc_delete')]
public function delete(Log $log, Access $access, int $id, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز'], 403);
}
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->find($id);
if (!$hesabdariDoc) {
return new JsonResponse(['success' => false, 'message' => 'سند مورد نظر یافت نشد'], 404);
}
if ($hesabdariDoc->getType() !== 'calc') {
return new JsonResponse(['success' => false, 'message' => 'سند مورد نظر قابل حذف نیست'], 400);
}
if ($hesabdariDoc->getBid() !== $acc['bid']) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به سند'], 403);
}
$entityManager->remove($hesabdariDoc);
$entityManager->flush();
$log->insert('حسابداری', 'حذف سند حسابداری شماره ' . $hesabdariDoc->getCode(), $this->getUser(), $acc['bid'], $hesabdariDoc);
return new JsonResponse(['success' => true, 'message' => 'سند با موفقیت حذف شد'], 200);
}
#[Route('/api/hesabdari/direct/doc/get/{id}', name: 'get_hesabdari_doc_get')]
public function get(Access $access, int $id, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز'], 403);
}
$hesabdariDoc = $entityManager->getRepository(HesabdariDoc::class)->find($id);
if (!$hesabdariDoc) {
return new JsonResponse(['success' => false, 'message' => 'سند مورد نظر یافت نشد'], 404);
}
if ($hesabdariDoc->getBid() !== $acc['bid']) {
return new JsonResponse(['success' => false, 'message' => 'دسترسی غیرمجاز به سند'], 403);
}
$rows = [];
foreach ($hesabdariDoc->getHesabdariRows() as $row) {
$rowData = [
'id' => $row->getId(),
'ref' => [
'id' => $row->getRef()->getId(),
'name' => $row->getRef()->getName(),
'tableType' => $row->getRef()->getType()
],
'bd' => $row->getBd(),
'bs' => $row->getBs(),
'des' => $row->getDes(),
'detail' => $row->getRefData(),
'bankAccount' => $row->getBank() ? $row->getBank()->getId() : null,
'cashdesk' => $row->getCashDesk() ? $row->getCashDesk()->getId() : null,
'salary' => $row->getSalary() ? $row->getSalary()->getId() : null,
'commodity' => $row->getCommodity() ? $row->getCommodity()->getId() : null,
'commodityCount' => $row->getCommdityCount(),
'person' => $row->getPerson() ? $row->getPerson()->getId() : null
];
$rows[] = $rowData;
}
$data = [
'id' => $hesabdariDoc->getId(),
'date' => $hesabdariDoc->getDate(),
'des' => $hesabdariDoc->getDes(),
'code' => $hesabdariDoc->getCode(),
'rows' => $rows
];
return new JsonResponse(['success' => true, 'data' => $data], 200);
}
}

View file

@ -1135,35 +1135,102 @@ class HesabdariController extends AbstractController
} }
#[Route('/api/hesabdari/tables/{id}/children', name: 'get_hesabdari_table_children', methods: ['GET'])] #[Route('/api/hesabdari/tables/tree', name: 'get_hesabdari_table_tree', methods: ['GET'])]
public function getHesabdariTableChildren(int $id, Access $access, EntityManagerInterface $entityManager): JsonResponse public function getHesabdariTableTree(Access $access, EntityManagerInterface $entityManager, Request $request): JsonResponse
{ {
$acc = $access->hasRole('accounting'); $acc = $access->hasRole('accounting');
if (!$acc) { if (!$acc) {
throw $this->createAccessDeniedException(); throw $this->createAccessDeniedException();
} }
$node = $entityManager->getRepository(HesabdariTable::class)->find($id); $depth = (int) $request->query->get('depth', 2); // عمق پیش‌فرض 2
if (!$node) { $rootId = (int) $request->query->get('rootId', 1); // گره ریشه پیش‌فرض
return $this->json(['Success' => false, 'message' => 'نود مورد نظر یافت نشد'], 404);
$root = $entityManager->getRepository(HesabdariTable::class)->find($rootId);
if (!$root) {
return $this->json(['Success' => false, 'message' => 'نود ریشه یافت نشد'], 404);
} }
$children = $entityManager->getRepository(HesabdariTable::class)->findBy([ $buildTree = function ($node, $depth, $currentDepth = 0) use ($entityManager, $acc, &$buildTree) {
'upper' => $node, if ($currentDepth >= $depth) {
'bid' => [$acc['bid']->getId(), null] // حساب‌های عمومی و خصوصی return null;
]); }
$result = []; $children = $entityManager->getRepository(HesabdariTable::class)->findBy([
foreach ($children as $child) { 'upper' => $node,
$result[] = [ 'bid' => [$acc['bid']->getId(), null],
'id' => $child->getId(), ]);
'name' => $child->getName(),
'code' => $child->getCode(), $result = [];
'type' => $child->getType(), foreach ($children as $child) {
'children' => $this->hasChild($entityManager, $child) ? [] : null $childData = [
]; 'id' => $child->getId(),
'name' => $child->getName(),
'code' => $child->getCode(),
'type' => $child->getType(),
'children' => $buildTree($child, $depth, $currentDepth + 1),
];
$result[] = $childData;
}
return $result;
};
$tree = [
'id' => $root->getId(),
'name' => $root->getName(),
'code' => $root->getCode(),
'type' => $root->getType(),
'children' => $buildTree($root, $depth),
];
return $this->json(['Success' => true, 'data' => $tree]);
}
#[Route('/api/hesabdari/tables/all', name: 'get_all_hesabdari_tables', methods: ['GET'])]
public function getAllHesabdariTables(Access $access, EntityManagerInterface $entityManager, Request $request): JsonResponse
{
$acc = $access->hasRole('accounting');
if (!$acc) {
throw $this->createAccessDeniedException();
} }
return $this->json(['Success' => true, 'data' => $result]); $rootId = (int) $request->query->get('rootId', 1); // گره ریشه پیش‌فرض
$root = $entityManager->getRepository(HesabdariTable::class)->find($rootId);
if (!$root) {
return $this->json(['Success' => false, 'message' => 'نود ریشه یافت نشد'], 404);
}
$buildTree = function ($node) use ($entityManager, $acc, &$buildTree) {
$children = $entityManager->getRepository(HesabdariTable::class)->findBy([
'upper' => $node,
'bid' => [$acc['bid']->getId(), null],
]);
$result = [];
foreach ($children as $child) {
$childData = [
'id' => $child->getId(),
'name' => $child->getName(),
'code' => $child->getCode(),
'type' => $child->getType(),
'children' => $buildTree($child),
];
$result[] = $childData;
}
return $result;
};
$tree = [
'id' => $root->getId(),
'name' => $root->getName(),
'code' => $root->getCode(),
'type' => $root->getType(),
'children' => $buildTree($root),
];
return $this->json(['Success' => true, 'data' => $tree]);
} }
} }

View file

@ -134,4 +134,137 @@ class SalaryController extends AbstractController
$log->insert('بانکداری', ' تنخواه‌گردان با نام ' . $name . ' حذف شد. ', $this->getUser(), $acc['bid']->getId()); $log->insert('بانکداری', ' تنخواه‌گردان با نام ' . $name . ' حذف شد. ', $this->getUser(), $acc['bid']->getId());
return $this->json(['result' => 1]); return $this->json(['result' => 1]);
} }
#[Route('/api/salary/search', name: 'app_salary_search')]
public function app_salary_search(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$query = $entityManager->createQueryBuilder()
->select('s')
->from(Salary::class, 's')
->where('s.bid = :bid')
->andWhere('s.money = :money')
->setParameter('bid', $acc['bid'])
->setParameter('money', $acc['money']);
if (isset($params['search']) && !empty($params['search'])) {
$query->andWhere('s.name LIKE :search')
->setParameter('search', '%' . $params['search'] . '%');
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$datas = $query->getQuery()->getResult();
foreach ($datas as $data) {
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $data
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
$data->setBalance($bd - $bs);
}
return $this->json([
'items' => $provider->ArrayEntity2Array($datas, 0),
'total' => count($datas)
]);
}
#[Route('/api/salary/balance/{code}', name: 'app_salary_balance')]
public function app_salary_balance($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$salary = $entityManager->getRepository(Salary::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$salary)
throw $this->createNotFoundException();
$bs = 0;
$bd = 0;
$items = $entityManager->getRepository(HesabdariRow::class)->findBy([
'salary' => $salary
]);
foreach ($items as $item) {
$bs += $item->getBs();
$bd += $item->getBd();
}
return $this->json([
'balance' => $bd - $bs,
'debit' => $bd,
'credit' => $bs
]);
}
#[Route('/api/salary/transactions/{code}', name: 'app_salary_transactions')]
public function app_salary_transactions($code, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
{
$acc = $access->hasRole('salary');
if (!$acc)
throw $this->createAccessDeniedException();
$params = [];
if ($content = $request->getContent()) {
$params = json_decode($content, true);
}
$salary = $entityManager->getRepository(Salary::class)->findOneBy([
'bid' => $acc['bid'],
'money' => $acc['money'],
'code' => $code
]);
if (!$salary)
throw $this->createNotFoundException();
$query = $entityManager->createQueryBuilder()
->select('r')
->from(HesabdariRow::class, 'r')
->where('r.salary = :salary')
->andWhere('r.bid = :bid')
->setParameter('salary', $salary)
->setParameter('bid', $acc['bid']);
if (isset($params['startDate']) && isset($params['endDate'])) {
$query->andWhere('r.doc.date BETWEEN :startDate AND :endDate')
->setParameter('startDate', $params['startDate'])
->setParameter('endDate', $params['endDate']);
}
if (isset($params['page']) && isset($params['itemsPerPage'])) {
$query->setFirstResult(($params['page'] - 1) * $params['itemsPerPage'])
->setMaxResults($params['itemsPerPage']);
}
$transactions = $query->getQuery()->getResult();
return $this->json([
'items' => $provider->ArrayEntity2Array($transactions, 0),
'total' => count($transactions)
]);
}
} }

View file

@ -1,18 +1,18 @@
{% extends 'pdf/base.html.twig' %} {% extends 'pdf/base.html.twig' %}
{% block body %} {% block body %}
<div style="width:100%;margin-top:20px;text-align:center;"> <div style="width:100%;margin-top:20px;text-align:center;border: 1px solid black;border-radius: 8px;">
<div style="width:100%;margin-top:5px;margin-bottom:10px;text-align:center;"> <div style="width:100%;margin-top:5px;margin-bottom:10px;text-align:center;">
<table style="width:100%; border-radius: 8px;"> <table style="width:100%;">
<tbody> <tbody>
<tr style="font-size:12px;"> <tr style="font-size:12px;">
<td class="right" style="border: 1px solid black;"> <td class="right">
<p> <p>
<b>شماره سند:</b> <b>شماره سند:</b>
{{ doc.code }} {{ doc.code }}
</p> </p>
</td> </td>
<td class="right" style="border: 1px solid black;"> <td class="right">
<p> <p>
<b>نوع سند:</b> <b>نوع سند:</b>
{% if doc.type == 'cost' %} {% if doc.type == 'cost' %}
@ -34,7 +34,7 @@
</td> </td>
</tr> </tr>
<tr style="font-size:12px;"> <tr style="font-size:12px;">
<td class="right" colspan="2" style="border: 1px solid black;"> <td class="right" colspan="2">
<p> <p>
<b>توضیحات:</b> <b>توضیحات:</b>
{{ doc.des }} {{ doc.des }}
@ -88,9 +88,9 @@
{% elseif item.bank %} {% elseif item.bank %}
{{item.bank.name}} {{item.bank.name}}
{% elseif item.cashdesk %} {% elseif item.cashdesk %}
{{item.salary.name}}
{% elseif item.salary %}
{{item.cashdesk.name}} {{item.cashdesk.name}}
{% elseif item.salary %}
{{item.salary.name}}
{% else %} {% else %}
{{item.ref.name}} {{item.ref.name}}
{% endif %} {% endif %}

View file

@ -56,6 +56,7 @@
}, },
"devDependencies": { "devDependencies": {
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/lodash": "^4.17.16",
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2", "@vitejs/plugin-vue-jsx": "^4.1.2",

View file

@ -1,303 +1,324 @@
<template> <template>
<v-menu <v-menu
v-model="menu" v-model="menu"
:close-on-content-click="false" :close-on-content-click="false"
location="bottom" location="bottom"
width="400" width="400"
> >
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-text-field <v-text-field
v-bind="props" v-bind="props"
:model-value="selectedAccountName" :model-value="selectedAccountName"
:label="label" :label="label"
:rules="rules" :rules="rules"
readonly readonly
hide-details hide-details
density="compact" density="compact"
@click="openMenu" @click="openMenu"
> >
<template v-slot:append-inner> <template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> <v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</template> </template>
</v-text-field> </v-text-field>
</template> </template>
<v-card> <v-card>
<v-progress-linear v-if="isLoading" indeterminate color="primary" /> <v-progress-linear v-if="isLoading" indeterminate color="primary" />
<v-card-text v-if="accountData.length > 0" class="pa-0"> <v-card-text v-if="accountData.length > 0" class="pa-0">
<div class="tree-container"> <div class="tree-container">
<div <tree-node
v-for="item in accountData" v-for="node in accountData"
:key="item.id" :key="node.id"
class="tree-node" :node="node"
:class="{ 'has-children': item.children && item.children.length > 0 }" :selected-id="selectedAccount?.id"
> @select="handleNodeSelect"
<div class="tree-node-content" @click="handleNodeClick(item)"> @toggle="toggleNode"
<div class="tree-node-toggle" @click.stop="toggleNode(item)"> />
<v-icon v-if="item.children && item.children.length > 0"> </div>
{{ item.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }} </v-card-text>
</v-icon> <v-card-text v-else>
</div> {{ isLoading ? 'در حال بارگذاری...' : 'هیچ حسابی یافت نشد' }}
<div class="tree-node-icon"> </v-card-text>
<v-icon v-if="item.children && item.children.length > 0">mdi-folder</v-icon> </v-card>
<v-icon v-else>mdi-file-document</v-icon> </v-menu>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === item.id }">
{{ item.name }}
</div>
</div>
<div v-if="item.isOpen && item.children && item.children.length > 0" class="tree-children">
<div
v-for="child in item.children"
:key="child.id"
class="tree-node"
:class="{ 'has-children': child.children && child.children.length > 0 }"
>
<div class="tree-node-content" @click="handleNodeClick(child)">
<div class="tree-node-toggle" @click.stop="toggleNode(child)">
<v-icon v-if="child.children && child.children.length > 0">
{{ child.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</div>
<div class="tree-node-icon">
<v-icon v-if="child.children && child.children.length > 0">mdi-folder</v-icon>
<v-icon v-else>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === child.id }">
{{ child.name }}
</div>
</div>
<div v-if="child.isOpen && child.children && child.children.length > 0" class="tree-children">
<div
v-for="grandChild in child.children"
:key="grandChild.id"
class="tree-node"
>
<div class="tree-node-content" @click="handleNodeClick(grandChild)">
<div class="tree-node-icon">
<v-icon>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedAccount?.id === grandChild.id }">
{{ grandChild.name }}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</v-card-text>
<v-card-text v-else>
در حال بارگذاری...
</v-card-text>
</v-card>
</v-menu>
</template>
<script setup lang="ts"> <v-snackbar
import { ref, onMounted, computed, watch } from 'vue'; v-model="snackbar.show"
import axios from 'axios'; :color="snackbar.color"
import { debounce } from 'lodash'; // برای دیبانس کردن loadChildren :timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
const props = defineProps({ <script setup lang="ts">
modelValue: { import { ref, computed, onMounted, watch } from 'vue';
type: Number, import axios from 'axios';
default: null import TreeNode from '@/components/forms/TreeNode.vue';
},
label: { // اضافه کردن کش سراسری
type: String, const globalCache = {
default: 'حساب' data: null as AccountNode[] | null,
}, isLoading: false,
rules: { promise: null as Promise<void> | null,
type: Array, };
default: () => []
type ValidationRule = (value: any) => boolean | string;
interface AccountNode {
id: number;
name: string;
children?: AccountNode[];
isOpen?: boolean;
tableType?: string;
type?: string;
}
const props = defineProps({
modelValue: {
type: Number,
default: null,
},
label: {
type: String,
default: 'حساب',
},
rules: {
type: Array as () => ValidationRule[],
default: () => [],
},
initialAccount: {
type: Object as () => AccountNode | null,
default: null,
},
});
const emit = defineEmits(['update:modelValue', 'select', 'tableType', 'accountSelected']);
const menu = ref(false);
const accountData = ref<AccountNode[]>([]);
const selectedAccount = ref<AccountNode | null>(null);
const cache = ref(new Map<string, AccountNode[]>());
const isLoading = ref(false);
const snackbar = ref({
show: false,
text: '',
color: 'success',
});
const selectedAccountName = computed(() => {
return selectedAccount.value?.name || '';
});
// نمایش پیام خطا یا موفقیت
const showMessage = (text: string, color = 'success') => {
snackbar.value.text = text;
snackbar.value.color = color;
snackbar.value.show = true;
};
// رمزگشایی کاراکترهای یونیکد
const decodeUnicode = (str: string): string => {
try {
return decodeURIComponent(
str.replace(/\\u([\dA-F]{4})/gi, (match, grp) =>
String.fromCharCode(parseInt(grp, 16))
)
);
} catch (e) {
console.error('خطا در رمزگشایی یونیکد:', e);
return str;
}
};
// پردازش دادهها
const processTreeData = (items: any[]): any[] => {
return items.map((item) => {
if (cache.value.has(`processed-${item.id}`)) {
return cache.value.get(`processed-${item.id}`);
} }
const processedItem = {
...item,
name: decodeUnicode(item.name),
children: item.children ? processTreeData(item.children) : [],
isOpen: false,
tableType: item.type,
};
cache.value.set(`processed-${item.id}`, processedItem);
return processedItem;
}); });
};
const emit = defineEmits(['update:modelValue', 'select']); // بارگذاری تمام دادهها
const fetchAllHesabdariTables = async () => {
// اگر دادهها در کش سراسری وجود دارند
if (globalCache.data) {
accountData.value = globalCache.data;
return;
}
const menu = ref(false); // اگر در حال بارگذاری است، منتظر بمان
const accountData = ref([]); if (globalCache.isLoading && globalCache.promise) {
const selectedAccount = ref(null); await globalCache.promise;
const cache = ref(new Map()); accountData.value = globalCache.data || [];
const isLoading = ref(false); return;
}
const selectedAccountName = computed(() => { // شروع بارگذاری جدید
return selectedAccount.value?.name || ''; globalCache.isLoading = true;
}); isLoading.value = true;
// تابع برای رمزگشایی کاراکترهای یونیکد try {
const decodeUnicode = (str: string): string => { globalCache.promise = new Promise(async (resolve) => {
try { try {
return decodeURIComponent( const response = await axios.get('/api/hesabdari/tables/all');
str.replace(/\\u([\dA-F]{4})/gi, (match, grp) => if (response.data.Success && response.data.data) {
String.fromCharCode(parseInt(grp, 16)) const allNodes = processTreeData(response.data.data.children || []);
) globalCache.data = allNodes;
); accountData.value = allNodes;
} catch (e) { } else {
console.error('خطا در رمزگشایی یونیکد:', e); showMessage('هیچ حسابی یافت نشد', 'warning');
return str; }
} } catch (error) {
}; console.error('خطا در بارگذاری حساب‌ها:', error);
showMessage('خطا در بارگذاری حساب‌ها', 'error');
// پردازش دادهها برای رمزگشایی نامها } finally {
const processTreeData = (items: any[]): any[] => { globalCache.isLoading = false;
return items.map(item => { isLoading.value = false;
if (cache.value.has(`processed-${item.id}`)) { resolve();
return cache.value.get(`processed-${item.id}`);
} }
const processedItem = {
...item,
name: decodeUnicode(item.name),
children: item.children ? processTreeData(item.children) : [],
isOpen: false
};
cache.value.set(`processed-${item.id}`, processedItem);
return processedItem;
}); });
};
// بارگذاری تنبل زیرشاخهها با دیبانس await globalCache.promise;
const loadChildren = debounce(async (node: any) => { } catch (error) {
if (cache.value.has(node.id)) { console.error('خطا در بارگذاری حساب‌ها:', error);
node.children = cache.value.get(node.id); showMessage('خطا در بارگذاری حساب‌ها', 'error');
return; globalCache.isLoading = false;
} isLoading.value = false;
try { }
const response = await axios.get(`/api/hesabdari/tables/${node.id}/children`); };
if (response.data.Success) {
const children = processTreeData(response.data.data || []);
node.children = children;
cache.value.set(node.id, children);
}
} catch (error) {
console.error(`خطا در بارگذاری زیرشاخه‌های گره ${node.id}:`, error);
}
}, 300);
const toggleNode = (node: any) => { // جستجوی حساب در دادههای موجود
if (node.children && node.children.length > 0) { const findAccount = (nodes: any[]): any => {
node.isOpen = !node.isOpen; for (const node of nodes) {
if (node.isOpen && (!node.children || node.children.length === 0)) { if (node.id === props.modelValue) {
loadChildren(node); return node;
}
if (node.children && node.children.length) {
const found = findAccount(node.children);
if (found) return found;
}
}
return null;
};
// تنظیم مقدار اولیه حساب
const initializeAccount = async () => {
if (props.modelValue) {
// اگر initialAccount وجود دارد و هنوز حساب انتخاب نشده
if (props.initialAccount && !selectedAccount.value) {
selectedAccount.value = {
id: props.modelValue,
name: props.initialAccount.name || '',
tableType: props.initialAccount.tableType || '',
};
emit('select', selectedAccount.value);
emit('accountSelected', selectedAccount.value);
if (selectedAccount.value.tableType) {
emit('tableType', selectedAccount.value.tableType);
} }
} }
};
// مدیریت انتخاب آیتمها // لود دادهها اگر هنوز لود نشدهاند
const handleNodeClick = (node: any) => { if (!accountData.value.length) {
selectedAccount.value = node; await fetchAllHesabdariTables();
emit('update:modelValue', node.id);
emit('select', node);
menu.value = false;
};
// باز کردن منو
const openMenu = () => {
menu.value = true;
if (!accountData.value.length && !isLoading.value) {
fetchHesabdariTables();
} }
};
// بارگذاری اولیه گرههای ریشه // جستجوی حساب در دادهها
const fetchHesabdariTables = async () => { const account = findAccount(accountData.value);
if (cache.value.has('root')) { if (account && (!selectedAccount.value || selectedAccount.value.id !== account.id)) {
accountData.value = cache.value.get('root'); selectedAccount.value = account;
return; emit('select', account);
} emit('accountSelected', account);
isLoading.value = true; if (account.tableType && account.tableType !== selectedAccount.value?.tableType) {
try { emit('tableType', account.tableType);
const response = await axios.get('/api/hesabdari/tables');
if (response.data.Success && response.data.data) {
accountData.value = processTreeData(response.data.data[0].children || []);
cache.value.set('root', accountData.value);
} }
} catch (error) {
console.error('خطا در بارگذاری حساب‌ها:', error);
} finally {
isLoading.value = false;
} }
}; } else {
if (selectedAccount.value) {
// دیباگ تعداد مونتها selectedAccount.value = null;
onMounted(() => { emit('select', null);
fetchHesabdariTables(); emit('accountSelected', null);
}); emit('tableType', null);
// بررسی تغییرات در vue-router
watch(
() => props.modelValue,
() => {
console.log('modelValue تغییر کرد، احتمالاً به دلیل ناوبری');
} }
);
</script>
<style scoped>
.tree-container {
max-height: 300px;
overflow-y: auto;
padding: 8px;
} }
};
.tree-node { // تغییر وضعیت گره
margin-left: 24px; const toggleNode = (node: any) => {
} node.isOpen = !node.isOpen;
};
.tree-node-content { // مدیریت انتخاب گره
display: flex; const handleNodeSelect = (node: any) => {
align-items: center; selectedAccount.value = node;
padding: 4px 8px; emit('update:modelValue', node.id);
border-radius: 4px; emit('select', node);
cursor: pointer; emit('accountSelected', node);
transition: background-color 0.2s; if (node.tableType) {
emit('tableType', node.tableType);
} }
};
.tree-node-content:hover { // باز کردن منو
background-color: rgba(var(--v-theme-primary), 0.1); const openMenu = () => {
menu.value = true;
if (!accountData.value.length && !isLoading.value) {
fetchAllHesabdariTables();
} }
};
.tree-node-toggle { // اصلاح watch برای modelValue
width: 24px; watch(
display: flex; () => props.modelValue,
justify-content: center; async (newVal, oldVal) => {
align-items: center; if (newVal === oldVal || newVal === selectedAccount.value?.id) return;
} await initializeAccount();
},
{ immediate: true }
);
.tree-node-icon { // اضافه کردن watch برای initialAccount
width: 24px; watch(
display: flex; () => props.initialAccount,
justify-content: center; async (newVal) => {
align-items: center; if (newVal && props.modelValue) {
margin-right: 8px; await initializeAccount();
} }
},
{ immediate: true }
);
.tree-node-label { onMounted(() => {
flex: 1; if (props.modelValue || props.initialAccount) {
font-size: 0.9rem; initializeAccount();
font-family: 'Vazir', sans-serif;
} }
});
</script>
.tree-node-label.selected { <style scoped>
color: rgb(var(--v-theme-primary)); .tree-container {
font-weight: 500; max-height: 300px;
} overflow-y: auto;
padding: 8px;
.tree-children { }
margin-left: 24px; </style>
border-right: 2px solid rgba(var(--v-theme-primary), 0.1);
padding-right: 8px;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
</style>

View file

@ -0,0 +1,448 @@
<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-bank"
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="400" 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.id"
@click="selectItem(item)"
class="mb-1"
>
<v-list-item-title class="text-right">{{ item.name }}</v-list-item-title>
<v-list-item-subtitle class="text-right">{{ item.accountNum }}</v-list-item-subtitle>
</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>
<v-btn
v-if="filteredItems.length === 0"
block
color="primary"
class="mt-2"
@click="showAddDialog = true"
>
افزودن حساب بانکی جدید
</v-btn>
</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-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="600">
<v-card>
<v-toolbar color="primary" density="compact" class="sticky-toolbar">
<v-toolbar-title>افزودن حساب بانکی جدید</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip text="بستن">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-close"
v-bind="props"
@click="showAddDialog = false"
></v-btn>
</template>
</v-tooltip>
<v-tooltip text="ذخیره">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-content-save"
v-bind="props"
@click="saveBankAccount"
:loading="saving"
></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-card-text class="content-container">
<v-form @submit.prevent="saveBankAccount">
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.bank"
label="نام بانک *"
required
:error-messages="bankErrors"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.owner"
label="صاحب حساب"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.accountNum"
label="شماره حساب"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.cardNum"
label="شماره کارت"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.shabaNum"
label="شماره شبا"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.shobe"
label="شعبه"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.posNum"
label="شماره دستگاه کارتخوان"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newAccount.mobileInternetBank"
label="شماره موبایل اینترنت بانک"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newAccount.des"
label="توضیحات"
rows="3"
></v-textarea>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Hbankaccountsearch',
props: {
modelValue: {
type: [Object, Number],
default: null
},
label: {
type: String,
default: 'حساب بانکی'
},
returnObject: {
type: Boolean,
default: false
},
rules: {
type: Array,
default: () => []
}
},
data() {
return {
selectedItem: null,
items: [],
loading: false,
menu: false,
searchQuery: '',
totalItems: 0,
currentPage: 1,
itemsPerPage: 10,
searchTimeout: null,
showAddDialog: false,
saving: false,
snackbar: {
show: false,
text: '',
color: 'success'
},
errorMessages: [],
newAccount: {
bank: '',
accountNum: '',
cardNum: '',
shabaNum: '',
des: '',
owner: '',
shobe: '',
posNum: '',
mobileInternetBank: '',
code: 0
}
};
},
computed: {
filteredItems() {
return Array.isArray(this.items) ? this.items : [];
},
displayValue: {
get() {
if (this.menu) {
return this.searchQuery;
}
return this.selectedItem ? this.selectedItem.name : this.searchQuery;
},
set(value) {
this.searchQuery = value;
if (!value) {
this.clearSelection();
}
}
},
bankErrors() {
if (!this.newAccount.bank) return ['نام بانک الزامی است'];
return [];
},
combinedRules() {
return [
v => !!v || 'انتخاب حساب بانکی الزامی است',
...this.rules
]
}
},
watch: {
modelValue: {
handler(newVal) {
if (newVal) {
if (this.returnObject) {
this.selectedItem = newVal;
this.searchQuery = newVal.name;
} else {
this.fetchAccountById(newVal);
}
} else {
this.selectedItem = null;
this.searchQuery = '';
}
},
immediate: true
},
searchQuery: {
handler(newVal) {
this.currentPage = 1;
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.fetchData();
}, 500);
}
},
showAddDialog: {
handler(newVal) {
if (newVal) {
this.newAccount.bank = this.searchQuery;
}
}
}
},
methods: {
showMessage(text, 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/bank/search', {
page: this.currentPage,
itemsPerPage: this.itemsPerPage,
search: this.searchQuery
});
if (response.data && response.data.items) {
this.items = response.data.items;
this.totalItems = response.data.total;
} else {
this.items = [];
this.totalItems = 0;
}
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue;
} else {
this.selectedItem = this.items.find(item => item.id === this.modelValue);
}
}
} catch (error) {
console.error('خطا در بارگذاری داده‌ها:', error);
this.showMessage('خطا در بارگذاری داده‌ها', 'error');
this.items = [];
this.totalItems = 0;
} finally {
this.loading = false;
}
},
async saveBankAccount() {
if (!this.newAccount.bank) {
this.showMessage('نام بانک الزامی است', 'error');
return;
}
this.saving = true;
try {
const response = await axios.post('/api/bank/mod/' + (this.newAccount.code || 0), {
name: this.newAccount.bank,
des: this.newAccount.des,
owner: this.newAccount.owner,
accountNum: this.newAccount.accountNum,
cardNum: this.newAccount.cardNum,
shaba: this.newAccount.shabaNum,
shobe: this.newAccount.shobe,
posNum: this.newAccount.posNum,
mobileInternetbank: this.newAccount.mobileInternetBank
});
if (response.data.result === 1) {
this.showMessage('حساب بانکی با موفقیت ثبت شد');
this.showAddDialog = false;
this.fetchData();
} else if (response.data.result === 2) {
this.showMessage('این حساب بانکی قبلاً ثبت شده است', 'error');
} else if (response.data.result === 3) {
this.showMessage('نام حساب بانکی نمی‌تواند خالی باشد', 'error');
} else {
this.showMessage('خطا در ثبت حساب بانکی', 'error');
}
} catch (error) {
console.error('خطا در ثبت حساب بانکی:', error);
this.showMessage('خطا در ثبت حساب بانکی', 'error');
} finally {
this.saving = false;
}
},
selectItem(item) {
this.selectedItem = item;
this.searchQuery = item.name;
this.$emit('update:modelValue', this.returnObject ? item : item.id);
this.menu = false;
this.errorMessages = [];
},
clearSelection() {
this.selectedItem = null;
this.searchQuery = '';
this.$emit('update:modelValue', null);
this.errorMessages = ['انتخاب حساب بانکی الزامی است'];
},
handleEnter() {
if (!this.loading && this.filteredItems.length === 0) {
this.showAddDialog = true;
}
},
async fetchAccountById(id) {
try {
const response = await axios.get(`/api/componenets/bank/get/${id}`);
if (response.data && response.data.item) {
this.selectedItem = response.data.item;
this.searchQuery = response.data.item.name;
}
} catch (error) {
console.error('خطا در دریافت حساب بانکی:', error);
}
}
},
created() {
this.fetchData();
}
};
</script>
<style scoped>
.list-container {
max-height: 300px;
overflow-y: auto;
}
.content-container {
max-height: 500px;
overflow-y: auto;
}
.sticky-toolbar {
position: sticky;
top: 0;
z-index: 1;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
@media (max-width: 600px) {
.content-container {
max-height: calc(100vh - 120px);
}
}
</style>

View file

@ -0,0 +1,392 @@
<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-cash-register"
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="400" 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.id"
@click="selectItem(item)"
class="mb-1"
>
<v-list-item-title class="text-right">{{ item.name }}</v-list-item-title>
<v-list-item-subtitle class="text-right">{{ item.balance }}</v-list-item-subtitle>
</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>
<v-btn
v-if="filteredItems.length === 0"
block
color="primary"
class="mt-2"
@click="showAddDialog = true"
>
افزودن صندوق جدید
</v-btn>
</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-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="600">
<v-card>
<v-toolbar color="primary" density="compact" class="sticky-toolbar">
<v-toolbar-title>افزودن صندوق جدید</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip text="بستن">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-close"
v-bind="props"
@click="showAddDialog = false"
></v-btn>
</template>
</v-tooltip>
<v-tooltip text="ذخیره">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-content-save"
v-bind="props"
@click="saveCashdesk"
:loading="saving"
></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-card-text class="content-container">
<v-form @submit.prevent="saveCashdesk">
<v-row class="mt-4">
<v-col cols="12">
<v-text-field
v-model="newCashdesk.name"
label="نام صندوق *"
required
:error-messages="nameErrors"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newCashdesk.des"
label="توضیحات"
rows="3"
></v-textarea>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Hcashdesksearch',
props: {
modelValue: {
type: [Object, Number],
default: null
},
label: {
type: String,
default: 'صندوق'
},
returnObject: {
type: Boolean,
default: false
},
rules: {
type: Array,
default: () => []
}
},
data() {
return {
selectedItem: null,
items: [],
loading: false,
menu: false,
searchQuery: '',
totalItems: 0,
currentPage: 1,
itemsPerPage: 10,
searchTimeout: null,
showAddDialog: false,
saving: false,
snackbar: {
show: false,
text: '',
color: 'success'
},
errorMessages: [],
newCashdesk: {
name: '',
des: '',
code: 0
}
};
},
computed: {
filteredItems() {
return Array.isArray(this.items) ? this.items : [];
},
displayValue: {
get() {
if (this.menu) {
return this.searchQuery;
}
return this.selectedItem ? this.selectedItem.name : this.searchQuery;
},
set(value) {
this.searchQuery = value;
if (!value) {
this.clearSelection();
}
}
},
nameErrors() {
if (!this.newCashdesk.name) return ['نام صندوق الزامی است'];
return [];
},
combinedRules() {
return [
v => !!v || 'انتخاب صندوق الزامی است',
...this.rules
]
}
},
watch: {
modelValue: {
handler(newVal) {
if (newVal) {
if (this.returnObject) {
this.selectedItem = newVal;
this.searchQuery = newVal.name;
} else {
this.fetchCashDeskById(newVal);
}
} else {
this.selectedItem = null;
this.searchQuery = '';
}
},
immediate: true
},
searchQuery: {
handler(newVal) {
this.currentPage = 1;
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.fetchData();
}, 500);
}
},
showAddDialog: {
handler(newVal) {
if (newVal) {
this.newCashdesk.name = this.searchQuery;
}
}
}
},
methods: {
showMessage(text, 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/cashdesk/search', {
page: this.currentPage,
itemsPerPage: this.itemsPerPage,
search: this.searchQuery
});
if (response.data && response.data.items) {
this.items = response.data.items;
this.totalItems = response.data.total;
} else {
this.items = [];
this.totalItems = 0;
}
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue;
} else {
this.selectedItem = this.items.find(item => item.id === this.modelValue);
}
}
} catch (error) {
console.error('خطا در بارگذاری داده‌ها:', error);
this.showMessage('خطا در بارگذاری داده‌ها', 'error');
this.items = [];
this.totalItems = 0;
} finally {
this.loading = false;
}
},
async saveCashdesk() {
if (!this.newCashdesk.name) {
this.showMessage('نام صندوق الزامی است', 'error');
return;
}
this.saving = true;
try {
const response = await axios.post('/api/cashdesk/mod/' + (this.newCashdesk.code || 0), {
name: this.newCashdesk.name,
des: this.newCashdesk.des
});
if (response.data.result === 1) {
this.showMessage('صندوق با موفقیت ثبت شد');
this.showAddDialog = false;
this.fetchData();
} else if (response.data.result === 2) {
this.showMessage('این صندوق قبلاً ثبت شده است', 'error');
} else if (response.data.result === 3) {
this.showMessage('نام صندوق نمی‌تواند خالی باشد', 'error');
} else {
this.showMessage('خطا در ثبت صندوق', 'error');
}
} catch (error) {
console.error('خطا در ثبت صندوق:', error);
this.showMessage('خطا در ثبت صندوق', 'error');
} finally {
this.saving = false;
}
},
selectItem(item) {
this.selectedItem = item;
this.searchQuery = item.name;
this.$emit('update:modelValue', this.returnObject ? item : item.id);
this.menu = false;
this.errorMessages = [];
},
clearSelection() {
this.selectedItem = null;
this.searchQuery = '';
this.$emit('update:modelValue', null);
this.errorMessages = ['انتخاب صندوق الزامی است'];
},
handleEnter() {
if (!this.loading && this.filteredItems.length === 0) {
this.showAddDialog = true;
}
},
async fetchCashDeskById(id) {
try {
const response = await axios.get(`/api/componenets/cashdesk/get/${id}`);
if (response.data && response.data.item) {
this.selectedItem = response.data.item;
this.searchQuery = response.data.item.name;
}
} catch (error) {
console.error('خطا در دریافت صندوق:', error);
}
}
},
created() {
this.fetchData();
}
};
</script>
<style scoped>
.list-container {
max-height: 300px;
overflow-y: auto;
}
.content-container {
max-height: 500px;
overflow-y: auto;
}
.sticky-toolbar {
position: sticky;
top: 0;
z-index: 1;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
@media (max-width: 600px) {
.content-container {
max-height: calc(100vh - 120px);
}
}
</style>

View file

@ -9,13 +9,15 @@
:error-messages="errorMessages" :error-messages="errorMessages"
:rules="combinedRules" :rules="combinedRules"
:label="label" :label="label"
class="" class="my-0"
prepend-inner-icon="mdi-package-variant" prepend-inner-icon="mdi-package-variant"
clearable clearable
@click:clear="clearSelection" @click:clear="clearSelection"
:loading="loading" :loading="loading"
@keydown.enter="handleEnter" @keydown.enter="handleEnter"
hide-details="auto" hide-details
density="compact"
style="font-size: 0.7rem;"
> >
<template v-slot:append-inner> <template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> <v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
@ -23,7 +25,7 @@
</v-text-field> </v-text-field>
</template> </template>
<v-card min-width="300" max-width="400"> <v-card min-width="300" max-width="400" class="search-card">
<v-card-text class="pa-2"> <v-card-text class="pa-2">
<template v-if="!loading"> <template v-if="!loading">
<v-list density="compact" class="list-container"> <v-list density="compact" class="list-container">
@ -68,161 +70,128 @@
<v-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="800"> <v-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="800">
<v-card> <v-card>
<v-toolbar color="primary" density="compact" class="sticky-toolbar"> <v-card-title class="text-h5">
<v-toolbar-title>افزودن کالا/خدمت جدید</v-toolbar-title> افزودن کالا/خدمت جدید
<v-spacer></v-spacer> </v-card-title>
<v-tooltip text="بستن"> <v-card-text>
<template v-slot:activator="{ props }"> <v-form @submit.prevent="saveCommodity">
<v-btn <v-row>
icon="mdi-close" <v-col cols="12" md="6">
v-bind="props" <v-text-field
@click="showAddDialog = false" v-model="newCommodity.name"
></v-btn> label="نام کالا/خدمت *"
</template> required
</v-tooltip> :error-messages="nameErrors"
<v-tooltip text="ذخیره"> ></v-text-field>
<template v-slot:activator="{ props }"> </v-col>
<v-btn <v-col cols="12" md="6">
icon="mdi-content-save" <v-text-field
v-bind="props" v-model="newCommodity.code"
@click="saveCommodity" label="کد"
:loading="saving" ></v-text-field>
></v-btn> </v-col>
</template> <v-col cols="12" md="6">
</v-tooltip> <v-select
</v-toolbar> v-model="newCommodity.type"
:items="commodityTypes"
<v-tabs label="نوع *"
v-model="tabs" required
color="primary" :error-messages="typeErrors"
show-arrows ></v-select>
class="sticky-tabs" </v-col>
> <v-col cols="12" md="6">
<v-tab value="basic" class="flex-grow-1">اطلاعات پایه</v-tab> <v-select
<v-tab value="details" class="flex-grow-1">جزئیات</v-tab> v-model="newCommodity.unit"
<v-tab value="pricing" class="flex-grow-1">قیمتگذاری</v-tab> :items="units"
</v-tabs> label="واحد اندازه‌گیری *"
required
<v-card-text class="content-container"> :error-messages="unitErrors"
<v-window v-model="tabs"> ></v-select>
<v-window-item value="basic"> </v-col>
<v-form @submit.prevent="saveCommodity"> <v-col cols="12">
<v-row class="mt-4"> <v-textarea
<v-col cols="12" md="6"> v-model="newCommodity.description"
<v-text-field label="توضیحات"
v-model="newCommodity.name" rows="3"
label="نام کالا/خدمت *" ></v-textarea>
required </v-col>
:error-messages="nameErrors" </v-row>
></v-text-field> <v-row>
</v-col> <v-col cols="12" md="6">
<v-col cols="12" md="6"> <v-text-field
<v-text-field v-model="newCommodity.brand"
v-model="newCommodity.code" label="برند"
label="کد" ></v-text-field>
></v-text-field> </v-col>
</v-col> <v-col cols="12" md="6">
<v-col cols="12" md="6"> <v-text-field
<v-select v-model="newCommodity.model"
v-model="newCommodity.type" label="مدل"
:items="commodityTypes" ></v-text-field>
label="نوع *" </v-col>
required <v-col cols="12" md="6">
:error-messages="typeErrors" <v-text-field
></v-select> v-model="newCommodity.barcode"
</v-col> label="بارکد"
<v-col cols="12" md="6"> ></v-text-field>
<v-select </v-col>
v-model="newCommodity.unit" <v-col cols="12" md="6">
:items="units" <v-text-field
label="واحد اندازه‌گیری *" v-model="newCommodity.serial"
required label="سریال"
:error-messages="unitErrors" ></v-text-field>
></v-select> </v-col>
</v-col> <v-col cols="12">
<v-col cols="12"> <v-switch
<v-textarea v-model="newCommodity.isService"
v-model="newCommodity.description" label="خدمت"
label="توضیحات" color="primary"
rows="3" ></v-switch>
></v-textarea> </v-col>
</v-col> </v-row>
</v-row> <v-row>
</v-form> <v-col cols="12" md="6">
</v-window-item> <v-text-field
v-model="newCommodity.basePrice"
<v-window-item value="details"> label="قیمت پایه"
<v-row class="mt-4"> type="number"
<v-col cols="12" md="6"> prefix="ریال"
<v-text-field ></v-text-field>
v-model="newCommodity.brand" </v-col>
label="برند" <v-col cols="12" md="6">
></v-text-field> <v-text-field
</v-col> v-model="newCommodity.salePrice"
<v-col cols="12" md="6"> label="قیمت فروش"
<v-text-field type="number"
v-model="newCommodity.model" prefix="ریال"
label="مدل" ></v-text-field>
></v-text-field> </v-col>
</v-col> <v-col cols="12" md="6">
<v-col cols="12" md="6"> <v-text-field
<v-text-field v-model="newCommodity.minStock"
v-model="newCommodity.barcode" label="حداقل موجودی"
label="بارکد" type="number"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field
v-model="newCommodity.serial" v-model="newCommodity.maxStock"
label="سریال" label="حداکثر موجودی"
></v-text-field> type="number"
</v-col> ></v-text-field>
<v-col cols="12"> </v-col>
<v-switch </v-row>
v-model="newCommodity.isService" </v-form>
label="خدمت"
color="primary"
></v-switch>
</v-col>
</v-row>
</v-window-item>
<v-window-item value="pricing">
<v-row class="mt-4">
<v-col cols="12" md="6">
<v-text-field
v-model="newCommodity.basePrice"
label="قیمت پایه"
type="number"
prefix="ریال"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newCommodity.salePrice"
label="قیمت فروش"
type="number"
prefix="ریال"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newCommodity.minStock"
label="حداقل موجودی"
type="number"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="newCommodity.maxStock"
label="حداکثر موجودی"
type="number"
></v-text-field>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text> </v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="grey-darken-1" variant="text" @click="showAddDialog = false">
انصراف
</v-btn>
<v-btn color="primary" variant="text" @click="saveCommodity" :loading="saving">
ذخیره
</v-btn>
</v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -281,7 +250,6 @@ export default defineComponent({
itemsPerPage: 10, itemsPerPage: 10,
searchTimeout: null, searchTimeout: null,
showAddDialog: false, showAddDialog: false,
tabs: 'basic',
saving: false, saving: false,
commodityTypes: ['کالا', 'خدمت'], commodityTypes: ['کالا', 'خدمت'],
units: ['عدد', 'کیلوگرم', 'گرم', 'متر', 'سانتیمتر', 'لیتر', 'متر مربع', 'متر مکعب'], units: ['عدد', 'کیلوگرم', 'گرم', 'متر', 'سانتیمتر', 'لیتر', 'متر مربع', 'متر مکعب'],
@ -349,10 +317,14 @@ export default defineComponent({
watch: { watch: {
modelValue: { modelValue: {
handler(newVal) { handler(newVal) {
if (this.returnObject) { if (newVal) {
this.selectedItem = newVal if (this.returnObject) {
this.selectedItem = newVal;
} else {
this.selectedItem = this.items.find(item => item.id === newVal) || { id: newVal, name: 'در حال بارگذاری...' };
}
} else { } else {
this.selectedItem = this.items.find(item => item.id === newVal) this.selectedItem = null;
} }
}, },
immediate: true immediate: true
@ -408,6 +380,9 @@ export default defineComponent({
this.selectedItem = this.modelValue this.selectedItem = this.modelValue
} else { } else {
this.selectedItem = this.items.find(item => item.id === this.modelValue) this.selectedItem = this.items.find(item => item.id === this.modelValue)
if (!this.selectedItem) {
await this.fetchSingleCommodity(this.modelValue)
}
} }
} }
} catch (error) { } catch (error) {
@ -418,6 +393,26 @@ export default defineComponent({
this.loading = false this.loading = false
} }
}, },
async fetchSingleCommodity(id) {
try {
const response = await axios.get(`/api/commodity/${id}`)
if (response.data && response.data.id) {
this.items.push(response.data)
this.selectedItem = response.data
this.searchQuery = response.data.name
this.$emit('update:modelValue', this.returnObject ? response.data : response.data.id)
}
} catch (error) {
this.showMessage('خطا در بارگذاری کالا', 'error')
}
},
async setValue(commodity) {
if (commodity) {
this.selectedItem = commodity
this.searchQuery = commodity.name
this.$emit('update:modelValue', this.returnObject ? commodity : commodity.id)
}
},
async saveCommodity() { async saveCommodity() {
if (!this.newCommodity.name) { if (!this.newCommodity.name) {
this.showMessage('نام کالا/خدمت الزامی است', 'error') this.showMessage('نام کالا/خدمت الزامی است', 'error')
@ -485,43 +480,4 @@ export default defineComponent({
max-height: 300px; max-height: 300px;
overflow-y: auto; overflow-y: auto;
} }
.content-container {
max-height: 500px;
overflow-y: auto;
}
.sticky-toolbar {
position: sticky;
top: 0;
z-index: 1;
}
.sticky-tabs {
position: sticky;
top: 48px;
z-index: 1;
overflow-x: auto;
white-space: nowrap;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
@media (max-width: 600px) {
.content-container {
max-height: calc(100vh - 120px);
}
.sticky-tabs {
-webkit-overflow-scrolling: touch;
}
}
</style> </style>

View file

@ -9,13 +9,15 @@
:error-messages="errorMessages" :error-messages="errorMessages"
:rules="combinedRules" :rules="combinedRules"
:label="label" :label="label"
class="" class="my-0"
prepend-inner-icon="mdi-account" prepend-inner-icon="mdi-account"
clearable clearable
@click:clear="clearSelection" @click:clear="clearSelection"
:loading="loading" :loading="loading"
@keydown.enter="handleEnter" @keydown.enter="handleEnter"
hide-details="auto" hide-details
density="compact"
style="font-size: 0.7rem;"
> >
<template v-slot:append-inner> <template v-slot:append-inner>
<v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon> <v-icon>{{ menu ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
@ -23,7 +25,7 @@
</v-text-field> </v-text-field>
</template> </template>
<v-card min-width="300" max-width="400"> <v-card min-width="300" max-width="400" class="search-card">
<v-card-text class="pa-2"> <v-card-text class="pa-2">
<template v-if="!loading"> <template v-if="!loading">
<v-list density="compact" class="list-container"> <v-list density="compact" class="list-container">

View file

@ -0,0 +1,373 @@
<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=""
prepend-inner-icon="mdi-cash"
clearable
@click:clear="clearSelection"
:loading="loading"
@keydown.enter="handleEnter"
hide-details="auto"
>
<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="400">
<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.id"
@click="selectItem(item)"
class="mb-1"
>
<v-list-item-title class="text-right">{{ item.name }}</v-list-item-title>
<v-list-item-subtitle class="text-right">{{ item.balance }}</v-list-item-subtitle>
</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>
<v-btn
v-if="filteredItems.length === 0"
block
color="primary"
class="mt-2"
@click="showAddDialog = true"
>
افزودن تنخواهگردان جدید
</v-btn>
</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-dialog v-model="showAddDialog" :fullscreen="$vuetify.display.mobile" max-width="600">
<v-card>
<v-toolbar color="primary" density="compact" class="sticky-toolbar">
<v-toolbar-title>افزودن تنخواهگردان جدید</v-toolbar-title>
<v-spacer></v-spacer>
<v-tooltip text="بستن">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-close"
v-bind="props"
@click="showAddDialog = false"
></v-btn>
</template>
</v-tooltip>
<v-tooltip text="ذخیره">
<template v-slot:activator="{ props }">
<v-btn
icon="mdi-content-save"
v-bind="props"
@click="saveSalary"
:loading="saving"
></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-card-text class="content-container">
<v-form @submit.prevent="saveSalary">
<v-row class="mt-4">
<v-col cols="12">
<v-text-field
v-model="newSalary.name"
label="نام تنخواه‌گردان *"
required
:error-messages="nameErrors"
></v-text-field>
</v-col>
<v-col cols="12">
<v-textarea
v-model="newSalary.des"
label="توضیحات"
rows="3"
></v-textarea>
</v-col>
</v-row>
</v-form>
</v-card-text>
</v-card>
</v-dialog>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
color="white"
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: 'Hsalarysearch',
props: {
modelValue: {
type: [Object, Number],
default: null
},
label: {
type: String,
default: 'تنخواه‌گردان'
},
returnObject: {
type: Boolean,
default: false
},
rules: {
type: Array,
default: () => []
}
},
data() {
return {
selectedItem: null,
items: [],
loading: false,
menu: false,
searchQuery: '',
totalItems: 0,
currentPage: 1,
itemsPerPage: 10,
searchTimeout: null,
showAddDialog: false,
saving: false,
snackbar: {
show: false,
text: '',
color: 'success'
},
errorMessages: [],
newSalary: {
name: '',
des: '',
code: 0
}
};
},
computed: {
filteredItems() {
return Array.isArray(this.items) ? this.items : [];
},
displayValue: {
get() {
if (this.menu) {
return this.searchQuery;
}
return this.selectedItem ? this.selectedItem.name : this.searchQuery;
},
set(value) {
this.searchQuery = value;
if (!value) {
this.clearSelection();
}
}
},
nameErrors() {
if (!this.newSalary.name) return ['نام تنخواه‌گردان الزامی است'];
return [];
},
combinedRules() {
return [
v => !!v || 'انتخاب تنخواه‌گردان الزامی است',
...this.rules
]
}
},
watch: {
modelValue: {
handler(newVal) {
if (this.returnObject) {
this.selectedItem = newVal;
} else {
this.selectedItem = this.items.find(item => item.id === newVal);
}
},
immediate: true
},
searchQuery: {
handler(newVal) {
this.currentPage = 1;
if (this.searchTimeout) {
clearTimeout(this.searchTimeout);
}
this.searchTimeout = setTimeout(() => {
this.fetchData();
}, 500);
}
},
showAddDialog: {
handler(newVal) {
if (newVal) {
this.newSalary.name = this.searchQuery;
}
}
}
},
methods: {
showMessage(text, 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/salary/search', {
page: this.currentPage,
itemsPerPage: this.itemsPerPage,
search: this.searchQuery
});
if (response.data && response.data.items) {
this.items = response.data.items;
this.totalItems = response.data.total;
} else {
this.items = [];
this.totalItems = 0;
}
if (this.modelValue) {
if (this.returnObject) {
this.selectedItem = this.modelValue;
} else {
this.selectedItem = this.items.find(item => item.id === this.modelValue);
}
}
} catch (error) {
console.error('خطا در بارگذاری داده‌ها:', error);
this.showMessage('خطا در بارگذاری داده‌ها', 'error');
this.items = [];
this.totalItems = 0;
} finally {
this.loading = false;
}
},
async saveSalary() {
if (!this.newSalary.name) {
this.showMessage('نام تنخواه‌گردان الزامی است', 'error');
return;
}
this.saving = true;
try {
const response = await axios.post('/api/salary/mod/' + (this.newSalary.code || 0), {
name: this.newSalary.name,
des: this.newSalary.des
});
if (response.data.result === 1) {
this.showMessage('تنخواه‌گردان با موفقیت ثبت شد');
this.showAddDialog = false;
this.fetchData();
} else if (response.data.result === 2) {
this.showMessage('این تنخواه‌گردان قبلاً ثبت شده است', 'error');
} else if (response.data.result === 3) {
this.showMessage('نام تنخواه‌گردان نمی‌تواند خالی باشد', 'error');
} else {
this.showMessage('خطا در ثبت تنخواه‌گردان', 'error');
}
} catch (error) {
console.error('خطا در ثبت تنخواه‌گردان:', error);
this.showMessage('خطا در ثبت تنخواه‌گردان', 'error');
} finally {
this.saving = false;
}
},
selectItem(item) {
this.selectedItem = item;
this.searchQuery = item.name;
this.$emit('update:modelValue', this.returnObject ? item : item.id);
this.menu = false;
this.errorMessages = [];
},
clearSelection() {
this.selectedItem = null;
this.searchQuery = '';
this.$emit('update:modelValue', null);
this.errorMessages = ['انتخاب تنخواه‌گردان الزامی است'];
},
handleEnter() {
if (!this.loading && this.filteredItems.length === 0) {
this.showAddDialog = true;
}
}
},
created() {
this.fetchData();
}
};
</script>
<style scoped>
.list-container {
max-height: 300px;
overflow-y: auto;
}
.content-container {
max-height: 500px;
overflow-y: auto;
}
.sticky-toolbar {
position: sticky;
top: 0;
z-index: 1;
}
:deep(.v-menu__content) {
position: fixed !important;
z-index: 9999 !important;
transform-origin: center top !important;
}
:deep(.v-overlay__content) {
position: fixed !important;
}
@media (max-width: 600px) {
.content-container {
max-height: calc(100vh - 120px);
}
}
</style>

View file

@ -0,0 +1,104 @@
<template>
<div class="tree-node" :class="{ 'has-children': node.children && node.children.length > 0 }">
<div class="tree-node-content" @click="handleNodeClick">
<div class="tree-node-toggle" @click.stop="toggleNode">
<v-icon v-if="node.children && node.children.length > 0">
{{ node.isOpen ? 'mdi-chevron-down' : 'mdi-chevron-right' }}
</v-icon>
</div>
<div class="tree-node-icon">
<v-icon v-if="node.children && node.children.length > 0">mdi-folder</v-icon>
<v-icon v-else>mdi-file-document</v-icon>
</div>
<div class="tree-node-label" :class="{ 'selected': selectedId === node.id }">
{{ node.name }}
</div>
</div>
<div v-if="node.isOpen && node.children && node.children.length > 0" class="tree-children">
<tree-node
v-for="child in node.children"
:key="child.id"
:node="child"
:selected-id="selectedId"
@select="$emit('select', $event)"
@toggle="$emit('toggle', $event)"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { defineEmits } from 'vue';
const props = defineProps({
node: {
type: Object,
required: true,
},
selectedId: {
type: Number,
default: null,
},
});
const emit = defineEmits(['select', 'toggle']);
const handleNodeClick = () => {
emit('select', props.node);
};
const toggleNode = () => {
emit('toggle', props.node);
};
</script>
<style scoped>
.tree-node {
margin-left: 24px;
}
.tree-node-content {
display: flex;
align-items: center;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.tree-node-content:hover {
background-color: rgba(var(--v-theme-primary), 0.1);
}
.tree-node-toggle {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
}
.tree-node-icon {
width: 24px;
display: flex;
justify-content: center;
align-items: center;
margin-right: 8px;
}
.tree-node-label {
flex: 1;
font-size: 0.9rem;
font-family: 'Vazir', sans-serif;
}
.tree-node-label.selected {
color: rgb(var(--v-theme-primary));
font-weight: 500;
}
.tree-children {
margin-left: 24px;
border-right: 2px solid rgba(var(--v-theme-primary), 0.1);
padding-right: 8px;
}
</style>

View file

@ -277,6 +277,7 @@ const fa_lang = {
fetch_data_error: "خطا در گرفتن داده از {url}" fetch_data_error: "خطا در گرفتن داده از {url}"
}, },
dialog: { dialog: {
change_password: 'تغییر کلمه عبور',
download: 'دانلود', download: 'دانلود',
delete_group: 'حذف گروهی', delete_group: 'حذف گروهی',
add_new_transfer: 'سند انتقال جدید', add_new_transfer: 'سند انتقال جدید',

View file

@ -1,60 +1,152 @@
<template> <template>
<div> <div>
<v-toolbar <v-toolbar color="toolbar" title="اسناد حسابداری">
color="toolbar" <template v-slot:prepend>
title="اسناد حسابداری" <v-tooltip :text="$t('dialog.back')" location="bottom">
> <template v-slot:activator="{ props }">
<template v-slot:prepend> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
<v-tooltip :text="$t('dialog.back')" location="bottom"> icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-tooltip v-if="isPluginActive('accpro')" text="افزودن سند حسابداری" location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" <v-btn v-bind="props" icon="mdi-plus" variant="text" color="success" :to="'/acc/accounting/mod'"></v-btn>
icon="mdi-arrow-right" />
</template> </template>
</v-tooltip> </v-tooltip>
</template>
</v-toolbar> </v-toolbar>
<v-text-field <v-text-field v-model="searchValue" prepend-inner-icon="mdi-magnify" density="compact" hide-details :rounded="false"
v-model="searchValue" placeholder="جست و جو ..."></v-text-field>
prepend-inner-icon="mdi-magnify"
density="compact"
hide-details
:rounded="false"
placeholder="جست و جو ..."
></v-text-field>
<v-data-table <v-data-table :headers="headers" :items="filteredItems" :search="searchValue" :loading="loading"
:headers="headers" :header-props="{ class: 'custom-header' }" hover>
:items="filteredItems"
:search="searchValue"
:loading="loading"
:header-props="{ class: 'custom-header' }"
hover
>
<template v-slot:item.state="{ item }"> <template v-slot:item.state="{ item }">
<v-icon <v-icon :color="item.type !== 'calc' ? 'error' : 'success'">
:color="item.type !== 'accounting' ? 'error' : 'success'" {{ item.type !== 'calc' ? 'mdi-lock' : 'mdi-lock-open' }}
>
{{ item.type !== 'accounting' ? 'mdi-lock' : 'mdi-lock-open' }}
</v-icon> </v-icon>
</template> </template>
<template v-slot:item.operation="{ item }"> <template v-slot:item.operation="{ item }">
<v-tooltip text="مشاهده سند" location="bottom"> <v-tooltip v-if="!isPluginActive('accpro') || item.type !== 'calc'" text="مشاهده سند" location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn <v-btn v-bind="props" icon variant="text" color="success" :to="'/acc/accounting/view/' + item.code">
v-bind="props"
icon
variant="text"
color="success"
:to="'/acc/accounting/view/' + item.code"
>
<v-icon>mdi-eye</v-icon> <v-icon>mdi-eye</v-icon>
</v-btn> </v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-menu v-else-if="item.type === 'calc' && isPluginActive('accpro')">
<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 :title="$t('dialog.view')" :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>
<v-list-item :title="$t('dialog.edit')" :to="'/acc/accounting/mod/' + item.id">
<template v-slot:prepend>
<v-icon icon="mdi-file-edit"></v-icon>
</template>
</v-list-item>
<v-list-item :title="$t('dialog.delete')" @click="openDeleteDialog(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-data-table> </v-data-table>
<!-- دیالوگ تأیید حذف -->
<v-dialog v-model="deleteDialog" max-width="500">
<v-card class="rounded-lg">
<v-card-title class="d-flex align-center pa-4">
<v-icon color="error" size="large" class="ml-2">mdi-alert-circle-outline</v-icon>
<span class="text-h5 font-weight-bold">حذف سند حسابداری</span>
</v-card-title>
<v-divider></v-divider>
<v-card-text class="pa-4">
<div class="d-flex flex-column">
<div class="text-subtitle-1 mb-2">آیا مطمئن هستید که میخواهید سند زیر را حذف کنید؟</div>
<v-card variant="outlined" class="mt-2">
<v-card-text>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">کد سند:</span>
<span>{{ selectedItem?.code?.toLocaleString() }}</span>
</div>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">تاریخ:</span>
<span>{{ selectedItem?.date }}</span>
</div>
<div class="d-flex justify-space-between mb-2">
<span class="text-subtitle-2 font-weight-bold">شرح:</span>
<span>{{ selectedItem?.des }}</span>
</div>
<div class="d-flex justify-space-between">
<span class="text-subtitle-2 font-weight-bold">مبلغ:</span>
<span>{{ selectedItem?.amountRaw?.toLocaleString() }}</span>
</div>
</v-card-text>
</v-card>
<v-alert
v-if="selectedItem?.type !== 'calc'"
type="warning"
variant="tonal"
class="mt-4"
>
<template v-slot:prepend>
<v-icon color="warning">mdi-alert</v-icon>
</template>
این سند قفل شده است و امکان حذف آن وجود ندارد.
</v-alert>
</div>
</v-card-text>
<v-divider></v-divider>
<v-card-actions class="pa-4">
<v-spacer></v-spacer>
<v-btn
color="grey-darken-1"
variant="text"
@click="deleteDialog = false"
:disabled="deleteLoading"
>
انصراف
</v-btn>
<v-btn
color="error"
variant="tonal"
@click="confirmDelete"
:loading="deleteLoading"
:disabled="selectedItem?.type !== 'calc'"
>
<template v-slot:prepend>
<v-icon>mdi-delete</v-icon>
</template>
حذف سند
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<!-- اسنکبار برای نمایش پیام -->
<v-snackbar v-model="snackbar.show" :color="snackbar.color" timeout="3000">
{{ snackbar.message }}
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false">
بستن
</v-btn>
</template>
</v-snackbar>
</div> </div>
</template> </template>
@ -65,6 +157,15 @@ import axios from 'axios'
const searchValue = ref('') const searchValue = ref('')
const loading = ref(true) const loading = ref(true)
const items = ref([]) const items = ref([])
const deleteDialog = ref(false)
const deleteLoading = ref(false)
const selectedItem = ref(null)
const snackbar = ref({
show: false,
message: '',
color: 'success'
})
const plugins = ref({})
const headers = [ const headers = [
{ title: 'وضعیت', key: 'state', sortable: true }, { title: 'وضعیت', key: 'state', sortable: true },
@ -76,6 +177,10 @@ const headers = [
{ title: 'ثبت کننده', key: 'submitter', sortable: true } { title: 'ثبت کننده', key: 'submitter', sortable: true }
] ]
const isPluginActive = (plugName) => {
return plugins.value[plugName] !== undefined
}
const loadData = async () => { const loadData = async () => {
try { try {
const response = await axios.post('/api/accounting/search', { const response = await axios.post('/api/accounting/search', {
@ -93,6 +198,15 @@ const loadData = async () => {
} }
} }
const loadPlugins = async () => {
try {
const response = await axios.post('/api/plugin/get/actives')
plugins.value = response.data
} catch (error) {
console.error('Error loading plugins:', error)
}
}
const filteredItems = computed(() => { const filteredItems = computed(() => {
if (!searchValue.value) return items.value if (!searchValue.value) return items.value
@ -117,8 +231,51 @@ const filteredItems = computed(() => {
}) })
}) })
const openDeleteDialog = (item) => {
selectedItem.value = item
deleteDialog.value = true
}
const confirmDelete = async () => {
try {
deleteLoading.value = true
const response = await axios.delete(`/api/hesabdari/direct/doc/delete/${selectedItem.value.id}`)
if (response.data.success) {
// حذف آیتم از لیست
const index = items.value.findIndex(item => item.id === selectedItem.value.id)
if (index !== -1) {
items.value.splice(index, 1)
}
deleteDialog.value = false
// نمایش پیام موفقیت
snackbar.value = {
show: true,
message: 'سند با موفقیت حذف شد',
color: 'success'
}
} else {
// نمایش پیام خطا
snackbar.value = {
show: true,
message: response.data.message || 'خطا در حذف سند',
color: 'error'
}
}
} catch (error) {
// نمایش پیام خطا
snackbar.value = {
show: true,
message: error.response?.data?.message || 'خطا در ارتباط با سرور',
color: 'error'
}
} finally {
deleteLoading.value = false
}
}
onMounted(() => { onMounted(() => {
loadData() loadData()
loadPlugins()
}) })
</script> </script>

View file

@ -1,26 +1,25 @@
<template> <template>
<v-toolbar color="toolbar" :title="$t('dialog.accounting_doc')">
<v-toolbar color="toolbar" :title="$t('dialog.accounting_doc')"> <template v-slot:prepend>
<template v-slot:prepend> <v-tooltip :text="$t('dialog.back')" location="bottom">
<v-tooltip :text="$t('dialog.back')" location="bottom"> <template v-slot:activator="{ props }">
<template v-slot:activator="{ props }"> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
icon="mdi-arrow-right" /> </template>
</template> </v-tooltip>
</v-tooltip> </template>
<v-spacer></v-spacer>
<v-tooltip text="ثبت سند" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-content-save" color="success" @click="submitForm" :loading="loading"></v-btn>
</template> </template>
<v-spacer></v-spacer> </v-tooltip>
<v-tooltip text="ثبت سند" location="bottom"> <v-tooltip v-if="docId" text="حذف سند" location="bottom">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-content-save" color="success" @click="submitForm" :loading="loading"></v-btn> <v-btn v-bind="props" variant="text" icon="mdi-delete" color="error" @click="deleteDialog = true" :loading="loading"></v-btn>
</template> </template>
</v-tooltip> </v-tooltip>
<v-tooltip v-if="docId" text="حذف سند" location="bottom"> </v-toolbar>
<template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="text" icon="mdi-delete" color="error" @click="deleteDialog = true" :loading="loading"></v-btn>
</template>
</v-tooltip>
</v-toolbar>
<v-container> <v-container>
<v-form @submit.prevent="submitForm"> <v-form @submit.prevent="submitForm">
@ -40,6 +39,13 @@
</v-col> </v-col>
</v-row> </v-row>
<v-alert v-if="error" type="error" class="mt-4">
{{ error }}
<template v-slot:close>
<v-btn icon="mdi-close" variant="text" @click="error = null"></v-btn>
</template>
</v-alert>
<v-table class="border rounded d-none d-sm-table mt-3" style="width: 100%;"> <v-table class="border rounded d-none d-sm-table mt-3" style="width: 100%;">
<thead> <thead>
<tr style="background-color: #0D47A1; color: white; height: 40px;"> <tr style="background-color: #0D47A1; color: white; height: 40px;">
@ -52,130 +58,251 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<template v-for="(row, index) in form.rows" :key="index"> <template v-if="loading">
<tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '40px' }"> <tr>
<td class="text-center" style="min-width: 150px; padding: 0 4px;"> <td colspan="6" class="text-center pa-4">
<Haccountsearch <v-progress-circular indeterminate color="primary"></v-progress-circular>
v-model="row.ref" <span class="mr-2">در حال بارگذاری...</span>
:rules="[v => !!v || 'حساب الزامی است']" </td>
@account-selected="(account) => handleAccountSelect(row, account)" </tr>
/> </template>
</td> <template v-else>
<td class="text-center" style="min-width: 100px; padding: 0 4px;"> <template v-for="(row, index) in form.rows" :key="index">
</td> <tr :style="{ backgroundColor: index % 2 === 0 ? '#f8f9fa' : 'white', height: '40px' }">
<td class="text-center" style="padding: 0 4px;"> <td class="text-center" style="min-width: 150px; padding: 0 4px;">
<v-text-field <Haccountsearch
v-model="row.des" v-model="row.ref"
label="توضیحات" :rules="[v => !!v || 'حساب الزامی است']"
density="compact" @account-selected="(account) => handleAccountSelect(row, account)"
class="my-0" @tableType="(type) => handleTableType(row, type)"
style="font-size: 0.7rem;" />
hide-details </td>
></v-text-field> <td class="text-center" style="min-width: 150px; padding: 0 4px;">
</td> <template v-if="row.tableType === 'bank'">
<td class="text-center" style="width: 100px; padding: 0 4px;"> <Hbankaccountsearch
<v-text-field v-model="row.bankAccount"
v-model="row.bd" :rules="[]"
label="بدهکار" @update:modelValue="(value) => handleBankAccountSelect(row, value)"
type="number" density="compact"
density="compact" hide-details
@input="calculateTotals" class="my-0"
class="my-0" style="font-size: 0.7rem;"
style="font-size: 0.7rem;" :ref="`bankAccount_${row.ref}`"
hide-details />
></v-text-field> </template>
</td> <template v-else-if="row.tableType === 'cashdesk'">
<td class="text-center" style="width: 100px; padding: 0 4px;"> <Hcashdesksearch
<v-text-field v-model="row.cashdesk"
v-model="row.bs" :rules="[]"
label="بستانکار" @update:modelValue="(value) => handleCashdeskSelect(row, value)"
type="number" density="compact"
density="compact" hide-details
@input="calculateTotals" class="my-0"
class="my-0" style="font-size: 0.7rem;"
style="font-size: 0.7rem;" :ref="`cashdesk_${row.ref}`"
hide-details />
></v-text-field> </template>
</td> <template v-else-if="row.tableType === 'salary'">
<td class="text-center" style="width: 50px; padding: 0 4px;"> <Hsalarysearch
<v-tooltip text="حذف" location="bottom"> v-model="row.salary"
<template v-slot:activator="{ props }"> :rules="[]"
<v-btn v-bind="props" icon="mdi-delete" variant="text" size="x-small" color="error" @update:modelValue="(value) => handleSalarySelect(row, value)"
@click="removeRow(row)" style="min-width: 30px;"></v-btn> density="compact"
</template> hide-details
</v-tooltip> class="my-0"
style="font-size: 0.7rem;"
:ref="`salary_${row.ref}`"
/>
</template>
<template v-else-if="row.tableType === 'person'">
<Hpersonsearch
v-model="row.person"
:rules="[]"
@update:modelValue="(value) => handlePersonSelect(row, value)"
density="compact"
hide-details
class="my-0"
style="font-size: 0.7rem;"
:ref="`person_${row.ref}`"
/>
</template>
<template v-else-if="row.tableType === 'commodity'">
<div class="d-flex align-center">
<Hcommoditysearch
v-model="row.commodity"
:rules="[]"
@update:modelValue="(value) => handleCommoditySelect(row, value)"
density="compact"
hide-details
class="my-0"
style="font-size: 0.7rem;"
:ref="`commodity_${row.ref}`"
:key="row.ref"
/>
<v-text-field
v-model="row.commodityCount"
label="تعداد"
type="number"
density="compact"
hide-details
class="my-0 mr-2"
style="font-size: 0.7rem; width: 80px;"
min="0"
></v-text-field>
</div>
</template>
<template v-else>
<v-text-field
v-model="row.detail"
label="تفصیل"
density="compact"
class="my-0"
style="font-size: 0.7rem;"
hide-details
></v-text-field>
</template>
</td>
<td class="text-center" style="padding: 0 4px;">
<v-text-field
v-model="row.des"
label="توضیحات"
density="compact"
class="my-0"
style="font-size: 0.7rem;"
hide-details
></v-text-field>
</td>
<td class="text-center" style="width: 100px; padding: 0 4px;">
<Hnumberinput
v-model="row.bd"
label="بدهکار"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.7rem;"
hide-details
/>
</td>
<td class="text-center" style="width: 100px; padding: 0 4px;">
<Hnumberinput
v-model="row.bs"
label="بستانکار"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.7rem;"
hide-details
/>
</td>
<td class="text-center" style="width: 50px; padding: 0 4px;">
<v-tooltip text="حذف" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-delete" variant="text" size="x-small" color="error"
@click="removeRow(row)" style="min-width: 30px;"></v-btn>
</template>
</v-tooltip>
</td>
</tr>
</template>
<tr>
<td colspan="6" class="text-center pa-1" style="height: 40px;">
<v-btn color="primary" prepend-icon="mdi-plus" size="x-small" @click="addRow">افزودن سطر جدید</v-btn>
</td> </td>
</tr> </tr>
</template> </template>
<tr>
<td colspan="6" class="text-center pa-1" style="height: 40px;">
<v-btn color="primary" prepend-icon="mdi-plus" size="x-small" @click="addRow">افزودن سطر جدید</v-btn>
</td>
</tr>
</tbody> </tbody>
</v-table> </v-table>
<!-- جدول موبایل --> <!-- جدول موبایل -->
<div class="d-sm-none"> <div class="d-sm-none">
<v-card v-for="(row, index) in form.rows" :key="index" class="mb-4" variant="outlined"> <template v-if="loading">
<v-card-text> <v-card class="mb-4" variant="outlined">
<div class="d-flex justify-space-between align-center mb-2"> <v-card-text class="text-center">
<span class="text-subtitle-2 font-weight-bold">ردیف:</span> <v-progress-circular indeterminate color="primary"></v-progress-circular>
<span>{{ index + 1 }}</span> <span class="mr-2">در حال بارگذاری...</span>
</div> </v-card-text>
<div class="mb-2"> </v-card>
<Haccountsearch </template>
v-model="row.ref" <template v-else>
:rules="[v => !!v || 'حساب الزامی است']" <v-card v-for="(row, index) in form.rows" :key="index" class="mb-4" variant="outlined">
@account-selected="(account) => handleAccountSelect(row, account)" <v-card-text>
/> <div class="d-flex justify-space-between align-center mb-2">
</div> <span class="text-subtitle-2 font-weight-bold">ردیف:</span>
<div class="mb-2"> <span>{{ index + 1 }}</span>
<v-text-field </div>
v-model="row.des" <div class="mb-2">
label="توضیحات" <Haccountsearch
density="compact" v-model="row.ref"
class="my-0" :rules="[v => !!v || 'حساب الزامی است']"
style="font-size: 0.8rem;" @account-selected="(account) => handleAccountSelect(row, account)"
></v-text-field> />
</div> </div>
<div class="d-flex justify-space-between mb-2"> <div class="mb-2">
<div style="width: 48%;"> <Hbankaccountsearch
v-model="row.bankAccount"
:rules="[]"
@update:modelValue="(value) => handleBankAccountSelect(row, value)"
/>
</div>
<div class="mb-2">
<Hcashdesksearch
v-model="row.cashdesk"
:rules="[]"
@update:modelValue="(value) => handleCashdeskSelect(row, value)"
/>
</div>
<div class="mb-2">
<Hpersonsearch
v-model="row.person"
:rules="[]"
@update:modelValue="(value) => handlePersonSelect(row, value)"
/>
</div>
<div class="mb-2">
<v-text-field <v-text-field
v-model="row.bd" v-model="row.des"
label="بدهکار" label="توضیحات"
type="number"
density="compact" density="compact"
@input="calculateTotals"
class="my-0" class="my-0"
style="font-size: 0.8rem;" style="font-size: 0.8rem;"
></v-text-field> ></v-text-field>
</div> </div>
<div style="width: 48%;"> <div class="d-flex justify-space-between mb-2">
<v-text-field <div style="width: 48%;">
v-model="row.bs" <Hnumberinput
label="بستانکار" v-model="row.bd"
type="number" label="بدهکار"
density="compact" density="compact"
@input="calculateTotals" @input="calculateTotals"
class="my-0" class="my-0"
style="font-size: 0.8rem;" style="font-size: 0.8rem;"
></v-text-field> />
</div>
<div style="width: 48%;">
<Hnumberinput
v-model="row.bs"
label="بستانکار"
density="compact"
@input="calculateTotals"
class="my-0"
style="font-size: 0.8rem;"
/>
</div>
</div> </div>
</div> </v-card-text>
</v-card-text> <v-card-actions>
<v-card-actions> <v-spacer></v-spacer>
<v-spacer></v-spacer> <v-btn icon="mdi-delete" variant="text" color="error" @click="removeRow(row)"></v-btn>
<v-btn icon="mdi-delete" variant="text" color="error" @click="removeRow(row)"></v-btn> </v-card-actions>
</v-card-actions> </v-card>
</v-card> </template>
<v-btn color="primary" prepend-icon="mdi-plus" block class="mb-4" @click="addRow">افزودن ردیف جدید</v-btn> <v-btn color="primary" prepend-icon="mdi-plus" block class="mb-4" @click="addRow">افزودن ردیف جدید</v-btn>
</div> </div>
<v-row class="mt-4"> <v-row class="mt-4">
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
:value="totalBd" v-model="totalBd"
label="جمع بدهکار" label="جمع بدهکار"
readonly readonly
dense dense
@ -183,15 +310,13 @@
</v-col> </v-col>
<v-col cols="6"> <v-col cols="6">
<v-text-field <v-text-field
:value="totalBs" v-model="totalBs"
label="جمع بستانکار" label="جمع بستانکار"
readonly readonly
dense dense
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-alert v-if="error" type="error" class="mt-4">{{ error }}</v-alert>
</v-form> </v-form>
</v-container> </v-container>
@ -215,6 +340,23 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- Snackbar برای نمایش پیامها -->
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
>
{{ snackbar.text }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template> </template>
<script> <script>
@ -222,17 +364,23 @@ import axios from 'axios';
import moment from 'jalali-moment'; import moment from 'jalali-moment';
import Hdatepicker from '@/components/forms/Hdatepicker.vue'; import Hdatepicker from '@/components/forms/Hdatepicker.vue';
import Haccountsearch from '@/components/forms/Haccountsearch.vue'; import Haccountsearch from '@/components/forms/Haccountsearch.vue';
import Hbankaccountsearch from '@/components/forms/Hbankaccountsearch.vue';
import Hcashdesksearch from '@/components/forms/Hcashdesksearch.vue';
import Hsalarysearch from '@/components/forms/Hsalarysearch.vue';
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue';
import Hpersonsearch from '@/components/forms/Hpersonsearch.vue';
import Hnumberinput from '@/components/forms/Hnumberinput.vue';
export default { export default {
components: { components: {
Hdatepicker, Hdatepicker,
Haccountsearch Haccountsearch,
}, Hbankaccountsearch,
props: { Hcashdesksearch,
docId: { Hsalarysearch,
type: Number, Hcommoditysearch,
default: null, Hpersonsearch,
}, Hnumberinput
}, },
data() { data() {
return { return {
@ -240,54 +388,117 @@ export default {
date: '', date: '',
des: '', des: '',
rows: [ rows: [
{ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] }, { ref: null, refName: '', bd: '0', bs: '0', des: '', detail: '', selectedAccounts: [], bankAccount: null, cashdesk: null, salary: null, commodity: null, commodityCount: null, person: null, tableType: null },
{ ref: null, refName: '', bd: '0', bs: '0', des: '', detail: '', selectedAccounts: [], bankAccount: null, cashdesk: null, salary: null, commodity: null, commodityCount: null, person: null, tableType: null },
], ],
}, },
hesabdariTables: [],
totalBd: 0, totalBd: 0,
totalBs: 0, totalBs: 0,
error: null, error: null,
deleteDialog: false, deleteDialog: false,
loading: false, loading: false,
snackbar: {
show: false,
text: '',
color: 'success'
}
}; };
}, },
mounted() { computed: {
this.fetchHesabdariTables(); docId() {
if (this.docId) { return this.$route.params.id;
this.fetchDoc();
} }
}, },
mounted() {
this.loading = true;
Promise.all([
this.docId ? this.fetchDoc() : Promise.resolve()
]).finally(() => {
this.loading = false;
});
},
methods: { methods: {
async fetchHesabdariTables() { showSnackbar(text, color = 'success') {
try { this.snackbar.text = text;
const response = await axios.get('/api/hesabdari/tables'); this.snackbar.color = color;
this.hesabdariTables = response.data.data; this.snackbar.show = true;
} catch (error) {
console.error('خطا در دریافت حساب‌ها:', error.response?.data || error.message);
this.error = 'خطا در بارگذاری حساب‌ها: ' + (error.response?.data?.message || 'مشکل ناشناخته');
}
}, },
async fetchDoc() { async fetchDoc() {
try { try {
const response = await axios.get(`/api/hesabdari/doc/${this.docId}`); const response = await axios.get(`/api/hesabdari/direct/doc/get/${this.docId}`);
const serverDate = response.data.data.date; if (response.data.success) {
this.form.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY-MM-DD'); const serverDate = response.data.data.date;
this.form.des = response.data.data.des || ''; this.form.date = moment(serverDate, 'YYYY/MM/DD').format('YYYY/MM/DD');
this.form.rows = response.data.data.rows.map(row => ({ this.form.des = response.data.data.des || '';
ref: row.ref.id,
refName: row.ref.name, // ایجاد یک آرایه موقت برای ذخیره ردیفها
bd: row.bd, const tempRows = response.data.data.rows.map(row => ({
bs: row.bs, ref: row.ref.id,
des: row.des, refName: row.ref.name,
selectedAccounts: [{ id: row.ref.id, name: row.ref.name }], bd: row.bd,
})); bs: row.bs,
this.calculateTotals(); des: row.des,
detail: row.detail || '',
selectedAccounts: [{ id: row.ref.id, name: row.ref.name }],
bankAccount: row.bankAccount,
cashdesk: row.cashdesk,
salary: row.salary,
commodity: row.commodity,
commodityCount: row.commodityCount,
person: row.person,
tableType: row.ref.tableType
}));
// یکباره تمام ردیفها را تنظیم کنیم
this.form.rows = tempRows;
// تنظیم مقادیر کامپوننتهای فرزند
await this.$nextTick();
// استفاده از Promise.all برای اجرای همزمان تنظیم مقادیر
await Promise.all(this.form.rows.map(async (row) => {
if (row.tableType === 'bank' && row.bankAccount) {
const bankAccountRef = this.$refs[`bankAccount_${row.ref}`]?.[0];
if (bankAccountRef) {
await bankAccountRef.setValue(row.bankAccount);
}
}
if (row.tableType === 'cashdesk' && row.cashdesk) {
const cashdeskRef = this.$refs[`cashdesk_${row.ref}`]?.[0];
if (cashdeskRef) {
await cashdeskRef.setValue(row.cashdesk);
}
}
if (row.tableType === 'person' && row.person) {
const personRef = this.$refs[`person_${row.ref}`]?.[0];
if (personRef) {
await personRef.setValue(row.person);
}
}
if (row.tableType === 'commodity' && row.commodity) {
const commodityRef = this.$refs[`commodity_${row.ref}`]?.[0];
if (commodityRef) {
await commodityRef.setValue(row.commodity);
}
}
if (row.tableType === 'salary' && row.salary) {
const salaryRef = this.$refs[`salary_${row.ref}`]?.[0];
if (salaryRef) {
await salaryRef.setValue(row.salary);
}
}
}));
this.calculateTotals();
} else {
this.error = response.data.message || 'خطا در بارگذاری سند';
}
} catch (error) { } catch (error) {
this.error = 'خطا در بارگذاری سند: ' + (error.response?.data?.message || 'مشکل ناشناخته'); this.error = 'خطا در بارگذاری سند: ' + (error.response?.data?.message || 'مشکل ناشناخته');
} }
}, },
addRow() { addRow() {
this.form.rows.push({ ref: null, refName: '', bd: '0', bs: '0', des: '', selectedAccounts: [] }); this.form.rows.push({ ref: null, refName: '', bd: '0', bs: '0', des: '', detail: '', selectedAccounts: [], bankAccount: null, cashdesk: null, salary: null, commodity: null, commodityCount: null, person: null, tableType: null });
}, },
removeRow(item) { removeRow(item) {
const index = this.form.rows.indexOf(item); const index = this.form.rows.indexOf(item);
@ -297,46 +508,180 @@ export default {
} }
}, },
calculateTotals() { calculateTotals() {
let hasError = false;
for (const row of this.form.rows) {
if (!this.validateDebitCredit(row)) {
hasError = true;
}
}
if (hasError) {
return;
}
this.error = null;
this.totalBd = this.form.rows.reduce((sum, row) => sum + parseInt(row.bd || 0), 0); this.totalBd = this.form.rows.reduce((sum, row) => sum + parseInt(row.bd || 0), 0);
this.totalBs = this.form.rows.reduce((sum, row) => sum + parseInt(row.bs || 0), 0); this.totalBs = this.form.rows.reduce((sum, row) => sum + parseInt(row.bs || 0), 0);
}, },
selectAccount(row, selected) { validateDebitCredit(row) {
if (selected.length > 0) { if (parseInt(row.bd) > 0 && parseInt(row.bs) > 0) {
const account = selected[0]; this.error = 'در هر سطر فقط یکی از فیلدهای بدهکار یا بستانکار می‌تواند مقدار داشته باشد';
row.ref = account.id; // صفر کردن مقدار نامعتبر
row.refName = account.name; if (row.bd > 0) {
row.selectedAccounts = [account]; row.bd = '0';
} else if (row.bs > 0) {
row.bs = '0';
}
return false;
} }
return true;
},
handleAccountSelect(row, account) {
row.ref = account.id;
row.refName = account.name;
row.selectedAccounts = [account];
// فقط tableType را تنظیم کنید اگر تغییر کرده باشد
if (row.tableType !== account.tableType) {
this.handleTableType(row, account.tableType);
}
},
handleBankAccountSelect(row, bankAccount) {
row.bankAccount = bankAccount;
},
handleCashdeskSelect(row, cashdesk) {
row.cashdesk = cashdesk;
},
handleSalarySelect(row, salary) {
row.salary = salary;
},
handleCommoditySelect(row, commodity) {
row.commodity = commodity;
row.commodityCount = commodity ? row.commodityCount : null;
},
handlePersonSelect(row, person) {
row.person = person;
},
handleTableType(row, type) {
if (row.tableType === type) return; // جلوگیری از تغییرات غیرضروری
const prevCommodity = row.commodity; // ذخیره مقدار قبلی commodity
const prevCommodityCount = row.commodityCount;
row.tableType = type;
// فقط فیلدهای غیرمرتبط را پاک کنید
if (type !== 'bank') row.bankAccount = null;
if (type !== 'cashdesk') row.cashdesk = null;
if (type !== 'person') row.person = null;
if (type !== 'salary') row.salary = null;
if (type !== 'commodity') {
row.commodity = null;
row.commodityCount = null;
} else {
// بازیابی commodity اگر tableType به commodity برگردد
row.commodity = prevCommodity;
row.commodityCount = prevCommodityCount;
}
if (type !== 'calc') row.detail = '';
}, },
async submitForm() { async submitForm() {
this.error = null; this.error = null;
if (this.form.rows.length < 2) {
this.error = 'حداقل باید دو سطر در سند وجود داشته باشد';
return;
}
if (this.totalBd !== this.totalBs) { if (this.totalBd !== this.totalBs) {
this.error = 'جمع بدهکار و بستانکار باید برابر باشد'; this.error = 'جمع بدهکار و بستانکار باید برابر باشد';
return; return;
} }
for (const row of this.form.rows) {
if (!row.ref) {
this.error = 'انتخاب حساب در تمام سطرها الزامی است';
return;
}
if (parseInt(row.bd) === 0 && parseInt(row.bs) === 0) {
this.error = 'در هر سطر باید حداقل یکی از فیلدهای بدهکار یا بستانکار مقدار داشته باشد';
return;
}
if (!this.validateDebitCredit(row)) {
return;
}
if (row.tableType === 'bank' && !row.bankAccount) {
this.error = 'انتخاب حساب بانکی در سطر مربوطه الزامی است';
return;
}
if (row.tableType === 'cashdesk' && !row.cashdesk) {
this.error = 'انتخاب صندوق در سطر مربوطه الزامی است';
return;
}
if (row.tableType === 'salary' && !row.salary) {
this.error = 'انتخاب حقوق در سطر مربوطه الزامی است';
return;
}
if (row.tableType === 'person' && !row.person) {
this.error = 'انتخاب شخص در سطر مربوطه الزامی است';
return;
}
if (row.tableType === 'commodity' && !row.commodity) {
this.error = 'انتخاب کالا در سطر مربوطه الزامی است';
return;
}
if (row.tableType === 'commodity' && !row.commodityCount) {
this.error = 'تعداد کالا در سطر مربوطه الزامی است';
return;
}
}
const payload = { const payload = {
date: moment(this.form.date, 'YYYY-MM-DD').locale('fa').format('YYYY/MM/DD'), date: this.form.date,
des: this.form.des, des: this.form.des,
rows: this.form.rows.map(row => ({ rows: this.form.rows.map(row => ({
ref: row.ref, ref: row.ref,
bd: row.bd, bd: row.bd,
bs: row.bs, bs: row.bs,
des: row.des, des: row.des,
detail: row.detail,
bankAccount: row.bankAccount,
cashdesk: row.cashdesk,
salary: row.salary,
commodity: row.commodity,
commodityCount: row.commodityCount,
person: row.person
})), })),
}; };
this.loading = true;
try { try {
this.loading = true; let response;
if (this.docId) { if (this.docId) {
await axios.put(`/api/hesabdari/doc/${this.docId}`, payload); response = await axios.put(`/api/hesabdari/direct/doc/update/${this.docId}`, payload);
this.$emit('saved', 'سند با موفقیت ویرایش شد');
} else { } else {
const response = await axios.post('/api/hesabdari/doc', payload); response = await axios.post('/api/hesabdari/direct/doc/create', payload);
this.$emit('saved', 'سند با موفقیت ثبت شد', response.data.data.id); }
if (response && response.data && response.data.success) {
this.showSnackbar(response.data.message);
setTimeout(() => {
this.$router.push('/acc/accounting/list');
}, 1000);
} else {
this.error = response?.data?.message || 'خطا در انجام عملیات';
} }
} catch (error) { } catch (error) {
this.error = error.response?.data?.message || 'خطا در ثبت سند'; if (error.response && error.response.data && error.response.data.success) {
this.showSnackbar(error.response.data.message);
setTimeout(() => {
this.$router.push('/acc/accounting/list');
}, 1000);
} else {
this.error = error.response?.data?.message || 'خطا در ارتباط با سرور';
}
} finally { } finally {
this.loading = false; this.loading = false;
} }
@ -344,10 +689,17 @@ export default {
async confirmDelete() { async confirmDelete() {
try { try {
this.loading = true; this.loading = true;
await axios.delete(`/api/hesabdari/doc/${this.docId}`); const response = await axios.delete(`/api/hesabdari/direct/doc/delete/${this.docId}`);
this.$router.push('/acc/accounting/list'); if (response && response.data && response.data.success) {
this.showSnackbar(response.data.message);
setTimeout(() => {
this.$router.push('/acc/accounting/list');
}, 1000);
} else {
this.error = response?.data?.message || 'خطا در حذف سند';
}
} catch (error) { } catch (error) {
this.error = 'خطا در حذف سند'; this.error = error.response?.data?.message || 'خطا در حذف سند';
console.error(error); console.error(error);
} finally { } finally {
this.loading = false; this.loading = false;

View file

@ -1,249 +1,400 @@
<script lang="ts"> <template>
import { defineComponent } from 'vue' <v-toolbar color="toolbar" title="حواله ورود به انبار">
import axios from "axios"; <template v-slot:prepend>
import Loading from "vue-loading-overlay"; <v-tooltip text="بازگشت" location="bottom">
import 'vue-loading-overlay/dist/css/index.css'; <template v-slot:activator="{ props }">
import VuePersianDatetimePicker from 'vue-persian-datetime-picker'; <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-tooltip text="تکمیل خودکار" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
@click="autofill"
variant="text"
icon="mdi-auto-fix"
class="mx-2"
/>
</template>
</v-tooltip>
<v-tooltip text="ثبت حواله ورود" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
:loading="loading"
:disabled="loading"
color="success"
icon="mdi-content-save"
@click="submit"
/>
</template>
</v-tooltip>
</v-toolbar>
import Swal from "sweetalert2"; <v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.store.des"
label="انبار"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.person.des"
label="خریدار"
variant="outlined"
density="compact"
readonly
/>
</v-col>
</v-row>
export default defineComponent({ <v-row>
name: "buy", <v-col cols="12">
components: { <v-text-field
Loading, v-model="ticket.des"
}, label="شرح"
data: () => { variant="outlined"
return { density="compact"
loading: false, />
doc: {}, </v-col>
ticket: { </v-row>
type: 'input',
typeString: 'حواله ورود', <v-row>
date: '', <v-col cols="12" md="3">
des: '', <v-text-field
transfer: '', v-model="ticket.transfer"
receiver: '', label="حمل‌و‌نقل"
code: '', variant="outlined"
store: {}, density="compact"
person: {}, />
transferType: {}, </v-col>
referral: '' <v-col cols="12" md="3">
}, <v-text-field
transferTypes: [], v-model="ticket.receiver"
year: {}, label="تحویل"
items: [], variant="outlined"
headers: [ density="compact"
{ text: "کد", value: "commodity.code" }, />
{ text: "کالا", value: "commodity.name", sortable: true }, </v-col>
{ text: "واحد", value: "commodity.unit", sortable: true }, <v-col cols="12" md="3">
{ text: "مورد نیاز", value: "docCount" }, <v-select
{ text: "از قبل", value: "countBefore" }, v-model="ticket.transferType"
{ text: "باقی‌مانده", value: "remain" }, :items="transferTypes"
{ text: "تعداد", value: "commdityCount", sortable: true }, item-title="name"
{ text: "ارجاع", value: "referal", sortable: true }, item-value="id"
{ text: "توضیحات", value: "des" }, label="روش تحویل"
], variant="outlined"
currencyConfig: { density="compact"
masked: false, />
prefix: '', </v-col>
suffix: '', <v-col cols="12" md="3">
thousands: ',', <v-text-field
decimal: '.', v-model="ticket.referral"
precision: 0, label="شماره پیگیری"
disableNegative: false, variant="outlined"
disabled: false, density="compact"
min: 0, />
max: null, </v-col>
allowBlank: false, </v-row>
minimumNumberOfCharacters: 0,
shouldRound: true, <v-row>
focusOnRight: true, <v-col cols="12">
}, <v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
>
<template v-slot:item.commdityCount="{ item, index }">
<v-text-field
v-model="items[index].ticketCount"
type="number"
variant="outlined"
density="compact"
:min="0"
:max="item.remain"
@blur="(event) => { if (items[index].ticketCount === '') { items[index].ticketCount = 0 } }"
@keypress="isNumber($event)"
/>
</template>
<template v-slot:item.des="{ item, index }">
<v-text-field
v-model="items[index].des"
variant="outlined"
density="compact"
/>
</template>
<template v-slot:item.referal="{ item, index }">
<v-text-field
v-model="items[index].referral"
variant="outlined"
density="compact"
/>
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
interface TransferType {
id: number;
name: string;
}
interface Person {
des: string;
}
interface Store {
des: string;
name: string;
manager: string;
}
interface Commodity {
code: string;
name: string;
unit: string;
commdityCount: number;
docCount: number;
countBefore: number;
remain: number;
ticketCount: number;
des: string;
referral: string;
type: string;
}
interface Ticket {
type: string;
typeString: string;
date: string;
des: string;
transfer: string;
receiver: string;
code: string;
store: Store;
person: Person;
transferType: TransferType;
referral: string;
}
interface Year {
start: string;
end: string;
now: string;
}
const router = useRouter()
const loading = ref(false)
// Refs
const doc = ref({})
const ticket = ref<Ticket>({
type: 'input',
typeString: 'حواله ورود',
date: '',
des: '',
transfer: '',
receiver: '',
code: '',
store: {} as Store,
person: {} as Person,
transferType: {} as TransferType,
referral: ''
})
const transferTypes = ref<TransferType[]>([])
const year = ref<Year>({} as Year)
const items = ref<Commodity[]>([])
const headers = [
{ title: "کد", key: "commodity.code" },
{ title: "کالا", key: "commodity.name", sortable: true },
{ title: "واحد", key: "commodity.unit", sortable: true },
{ title: "مورد نیاز", key: "docCount" },
{ title: "از قبل", key: "countBefore" },
{ title: "باقی‌مانده", key: "remain" },
{ title: "تعداد", key: "commdityCount", sortable: true },
{ title: "ارجاع", key: "referal", sortable: true },
{ title: "توضیحات", key: "des" },
]
const snackbar = ref({
show: false,
message: '',
color: 'primary' as 'primary' | 'error' | 'success' | 'warning'
})
// Methods
const submit = async () => {
loading.value = true
try {
const errors: string[] = []
let rowsWithZeroCount = 0
let totalCount = 0
items.value.forEach((element, index) => {
if (element.ticketCount === 0) {
rowsWithZeroCount++
} else if (element.ticketCount === undefined || element.ticketCount === null) {
errors.push(`تعداد کالا در ردیف ${index + 1} وارد نشده است.`)
} else {
totalCount += element.ticketCount
}
})
if (totalCount === 0) {
errors.push('تعداد تمام کالاها صفر است!')
} }
},
methods: { if (errors.length !== 0) {
submit() { snackbar.value = {
this.loading = true; show: true,
let errors = []; message: errors.join('\n'),
let rowsWithZeroCount = 0; color: 'error'
this.items.forEach((element, index) => {
if (element.ticketCount === '') {
errors.push('تعداد کالا در ردیف ' + (index + 1) + 'وارد نشده است.');
}
else if (element.ticketCount === 0 && element.remain != 0) {
rowsWithZeroCount++;
}
});
//check all values is zero
if (rowsWithZeroCount != 0) {
errors.push('تعداد تمام کالاها صفر است!');
} }
if (errors.length != 0) { return
let errorStr = '<ul>'; }
errors.forEach((item) => { errorStr += '<li>' + item + '</li>' })
errorStr += '</ul>' const response = await axios.post('/api/storeroom/ticket/insert', {
Swal.fire({ doc: doc.value,
html: errorStr, ticket: {
icon: 'error', ...ticket.value,
confirmButtonText: 'قبول' senderTel: ticket.value.person.mobile || '',
}).then((response) => { sms: false
this.loading = false; },
}); items: items.value
})
if (response.data.result === 0) {
snackbar.value = {
show: true,
message: 'حواله انبار با موفقیت ثبت شد.',
color: 'success'
} }
else { setTimeout(() => {
//going to save ticket router.push('/acc/storeroom/tickets/list')
axios.post('/api/storeroom/ticket/insert', { }, 1000)
doc: this.doc, }
ticket: this.ticket, } catch (error) {
items: this.items console.error('Error submitting form:', error)
}).then((resp) => { snackbar.value = {
Swal.fire({ show: true,
text: 'حواله انبار با موفقیت ثبت شد.', message: 'خطا در ثبت اطلاعات',
icon: 'success', color: 'error'
confirmButtonText: 'قبول' }
}).then((response) => { } finally {
this.$router.push('/acc/storeroom/tickets/list'); loading.value = false
this.loading = false;
});
});
}
},
autofill() {
this.items.forEach((element, index) => {
this.items[index].ticketCount = this.items[index].docCount;
this.items[index].des = 'تعداد ' + this.items[index].ticketCount + 'مورد تحویل شد. ';
})
},
isNumber(evt: KeyboardEvent): void {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const keyPressed: string = evt.key;
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
},
loadData() {
axios.post('/api/storeroom/doc/get/info/' + this.$route.params.doc).then((res) => {
this.doc = res.data;
this.ticket.person = res.data.person;
this.ticket.des = 'حواله ورود انبار برای فاکتور خرید شماره # ' + this.doc.code;
this.items = res.data.commodities;
this.items.forEach((element, index) => {
this.items[index].ticketCount = 0;
this.items[index].docCount = element.commdityCount;
this.items[index].des = '';
this.items[index].type = 'input';
})
});
axios.post('/api/storeroom/info/' + this.$route.params.storeID).then((res) => {
this.ticket.store = res.data;
this.ticket.store.des = this.ticket.store.name + ' انباردار : ' + this.ticket.store.manager
});
//load year
axios.post('/api/year/get').then((response) => {
this.year = response.data;
this.ticket.date = response.data.now;
})
//load transfer types
axios.post('/api/storeroom/transfertype/list').then((response) => {
this.transferTypes = response.data;
this.ticket.transferType = response.data[0];
})
},
},
mounted() {
this.loadData();
} }
}
const autofill = () => {
items.value.forEach((element, index) => {
const remain = Math.max(0, items.value[index].remain)
items.value[index].ticketCount = remain
items.value[index].des = remain > 0 ? `تعداد ${remain} مورد تحویل شد.` : ''
items.value[index].type = 'input'
})
}
const isNumber = (evt: KeyboardEvent): void => {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const keyPressed: string = evt.key
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
}
const loadData = async () => {
loading.value = true
try {
const [docResponse, storeResponse, yearResponse, transferTypesResponse] = await Promise.all([
axios.post(`/api/storeroom/doc/get/info/${router.currentRoute.value.params.doc}`),
axios.post(`/api/storeroom/info/${router.currentRoute.value.params.storeID}`),
axios.post('/api/year/get'),
axios.post('/api/storeroom/transfertype/list')
])
doc.value = docResponse.data
ticket.value.person = docResponse.data.person
ticket.value.des = `حواله ورود انبار برای فاکتور خرید شماره # ${docResponse.data.code}`
items.value = docResponse.data.commodities.map((element: Commodity) => ({
...element,
ticketCount: 0,
docCount: element.commdityCount,
des: '',
type: 'input'
}))
ticket.value.store = storeResponse.data
ticket.value.store.des = `${storeResponse.data.name} انباردار : ${storeResponse.data.manager}`
year.value = yearResponse.data
ticket.value.date = yearResponse.data.now
transferTypes.value = transferTypesResponse.data
ticket.value.transferType = transferTypesResponse.data[0]
} catch (error) {
console.error('Error loading data:', error)
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
}
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
}) })
</script> </script>
<template> <style scoped>
<div class="block block-content-full "> .v-data-table {
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> border-radius: 8px;
<h3 class="block-title text-primary-dark"> }
<button @click="$router.back()" type="button" </style>
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="mx-2 fa fa-file-import"></i>
حواله ورود به انبار
</h3>
<div class="block-options">
<button @click="autofill()" class="btn btn-sm btn-outline-primary">
<i class="fa fa-list-check me-2"></i>
تکمیل خودکار
</button>
<button :disabled="this.loading" @click="submit()" type="button" class="mx-2 btn btn-sm btn-success">
<i class="fa fa-save me-2"></i>
ثبت حواله ورود
</button>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-4">
<label class="form-label">تاریخ</label>
<date-picker class="" v-model="this.ticket.date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="year.start" :max="year.end" />
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">انبار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.store.des" type="text"
class="form-control">
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">خریدار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.person.des" type="text"
class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-12">
<label class="form-label">شرح</label>
<input v-model="this.ticket.des" type="text" class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-3">
<label class="form-label">حمل و نقل</label>
<input v-model="this.ticket.transfer" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">تحویل</label>
<input v-model="this.ticket.receiver" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">روش تحویل</label>
<select class="form-select" v-model="ticket.transferType">
<option v-for="transferType in transferTypes" :value="transferType">{{ transferType.name }}</option>
</select>
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">شماره پیگیری</label>
<input v-model="this.ticket.referral" type="text" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-sm-12 col-md-12">
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating :headers="headers"
:items="items" theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="this.loading">
<template #item-commdityCount="{ index, commdityCount, ticketCount }">
<input @blur="(event) => { if (this.items[index - 1].ticketCount === '') { this.items[index - 1].ticketCount = 0 } }"
@keypress="isNumber($event)" class="form-control form-control-sm" type="number" min="0"
:max="this.items[index - 1].remain" v-model="this.items[index - 1].ticketCount" />
</template>
<template #item-des="{ index, des }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].des" />
</template>
<template #item-referal="{ index }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].referral" />
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View file

@ -1,257 +1,455 @@
<script lang="ts">
import {defineComponent, ref} from 'vue'
import recList from "../../component/recList.vue";
import axios from "axios";
import Swal from "sweetalert2";
export default defineComponent({
name: "modalNew",
components: {},
watch:{
'item.type'(newValue,oldValue) {
if (newValue == 'sell') { this.$data.item.title = 'فروش'; this.$data.item.removeBeforeTicketsEnable = true; }
else if (newValue == 'buy') { this.$data.item.title = 'خرید'; this.$data.item.removeBeforeTicketsEnable = true; }
else if (newValue == 'rfbuy') { this.$data.item.title = 'برگشت از خرید'; this.$data.item.removeBeforeTicketsEnable = true; }
else if (newValue == 'rfsell') { this.$data.item.title = 'برگشت از فروش'; this.$data.item.removeBeforeTicketsEnable = true; }
else if (newValue == 'wastage') {
this.$data.item.title = 'ضایعات';
this.$data.item.removeBeforeTicketsEnable = false;
this.$data.item.removeBeforeTickets = false;
}
else if (newValue == 'used') {
this.$data.item.title = 'مصرف مستقیم';
this.$data.item.removeBeforeTicketsEnable = false;
this.$data.item.removeBeforeTickets = false;
}
},
},
data: () => {
return {
loading: ref(false),
storerooms: [],
item: {
storeroom: null,
type: 'sell',
title: 'فروش',
docSell: null,
docBuy: null,
removeBeforeTickets: true,
removeBeforeTicketsEnable: true
},
buys: [],
sells: [],
rfsells: [],
rfbuys: []
}
},
methods: {
loadData() {
this.loading = true;
axios.post('/api/storeroom/list')
.then((response) => {
this.storerooms = response.data.data;
this.storerooms.forEach((element) => {
element.name = element.name + ' انباردار : ' + element.manager
});
if (this.storerooms.length != 0) {
this.item.storeroom = this.storerooms[0];
}
this.loading = false;
});
axios.post('/api/storeroom/docs/get').then((response) => {
this.buys = response.data.buys;
this.sells = response.data.sells;
this.rfsells = response.data.rfsells;
this.rfbuys = response.data.rfbuys;
})
},
submit() {
this.loading = true;
if (this.item.storeroom == null) {
Swal.fire({
text: 'انبار انتخاب نشده است.',
icon: 'error',
confirmButtonText: 'قبول'
}).then((res) => {
this.loading = false;
});
}
else if (this.item.type == 'sell' && this.item.docSell == null) {
Swal.fire({
text: 'فاکتور فروش انتخاب نشده است.',
icon: 'error',
confirmButtonText: 'قبول'
}).then((res) => {
this.loading = false;
});
}
else if (this.item.type == 'buy' && this.item.docBuy == null) {
Swal.fire({
text: 'فاکتور خرید انتخاب نشده است.',
icon: 'error',
confirmButtonText: 'قبول'
}).then((res) => {
this.loading = false;
});
}
else if (this.item.type == 'rfbuy' && this.item.docRfbuy == null) {
Swal.fire({
text: 'فاکتور برگشت از خرید انتخاب نشده است.',
icon: 'error',
confirmButtonText: 'قبول'
}).then((res) => {
this.loading = false;
});
}
else if (this.item.type == 'rfsell' && this.item.docRfsell == null) {
Swal.fire({
text: 'فاکتور برگشت از خرید انتخاب نشده است.',
icon: 'error',
confirmButtonText: 'قبول'
}).then((res) => {
this.loading = false;
});
}
else {
//going to save storeroom ticket
if (this.item.type == 'sell') {
this.$router.push({ name: 'storeroom_new_ticket_sell', params: { doc: this.item.docSell.code, storeID: this.item.storeroom.id } })
}
else if (this.item.type == 'buy') {
this.$router.push({ name: 'storeroom_new_ticket_buy', params: { doc: this.item.docBuy.code, storeID: this.item.storeroom.id } })
}
else if (this.item.type == 'rfbuy') {
this.$router.push({ name: 'storeroom_new_ticket_rfbuy', params: { doc: this.item.docRfbuy.code, storeID: this.item.storeroom.id } })
}
else if (this.item.type == 'rfsell') {
this.$router.push({ name: 'storeroom_new_ticket_rfsell', params: { doc: this.item.docRfsell.code, storeID: this.item.storeroom.id } })
}
}
}
},
beforeMount() {
this.loadData();
}
})
</script>
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" title="حواله انبار جدید">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip text="بازگشت" location="bottom">
<button @click="$router.back()" type="button" <template v-slot:activator="{ props }">
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
<i class="fa fw-bold fa-arrow-right"></i> </template>
</button> </v-tooltip>
<i class="fa fa-file-circle-plus"></i> </template>
حواله انبار جدید <v-spacer />
</h3> <v-tooltip text="ثبت" location="bottom">
<div class="block-options"> <template v-slot:activator="{ props }">
<button :disabled="this.loading" @click="submit()" type="button" class="btn btn-sm btn-alt-primary"> <v-btn
<i class="fa fa-save me-2"></i> v-bind="props"
ثبت :loading="loading"
</button> :disabled="loading"
</div> color="primary"
</div> icon="mdi-content-save"
<div class="block-content pt-1 pb-3"> @click="submit"
<div class="row content"> />
<div class="col-sm-12 col-md-12"> </template>
<b class="alert alert-light">نوع حواله انبار را انتخاب کنید </b> </v-tooltip>
<div class="row mt-4"> </v-toolbar>
<div class="col-sm-12 col-md-6 mt-2">
<div class="form-control mb-2">
<label class="form-label">انبار</label>
<v-cob dir="rtl" :options="storerooms" label="name" v-model="item.storeroom">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
</div>
</div>
<div class="col-sm-12 col-md-6 mt-2">
<b class="mb-3 pb-3">نوع حواله انبار</b>
<div class="form-check mt-3">
<input v-model="this.item.type" value="sell" class="form-check-input" type="radio">
<label class="form-check-label">
حواله برای فاکتور فروش
</label>
</div>
<div class="form-check">
<input v-model="this.item.type" value="buy" class="form-check-input" type="radio">
<label class="form-check-label">
حواله برای فاکتور خرید
</label>
</div>
<div>
<div class="form-check">
<input v-model="this.item.type" value="rfbuy" class="form-check-input" type="radio">
<label class="form-check-label">
حواله برای فاکتور برگشت از خرید
</label>
</div>
<div class="form-check">
<input v-model="this.item.type" value="rfsell" class="form-check-input" type="radio">
<label class="form-check-label">
حواله برای فاکتور برگشت از فروش
</label>
</div>
<div class="form-check d-none">
<input v-model="this.item.type" value="wastage" class="form-check-input" type="radio">
<label class="form-check-label">
ضایعات
</label>
</div>
<div class="form-check d-none">
<input v-model="this.item.type" value="used" class="form-check-input" type="radio">
<label class="form-check-label">
مصرف مستقیم
</label>
</div>
</div>
</div>
<div class="col-sm-12 col-md-12 mt-2">
<div class="rounded-2 border border-secondary bg-danger-light p-2">
<span>فاکتور {{ item.title }}</span>
<v-cob v-if="this.item.type == 'buy'" dir="rtl" :options="buys" label="des" v-model="item.docBuy"
class="bg-white">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<v-cob v-if="this.item.type == 'sell'" dir="rtl" :options="sells" label="des" v-model="item.docSell"
class="bg-white">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<v-cob v-if="this.item.type == 'rfsell'" dir="rtl" :options="rfsells" label="des"
v-model="item.docRfsell" class="bg-white">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<v-cob v-if="this.item.type == 'rfbuy'" dir="rtl" :options="rfbuys" label="des"
v-model="item.docRfbuy" class="bg-white">
<template #no-options="{ search, searching, loading }">
نتیجهای یافت نشد!
</template>
</v-cob>
<div class="form-check mt-2 ms-3">
<input :disabled="!item.removeBeforeTicketsEnable" v-model="this.item.removeBeforeTickets" class="form-check-input" type="checkbox">
<label class="form-check-label" >
حذف حوالههای انباری که قبلا برای این فاکتور صادر شده است.
</label>
</div>
</div> <v-container>
</div> <v-row>
</div> <!-- ستون سمت راست - انتخاب انبار -->
</div> <v-col cols="12" md="4">
</div> <v-card variant="outlined" class="h-100">
</div> <v-card-title class="text-subtitle-1 font-weight-bold">
</div> <v-icon start>mdi-warehouse</v-icon>
انتخاب انبار
</v-card-title>
<v-card-text>
<v-select
v-model="item.storeroom"
:items="storerooms"
item-title="name"
item-value="id"
label="انبار"
variant="outlined"
density="compact"
:loading="loading"
return-object
class="mb-4"
>
<template v-slot:no-data>
<v-list-item>
<v-list-item-title>
نتیجهای یافت نشد!
</v-list-item-title>
</v-list-item>
</template>
</v-select>
</v-card-text>
</v-card>
</v-col>
<!-- ستون وسط - نوع حواله -->
<v-col cols="12" md="4">
<v-card variant="outlined" class="h-100">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-file-document-edit</v-icon>
نوع حواله
</v-card-title>
<v-card-text>
<v-radio-group v-model="item.type" class="mt-0">
<v-radio value="sell" color="success">
<template v-slot:label>
<div class="d-flex align-center">
<v-icon color="success" class="ml-2">mdi-cart-arrow-down</v-icon>
<span>حواله برای فاکتور فروش</span>
</div>
</template>
</v-radio>
<v-radio value="buy" color="primary">
<template v-slot:label>
<div class="d-flex align-center">
<v-icon color="primary" class="ml-2">mdi-cart-arrow-up</v-icon>
<span>حواله برای فاکتور خرید</span>
</div>
</template>
</v-radio>
<v-radio value="rfbuy" color="warning">
<template v-slot:label>
<div class="d-flex align-center">
<v-icon color="warning" class="ml-2">mdi-cart-remove</v-icon>
<span>حواله برای فاکتور برگشت از خرید</span>
</div>
</template>
</v-radio>
<v-radio value="rfsell" color="error">
<template v-slot:label>
<div class="d-flex align-center">
<v-icon color="error" class="ml-2">mdi-cart-remove</v-icon>
<span>حواله برای فاکتور برگشت از فروش</span>
</div>
</template>
</v-radio>
</v-radio-group>
</v-card-text>
</v-card>
</v-col>
<!-- ستون سمت چپ - انتخاب فاکتور -->
<v-col cols="12" md="4">
<v-card variant="outlined" class="h-100">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-file-document</v-icon>
انتخاب فاکتور
</v-card-title>
<v-card-text>
<v-select
v-if="item.type === 'buy'"
v-model="item.docBuy"
:items="buys"
item-title="des"
item-value="code"
label="فاکتور خرید"
variant="outlined"
density="compact"
:loading="loading"
return-object
class="mb-4"
>
<template v-slot:no-data>
<v-list-item>
<v-list-item-title>
نتیجهای یافت نشد!
</v-list-item-title>
</v-list-item>
</template>
</v-select>
<v-select
v-if="item.type === 'sell'"
v-model="item.docSell"
:items="sells"
item-title="des"
item-value="code"
label="فاکتور فروش"
variant="outlined"
density="compact"
:loading="loading"
return-object
class="mb-4"
>
<template v-slot:no-data>
<v-list-item>
<v-list-item-title>
نتیجهای یافت نشد!
</v-list-item-title>
</v-list-item>
</template>
</v-select>
<v-select
v-if="item.type === 'rfsell'"
v-model="item.docRfsell"
:items="rfsells"
item-title="des"
item-value="code"
label="فاکتور برگشت از فروش"
variant="outlined"
density="compact"
:loading="loading"
return-object
class="mb-4"
>
<template v-slot:no-data>
<v-list-item>
<v-list-item-title>
نتیجهای یافت نشد!
</v-list-item-title>
</v-list-item>
</template>
</v-select>
<v-select
v-if="item.type === 'rfbuy'"
v-model="item.docRfbuy"
:items="rfbuys"
item-title="des"
item-value="code"
label="فاکتور برگشت از خرید"
variant="outlined"
density="compact"
:loading="loading"
return-object
class="mb-4"
>
<template v-slot:no-data>
<v-list-item>
<v-list-item-title>
نتیجهای یافت نشد!
</v-list-item-title>
</v-list-item>
</template>
</v-select>
<v-checkbox
v-model="item.removeBeforeTickets"
:disabled="!item.removeBeforeTicketsEnable"
label="حذف حواله‌های انباری که قبلا برای این فاکتور صادر شده است"
color="warning"
hide-details
/>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template> </template>
<style scoped> <script setup lang="ts">
import { ref, watch } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
interface Storeroom {
id: number;
name: string;
manager: string;
}
interface Document {
code: string;
des: string;
}
interface Item {
storeroom: Storeroom | null;
type: 'sell' | 'buy' | 'rfbuy' | 'rfsell' | 'wastage' | 'used';
title: string;
docSell: Document | null;
docBuy: Document | null;
docRfsell: Document | null;
docRfbuy: Document | null;
removeBeforeTickets: boolean;
removeBeforeTicketsEnable: boolean;
}
const router = useRouter()
const loading = ref(false)
// Refs
const storerooms = ref<Storeroom[]>([])
const buys = ref<Document[]>([])
const sells = ref<Document[]>([])
const rfsells = ref<Document[]>([])
const rfbuys = ref<Document[]>([])
const item = ref<Item>({
storeroom: null,
type: 'sell',
title: 'فروش',
docSell: null,
docBuy: null,
docRfsell: null,
docRfbuy: null,
removeBeforeTickets: true,
removeBeforeTicketsEnable: true
})
const snackbar = ref({
show: false,
message: '',
color: 'primary' as 'primary' | 'error' | 'success' | 'warning'
})
// Watchers
watch(() => item.value.type, (newValue) => {
if (newValue === 'sell') {
item.value.title = 'فروش'
item.value.removeBeforeTicketsEnable = true
} else if (newValue === 'buy') {
item.value.title = 'خرید'
item.value.removeBeforeTicketsEnable = true
} else if (newValue === 'rfbuy') {
item.value.title = 'برگشت از خرید'
item.value.removeBeforeTicketsEnable = true
} else if (newValue === 'rfsell') {
item.value.title = 'برگشت از فروش'
item.value.removeBeforeTicketsEnable = true
} else if (newValue === 'wastage') {
item.value.title = 'ضایعات'
item.value.removeBeforeTicketsEnable = false
item.value.removeBeforeTickets = false
} else if (newValue === 'used') {
item.value.title = 'مصرف مستقیم'
item.value.removeBeforeTicketsEnable = false
item.value.removeBeforeTickets = false
}
})
// Methods
const loadData = async () => {
loading.value = true
try {
const [storeroomsResponse, docsResponse] = await Promise.all([
axios.post('/api/storeroom/list'),
axios.post('/api/storeroom/docs/get')
])
storerooms.value = storeroomsResponse.data.data.map((element: Storeroom) => ({
...element,
name: `${element.name} انباردار : ${element.manager}`
}))
if (storerooms.value.length > 0) {
item.value.storeroom = storerooms.value[0]
}
buys.value = docsResponse.data.buys
sells.value = docsResponse.data.sells
rfsells.value = docsResponse.data.rfsells
rfbuys.value = docsResponse.data.rfbuys
} catch (error) {
console.error('Error loading data:', error)
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
}
} finally {
loading.value = false
}
}
const submit = async () => {
loading.value = true
try {
if (!item.value.storeroom) {
snackbar.value = {
show: true,
message: 'انبار انتخاب نشده است',
color: 'error'
}
return
}
// بررسی وجود فاکتور بر اساس نوع حواله
let selectedDoc: Document | null = null
switch (item.value.type) {
case 'sell':
selectedDoc = item.value.docSell
if (!selectedDoc) {
snackbar.value = {
show: true,
message: 'فاکتور فروش انتخاب نشده است',
color: 'error'
}
return
}
break
case 'buy':
selectedDoc = item.value.docBuy
if (!selectedDoc) {
snackbar.value = {
show: true,
message: 'فاکتور خرید انتخاب نشده است',
color: 'error'
}
return
}
break
case 'rfbuy':
selectedDoc = item.value.docRfbuy
if (!selectedDoc) {
snackbar.value = {
show: true,
message: 'فاکتور برگشت از خرید انتخاب نشده است',
color: 'error'
}
return
}
break
case 'rfsell':
selectedDoc = item.value.docRfsell
if (!selectedDoc) {
snackbar.value = {
show: true,
message: 'فاکتور برگشت از فروش انتخاب نشده است',
color: 'error'
}
return
}
break
default:
snackbar.value = {
show: true,
message: 'نوع حواله نامعتبر است',
color: 'error'
}
return
}
// Navigate to appropriate route
const routes = {
sell: { name: 'storeroom_new_ticket_sell', params: { doc: selectedDoc.code, storeID: item.value.storeroom.id } },
buy: { name: 'storeroom_new_ticket_buy', params: { doc: selectedDoc.code, storeID: item.value.storeroom.id } },
rfbuy: { name: 'storeroom_new_ticket_rfbuy', params: { doc: selectedDoc.code, storeID: item.value.storeroom.id } },
rfsell: { name: 'storeroom_new_ticket_rfsell', params: { doc: selectedDoc.code, storeID: item.value.storeroom.id } }
} as const
type RouteType = keyof typeof routes
const routeType = item.value.type as RouteType
router.push(routes[routeType])
} catch (error) {
console.error('Error submitting form:', error)
snackbar.value = {
show: true,
message: 'خطا در ثبت اطلاعات',
color: 'error'
}
} finally {
loading.value = false
}
}
// Lifecycle hooks
loadData()
</script>
<style scoped>
.v-card {
border-radius: 8px;
transition: all 0.3s ease;
}
.v-card:hover {
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.v-radio :deep(.v-label) {
font-size: 0.9rem;
}
</style> </style>

View file

@ -1,249 +1,402 @@
<script lang="ts"> <template>
import { defineComponent } from 'vue' <v-toolbar color="toolbar" title="حواله خروج از انبار">
import axios from "axios"; <template v-slot:prepend>
import Loading from "vue-loading-overlay"; <v-tooltip text="بازگشت" location="bottom">
import 'vue-loading-overlay/dist/css/index.css'; <template v-slot:activator="{ props }">
import VuePersianDatetimePicker from 'vue-persian-datetime-picker'; <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
import Swal from "sweetalert2"; </template>
</v-tooltip>
</template>
<v-spacer />
<v-tooltip text="تکمیل خودکار" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
@click="autofill"
variant="text"
icon="mdi-auto-fix"
class="mx-2"
/>
</template>
</v-tooltip>
<v-tooltip text="ثبت حواله خروج" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
:loading="loading"
:disabled="loading"
color="success"
icon="mdi-content-save"
@click="submit"
/>
</template>
</v-tooltip>
</v-toolbar>
export default defineComponent({ <v-container>
name: "rfbuy", <v-row>
components: { <v-col cols="12" md="4">
Loading, <v-text-field
}, v-model="ticket.date"
data: () => { label="تاریخ"
return { variant="outlined"
loading: false, density="compact"
doc: {}, readonly
ticket: { />
type: 'output', </v-col>
typeString: 'حواله خروج', <v-col cols="12" md="4">
date: '', <v-text-field
des: '', v-model="ticket.store.des"
transfer: '', label="انبار"
receiver: '', variant="outlined"
code: '', density="compact"
store: {}, readonly
person: {}, />
transferType: {}, </v-col>
referral: '' <v-col cols="12" md="4">
}, <v-text-field
transferTypes: [], v-model="ticket.person.des"
year: {}, label="خریدار"
items: [], variant="outlined"
headers: [ density="compact"
{ text: "کد", value: "commodity.code" }, readonly
{ text: "کالا", value: "commodity.name", sortable: true }, />
{ text: "واحد", value: "commodity.unit", sortable: true }, </v-col>
{ text: "مورد نیاز", value: "docCount" }, </v-row>
{ text: "از قبل", value: "countBefore" },
{ text: "باقی‌مانده", value: "remain" }, <v-row>
{ text: "تعداد", value: "commdityCount", sortable: true }, <v-col cols="12">
{ text: "ارجاع", value: "referal", sortable: true }, <v-text-field
{ text: "توضیحات", value: "des" }, v-model="ticket.des"
], label="شرح"
currencyConfig: { variant="outlined"
masked: false, density="compact"
prefix: '', />
suffix: '', </v-col>
thousands: ',', </v-row>
decimal: '.',
precision: 0, <v-row>
disableNegative: false, <v-col cols="12" md="3">
disabled: false, <v-text-field
min: 0, v-model="ticket.transfer"
max: null, label="حمل‌و‌نقل"
allowBlank: false, variant="outlined"
minimumNumberOfCharacters: 0, density="compact"
shouldRound: true, />
focusOnRight: true, </v-col>
}, <v-col cols="12" md="3">
<v-text-field
v-model="ticket.receiver"
label="تحویل"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="ticket.transferType"
:items="transferTypes"
item-title="name"
item-value="id"
label="روش تحویل"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model="ticket.referral"
label="شماره پیگیری"
variant="outlined"
density="compact"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
>
<template v-slot:item.commdityCount="{ item, index }">
<v-text-field
v-model="items[index].ticketCount"
type="number"
variant="outlined"
density="compact"
:min="0"
:max="item.remain"
@blur="(event) => { if (items[index].ticketCount === '') { items[index].ticketCount = 0 } }"
@keypress="isNumber($event)"
/>
</template>
<template v-slot:item.des="{ item, index }">
<v-text-field
v-model="items[index].des"
variant="outlined"
density="compact"
/>
</template>
<template v-slot:item.referal="{ item, index }">
<v-text-field
v-model="items[index].referral"
variant="outlined"
density="compact"
/>
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
interface TransferType {
id: number;
name: string;
}
interface Person {
des: string;
mobile?: string;
}
interface Store {
des: string;
name: string;
manager: string;
}
interface Commodity {
code: string;
name: string;
unit: string;
commdityCount: number;
docCount: number;
countBefore: number;
remain: number;
ticketCount: number;
des: string;
referral: string;
type: string;
}
interface Ticket {
type: string;
typeString: string;
date: string;
des: string;
transfer: string;
receiver: string;
code: string;
store: Store;
person: Person;
transferType: TransferType;
referral: string;
sms?: boolean;
}
interface Year {
start: string;
end: string;
now: string;
}
const router = useRouter()
const loading = ref(false)
// Refs
const doc = ref({})
const ticket = ref<Ticket>({
type: 'output',
typeString: 'حواله خروج',
date: '',
des: '',
transfer: '',
receiver: '',
code: '',
store: {} as Store,
person: {} as Person,
transferType: {} as TransferType,
referral: '',
sms: false
})
const transferTypes = ref<TransferType[]>([])
const year = ref<Year>({} as Year)
const items = ref<Commodity[]>([])
const headers = [
{ title: "کد", key: "commodity.code" },
{ title: "کالا", key: "commodity.name", sortable: true },
{ title: "واحد", key: "commodity.unit", sortable: true },
{ title: "مورد نیاز", key: "docCount" },
{ title: "از قبل", key: "countBefore" },
{ title: "باقی‌مانده", key: "remain" },
{ title: "تعداد", key: "commdityCount", sortable: true },
{ title: "ارجاع", key: "referal", sortable: true },
{ title: "توضیحات", key: "des" },
]
const snackbar = ref({
show: false,
message: '',
color: 'primary' as 'primary' | 'error' | 'success' | 'warning'
})
// Methods
const submit = async () => {
loading.value = true
try {
const errors: string[] = []
let rowsWithZeroCount = 0
let totalCount = 0
items.value.forEach((element, index) => {
if (element.ticketCount === 0) {
rowsWithZeroCount++
} else if (element.ticketCount === undefined || element.ticketCount === null) {
errors.push(`تعداد کالا در ردیف ${index + 1} وارد نشده است.`)
} else {
totalCount += element.ticketCount
}
})
if (totalCount === 0) {
errors.push('تعداد تمام کالاها صفر است!')
} }
},
methods: { if (errors.length !== 0) {
submit() { snackbar.value = {
this.loading = true; show: true,
let errors = []; message: errors.join('\n'),
let rowsWithZeroCount = 0; color: 'error'
this.items.forEach((element, index) => {
if (element.ticketCount === '') {
errors.push('تعداد کالا در ردیف ' + (index + 1) + 'وارد نشده است.');
}
else if (element.ticketCount === 0) {
rowsWithZeroCount++;
}
});
//check all values is zero
if (rowsWithZeroCount != 0) {
errors.push('تعداد تمام کالاها صفر است!');
} }
if (errors.length != 0) { return
let errorStr = '<ul>'; }
errors.forEach((item) => { errorStr += '<li>' + item + '</li>' })
errorStr += '</ul>' const response = await axios.post('/api/storeroom/ticket/insert', {
Swal.fire({ doc: doc.value,
html: errorStr, ticket: {
icon: 'error', ...ticket.value,
confirmButtonText: 'قبول' senderTel: ticket.value.person.mobile || ''
}).then((response) => { },
this.loading = false; items: items.value
}); })
if (response.data.result === 0) {
snackbar.value = {
show: true,
message: 'حواله انبار با موفقیت ثبت شد.',
color: 'success'
} }
else { setTimeout(() => {
//going to save ticket router.push('/acc/storeroom/tickets/list')
axios.post('/api/storeroom/ticket/insert', { }, 1000)
doc: this.doc, }
ticket: this.ticket, } catch (error) {
items: this.items console.error('Error submitting form:', error)
}).then((resp) => { snackbar.value = {
Swal.fire({ show: true,
text: 'حواله انبار با موفقیت ثبت شد.', message: 'خطا در ثبت اطلاعات',
icon: 'success', color: 'error'
confirmButtonText: 'قبول' }
}).then((response) => { } finally {
this.$router.push('/acc/storeroom/tickets/list'); loading.value = false
this.loading = false;
});
});
}
},
autofill() {
this.items.forEach((element, index) => {
this.items[index].ticketCount = this.items[index].remain;
this.items[index].des = 'تعداد ' + this.items[index].remain + 'مورد تحویل شد. ';
this.items[index].type = 'output';
})
},
isNumber(evt: KeyboardEvent): void {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const keyPressed: string = evt.key;
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
},
loadData() {
axios.post('/api/storeroom/doc/get/info/' + this.$route.params.doc).then((res) => {
this.doc = res.data;
this.ticket.person = res.data.person;
this.ticket.des = 'حواله خروج از انبار برای فاکتور برگشت از خرید شماره # ' + this.doc.code;
this.items = res.data.commodities;
this.items.forEach((element, index) => {
this.items[index].ticketCount = 0;
this.items[index].docCount = element.commdityCount;
this.items[index].des = '';
this.items[index].type = 'output';
})
});
axios.post('/api/storeroom/info/' + this.$route.params.storeID).then((res) => {
this.ticket.store = res.data;
this.ticket.store.des = this.ticket.store.name + ' انباردار : ' + this.ticket.store.manager
});
//load year
axios.post('/api/year/get').then((response) => {
this.year = response.data;
this.ticket.date = response.data.now;
})
//load transfer types
axios.post('/api/storeroom/transfertype/list').then((response) => {
this.transferTypes = response.data;
this.ticket.transferType = response.data[0];
})
},
},
mounted() {
this.loadData();
} }
}
const autofill = () => {
items.value.forEach((element, index) => {
const remain = Math.max(0, items.value[index].remain)
items.value[index].ticketCount = remain
items.value[index].des = remain > 0 ? `تعداد ${remain} مورد تحویل شد.` : ''
items.value[index].type = 'output'
})
}
const isNumber = (evt: KeyboardEvent): void => {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const keyPressed: string = evt.key
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
}
const loadData = async () => {
loading.value = true
try {
const [docResponse, storeResponse, yearResponse, transferTypesResponse] = await Promise.all([
axios.post(`/api/storeroom/doc/get/info/${router.currentRoute.value.params.doc}`),
axios.post(`/api/storeroom/info/${router.currentRoute.value.params.storeID}`),
axios.post('/api/year/get'),
axios.post('/api/storeroom/transfertype/list')
])
doc.value = docResponse.data
ticket.value.person = docResponse.data.person
ticket.value.des = `حواله خروج از انبار برای فاکتور برگشت از خرید شماره # ${docResponse.data.code}`
items.value = docResponse.data.commodities.map((element: Commodity) => ({
...element,
ticketCount: 0,
docCount: element.commdityCount,
des: '',
type: 'output'
}))
ticket.value.store = storeResponse.data
ticket.value.store.des = `${storeResponse.data.name} انباردار : ${storeResponse.data.manager}`
year.value = yearResponse.data
ticket.value.date = yearResponse.data.now
transferTypes.value = transferTypesResponse.data
ticket.value.transferType = transferTypesResponse.data[0]
} catch (error) {
console.error('Error loading data:', error)
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
}
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
}) })
</script> </script>
<template> <style scoped>
<div class="block block-content-full "> .v-data-table {
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> border-radius: 8px;
<h3 class="block-title text-primary-dark"> }
<button @click="$router.back()" type="button" </style>
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="mx-2 fa fa-file-export"></i>
حواله خروج از انبار
</h3>
<div class="block-options">
<button @click="autofill()" class="btn btn-sm btn-outline-primary">
<i class="fa fa-list-check me-2"></i>
تکمیل خودکار
</button>
<button :disabled="this.loading" @click="submit()" type="button" class="mx-2 btn btn-sm btn-success">
<i class="fa fa-save me-2"></i>
ثبت حواله خروج
</button>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-4">
<label class="form-label">تاریخ</label>
<date-picker class="" v-model="this.ticket.date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="year.start" :max="year.end" />
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">انبار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.store.des" type="text"
class="form-control">
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">خریدار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.person.des" type="text"
class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-12">
<label class="form-label">شرح</label>
<input v-model="this.ticket.des" type="text" class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-3">
<label class="form-label">حمل و نقل</label>
<input v-model="this.ticket.transfer" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">تحویل</label>
<input v-model="this.ticket.receiver" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">روش تحویل</label>
<select class="form-select" v-model="ticket.transferType">
<option v-for="transferType in transferTypes" :value="transferType">{{ transferType.name }}</option>
</select>
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">شماره پیگیری</label>
<input v-model="this.ticket.referral" type="text" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-sm-12 col-md-12">
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating :headers="headers"
:items="items" theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="this.loading">
<template #item-commdityCount="{ index, commdityCount, ticketCount }">
<input @blur="(event) => { if (this.items[index - 1].ticketCount === '') { this.items[index - 1].ticketCount = 0 } }"
@keypress="isNumber($event)" class="form-control form-control-sm" type="number" min="0"
:max="this.items[index - 1].remain" v-model="this.items[index - 1].ticketCount" />
</template>
<template #item-des="{ index, des }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].des" />
</template>
<template #item-referal="{ index }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].referral" />
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View file

@ -1,249 +1,401 @@
<script lang="ts"> <template>
import { defineComponent } from 'vue' <v-toolbar color="toolbar" title="حواله خروج از انبار">
import axios from "axios"; <template v-slot:prepend>
import Loading from "vue-loading-overlay"; <v-tooltip text="بازگشت" location="bottom">
import 'vue-loading-overlay/dist/css/index.css'; <template v-slot:activator="{ props }">
import VuePersianDatetimePicker from 'vue-persian-datetime-picker'; <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-tooltip text="تکمیل خودکار" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
@click="autofill"
variant="text"
icon="mdi-auto-fix"
class="mx-2"
/>
</template>
</v-tooltip>
<v-tooltip text="ثبت حواله خروج" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
:loading="loading"
:disabled="loading"
color="success"
icon="mdi-content-save"
@click="submit"
/>
</template>
</v-tooltip>
</v-toolbar>
import Swal from "sweetalert2"; <v-container>
<v-row>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.store.des"
label="انبار"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="ticket.person.des"
label="فروشنده"
variant="outlined"
density="compact"
readonly
/>
</v-col>
</v-row>
export default defineComponent({ <v-row>
name: "rfsell", <v-col cols="12">
components: { <v-text-field
Loading, v-model="ticket.des"
}, label="شرح"
data: () => { variant="outlined"
return { density="compact"
loading: false, />
doc: {}, </v-col>
ticket: { </v-row>
type: 'input',
typeString: 'حواله ورود', <v-row>
date: '', <v-col cols="12" md="3">
des: '', <v-text-field
transfer: '', v-model="ticket.transfer"
receiver: '', label="حمل‌و‌نقل"
code: '', variant="outlined"
store: {}, density="compact"
person: {}, />
transferType: {}, </v-col>
referral: '' <v-col cols="12" md="3">
}, <v-text-field
transferTypes: [], v-model="ticket.receiver"
year: {}, label="تحویل"
items: [], variant="outlined"
headers: [ density="compact"
{ text: "کد", value: "commodity.code" }, />
{ text: "کالا", value: "commodity.name", sortable: true }, </v-col>
{ text: "واحد", value: "commodity.unit", sortable: true }, <v-col cols="12" md="3">
{ text: "مورد نیاز", value: "docCount" }, <v-select
{ text: "از قبل", value: "countBefore" }, v-model="ticket.transferType"
{ text: "باقی‌مانده", value: "remain" }, :items="transferTypes"
{ text: "تعداد", value: "commdityCount", sortable: true }, item-title="name"
{ text: "ارجاع", value: "referal", sortable: true }, item-value="id"
{ text: "توضیحات", value: "des" }, label="روش تحویل"
], variant="outlined"
currencyConfig: { density="compact"
masked: false, />
prefix: '', </v-col>
suffix: '', <v-col cols="12" md="3">
thousands: ',', <v-text-field
decimal: '.', v-model="ticket.referral"
precision: 0, label="شماره پیگیری"
disableNegative: false, variant="outlined"
disabled: false, density="compact"
min: 0, />
max: null, </v-col>
allowBlank: false, </v-row>
minimumNumberOfCharacters: 0,
shouldRound: true, <v-row>
focusOnRight: true, <v-col cols="12">
}, <v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
>
<template v-slot:item.commdityCount="{ item, index }">
<v-text-field
v-model="items[index].ticketCount"
type="number"
variant="outlined"
density="compact"
:min="0"
:max="item.remain"
@blur="(event: Event) => { if (items[index].ticketCount === '') { items[index].ticketCount = 0 } }"
@keypress="isNumber($event)"
/>
</template>
<template v-slot:item.des="{ item, index }">
<v-text-field
v-model="items[index].des"
variant="outlined"
density="compact"
/>
</template>
<template v-slot:item.referal="{ item, index }">
<v-text-field
v-model="items[index].referral"
variant="outlined"
density="compact"
/>
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
interface TransferType {
id: number;
name: string;
}
interface Person {
des: string;
mobile?: string;
}
interface Store {
des: string;
name: string;
manager: string;
}
interface Commodity {
code: string;
name: string;
unit: string;
commdityCount: number;
docCount: number;
countBefore: number;
remain: number;
ticketCount: number;
des: string;
referral: string;
type: string;
}
interface Ticket {
type: string;
typeString: string;
date: string;
des: string;
transfer: string;
receiver: string;
code: string;
store: Store;
person: Person;
transferType: TransferType;
referral: string;
sms?: boolean;
}
interface Year {
start: string;
end: string;
now: string;
}
const router = useRouter()
const loading = ref(false)
// Refs
const doc = ref({})
const ticket = ref<Ticket>({
type: 'input',
typeString: 'حواله ورود از انبار',
date: '',
des: '',
transfer: '',
receiver: '',
code: '',
store: {} as Store,
person: {} as Person,
transferType: {} as TransferType,
referral: '',
sms: false
})
const transferTypes = ref<TransferType[]>([])
const year = ref<Year>({} as Year)
const items = ref<Commodity[]>([])
const headers = [
{ title: "کد", key: "commodity.code" },
{ title: "کالا", key: "commodity.name", sortable: true },
{ title: "واحد", key: "commodity.unit", sortable: true },
{ title: "مورد نیاز", key: "docCount" },
{ title: "از قبل", key: "countBefore" },
{ title: "باقی‌مانده", key: "remain" },
{ title: "تعداد", key: "commdityCount", sortable: true },
{ title: "ارجاع", key: "referal", sortable: true },
{ title: "توضیحات", key: "des" },
]
const snackbar = ref({
show: false,
message: '',
color: 'primary' as 'primary' | 'error' | 'success' | 'warning'
})
// Methods
const submit = async () => {
loading.value = true
try {
const errors: string[] = []
let rowsWithZeroCount = 0
let totalCount = 0
items.value.forEach((element, index) => {
if (element.ticketCount === 0 && element.remain !== 0) {
rowsWithZeroCount++
} else if (element.ticketCount === undefined || element.ticketCount === null) {
errors.push(`تعداد کالا در ردیف ${index + 1} وارد نشده است.`)
} else {
totalCount += element.ticketCount
}
})
if (rowsWithZeroCount !== 0) {
errors.push('تعداد تمام کالاها صفر است!')
} }
},
methods: { if (errors.length !== 0) {
submit() { snackbar.value = {
this.loading = true; show: true,
let errors = []; message: errors.join('\n'),
let rowsWithZeroCount = 0; color: 'error'
this.items.forEach((element, index) => {
if (element.ticketCount === '') {
errors.push('تعداد کالا در ردیف ' + (index + 1) + 'وارد نشده است.');
}
else if (element.ticketCount === 0 && element.remain != 0) {
rowsWithZeroCount++;
}
});
//check all values is zero
if (rowsWithZeroCount != 0) {
errors.push('تعداد تمام کالاها صفر است!');
} }
if (errors.length != 0) { return
let errorStr = '<ul>'; }
errors.forEach((item) => { errorStr += '<li>' + item + '</li>' })
errorStr += '</ul>' const response = await axios.post('/api/storeroom/ticket/insert', {
Swal.fire({ doc: doc.value,
html: errorStr, ticket: {
icon: 'error', ...ticket.value,
confirmButtonText: 'قبول' senderTel: ticket.value.person.mobile || ''
}).then((response) => { },
this.loading = false; items: items.value
}); })
if (response.data.result === 0) {
snackbar.value = {
show: true,
message: 'حواله انبار با موفقیت ثبت شد.',
color: 'success'
} }
else { setTimeout(() => {
//going to save ticket router.push('/acc/storeroom/tickets/list')
axios.post('/api/storeroom/ticket/insert', { }, 1000)
doc: this.doc, }
ticket: this.ticket, } catch (error) {
items: this.items console.error('Error submitting form:', error)
}).then((resp) => { snackbar.value = {
Swal.fire({ show: true,
text: 'حواله انبار با موفقیت ثبت شد.', message: 'خطا در ثبت اطلاعات',
icon: 'success', color: 'error'
confirmButtonText: 'قبول' }
}).then((response) => { } finally {
this.$router.push('/acc/storeroom/tickets/list'); loading.value = false
this.loading = false;
});
});
}
},
autofill() {
this.items.forEach((element, index) => {
this.items[index].ticketCount = this.items[index].docCount;
this.items[index].des = 'تعداد ' + this.items[index].ticketCount + 'مورد تحویل شد. ';
})
},
isNumber(evt: KeyboardEvent): void {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const keyPressed: string = evt.key;
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
},
loadData() {
axios.post('/api/storeroom/doc/get/info/' + this.$route.params.doc).then((res) => {
this.doc = res.data;
this.ticket.person = res.data.person;
this.ticket.des = 'حواله ورود انبار برای فاکتور برگشت از فروش شماره # ' + this.doc.code;
this.items = res.data.commodities;
this.items.forEach((element, index) => {
this.items[index].ticketCount = 0;
this.items[index].docCount = element.commdityCount;
this.items[index].des = '';
this.items[index].type = 'input';
})
});
axios.post('/api/storeroom/info/' + this.$route.params.storeID).then((res) => {
this.ticket.store = res.data;
this.ticket.store.des = this.ticket.store.name + ' انباردار : ' + this.ticket.store.manager
});
//load year
axios.post('/api/year/get').then((response) => {
this.year = response.data;
this.ticket.date = response.data.now;
})
//load transfer types
axios.post('/api/storeroom/transfertype/list').then((response) => {
this.transferTypes = response.data;
this.ticket.transferType = response.data[0];
})
},
},
mounted() {
this.loadData();
} }
}
const autofill = () => {
items.value.forEach((element, index) => {
items.value[index].ticketCount = items.value[index].docCount
items.value[index].des = `تعداد ${items.value[index].ticketCount} مورد تحویل شد.`
items.value[index].type = 'output'
})
}
const isNumber = (evt: KeyboardEvent): void => {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const keyPressed: string = evt.key
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
}
const loadData = async () => {
loading.value = true
try {
const [docResponse, storeResponse, yearResponse, transferTypesResponse] = await Promise.all([
axios.post(`/api/storeroom/doc/get/info/${router.currentRoute.value.params.doc}`),
axios.post(`/api/storeroom/info/${router.currentRoute.value.params.storeID}`),
axios.post('/api/year/get'),
axios.post('/api/storeroom/transfertype/list')
])
doc.value = docResponse.data
ticket.value.person = docResponse.data.person
ticket.value.des = `حواله خروج از انبار برای فاکتور برگشت از فروش شماره # ${docResponse.data.code}`
items.value = docResponse.data.commodities.map((element: Commodity) => ({
...element,
ticketCount: 0,
docCount: element.commdityCount,
des: '',
type: 'output'
}))
ticket.value.store = storeResponse.data
ticket.value.store.des = `${storeResponse.data.name} انباردار : ${storeResponse.data.manager}`
year.value = yearResponse.data
ticket.value.date = yearResponse.data.now
transferTypes.value = transferTypesResponse.data
ticket.value.transferType = transferTypesResponse.data[0]
} catch (error) {
console.error('Error loading data:', error)
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
}
} finally {
loading.value = false
}
}
onMounted(() => {
loadData()
}) })
</script> </script>
<template> <style scoped>
<div class="block block-content-full "> .v-data-table {
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> border-radius: 8px;
<h3 class="block-title text-primary-dark"> }
<button @click="$router.back()" type="button" </style>
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="mx-2 fa fa-file-import"></i>
حواله ورود به انبار
</h3>
<div class="block-options">
<button @click="autofill()" class="btn btn-sm btn-outline-primary">
<i class="fa fa-list-check me-2"></i>
تکمیل خودکار
</button>
<button :disabled="this.loading" @click="submit()" type="button" class="mx-2 btn btn-sm btn-success">
<i class="fa fa-save me-2"></i>
ثبت حواله ورود
</button>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-4">
<label class="form-label">تاریخ</label>
<date-picker class="" v-model="this.ticket.date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="year.start" :max="year.end" />
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">انبار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.store.des" type="text"
class="form-control">
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">خریدار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.person.des" type="text"
class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-12">
<label class="form-label">شرح</label>
<input v-model="this.ticket.des" type="text" class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-3">
<label class="form-label">حمل و نقل</label>
<input v-model="this.ticket.transfer" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">تحویل</label>
<input v-model="this.ticket.receiver" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">روش تحویل</label>
<select class="form-select" v-model="ticket.transferType">
<option v-for="transferType in transferTypes" :value="transferType">{{ transferType.name }}</option>
</select>
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">شماره پیگیری</label>
<input v-model="this.ticket.referral" type="text" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-sm-12 col-md-12">
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating :headers="headers"
:items="items" theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="this.loading">
<template #item-commdityCount="{ index, commdityCount, ticketCount }">
<input @blur="(event) => { if (this.items[index - 1].ticketCount === '') { this.items[index - 1].ticketCount = 0 } }"
@keypress="isNumber($event)" class="form-control form-control-sm" type="number" min="0"
:max="this.items[index - 1].remain" v-model="this.items[index - 1].ticketCount" />
</template>
<template #item-des="{ index, des }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].des" />
</template>
<template #item-referal="{ index }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].referral" />
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View file

@ -1,282 +1,441 @@
<script lang="ts"> <template>
import { defineComponent } from 'vue' <v-toolbar color="toolbar" title="حواله خروج از انبار">
import axios from "axios"; <template v-slot:prepend>
import Loading from "vue-loading-overlay"; <v-tooltip text="بازگشت" location="bottom">
import 'vue-loading-overlay/dist/css/index.css'; <template v-slot:activator="{ props }">
import VuePersianDatetimePicker from 'vue-persian-datetime-picker'; <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
import Swal from "sweetalert2"; </template>
</v-tooltip>
</template>
<v-spacer />
<v-switch
v-if="isPluginActive('accpro')"
v-model="ticket.sms"
:disabled="!ticket.person.mobile"
color="primary"
hide-details
class="mx-2"
>
<template v-slot:label>
<v-icon>mdi-message-text</v-icon>
پیامک
</template>
</v-switch>
<v-tooltip text="تکمیل خودکار" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
@click="autofill"
variant="text"
icon="mdi-auto-fix"
class="mx-2"
/>
</template>
</v-tooltip>
<v-tooltip text="ثبت حواله خروج" location="bottom">
<template v-slot:activator="{ props }">
<v-btn
v-bind="props"
:loading="loading"
:disabled="loading"
color="success"
icon="mdi-content-save"
@click="submit"
/>
</template>
</v-tooltip>
</v-toolbar>
export default defineComponent({ <v-container>
name: "sell", <v-row>
components: { <v-col cols="12" md="4">
Loading, <v-text-field
}, v-model="ticket.date"
data: () => { label="تاریخ"
return { variant="outlined"
loading: false, density="compact"
plugins: [], readonly
doc: {}, />
ticket: { </v-col>
type: 'output', <v-col cols="12" md="4">
typeString: 'حواله خروج', <v-text-field
date: '', v-model="ticket.store.des"
des: '', label="انبار"
transfer: '', variant="outlined"
receiver: '', density="compact"
code: '', readonly
store: {}, />
person: {}, </v-col>
transferType: {}, <v-col cols="12" md="4">
referral: '', <v-text-field
sms: false, v-model="ticket.person.des"
senderTel: 0 label="خریدار"
}, variant="outlined"
transferTypes: [], density="compact"
year: {}, readonly
items: [], />
headers: [ </v-col>
{ text: "کد", value: "commodity.code" }, </v-row>
{ text: "کالا", value: "commodity.name", sortable: true },
{ text: "واحد", value: "commodity.unit", sortable: true }, <v-row>
{ text: "مورد نیاز", value: "docCount" }, <v-col cols="12">
{ text: "از قبل", value: "countBefore" }, <v-text-field
{ text: "باقی‌مانده", value: "remain" }, v-model="ticket.des"
{ text: "تعداد", value: "commdityCount", sortable: true }, label="شرح"
{ text: "ارجاع", value: "referal", sortable: true }, variant="outlined"
{ text: "توضیحات", value: "des" }, density="compact"
], />
currencyConfig: { </v-col>
masked: false, </v-row>
prefix: '',
suffix: '', <v-row>
thousands: ',', <v-col cols="12" md="2">
decimal: '.', <v-select
precision: 0, v-model="ticket.transferType"
disableNegative: false, :items="transferTypes"
disabled: false, item-title="name"
min: 0, item-value="id"
max: null, label="روش تحویل"
allowBlank: false, variant="outlined"
minimumNumberOfCharacters: 0, density="compact"
shouldRound: true, />
focusOnRight: true, </v-col>
}, <v-col cols="12" md="3">
<v-text-field
v-model="ticket.transfer"
label="حمل‌و‌نقل/نام باربری"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="ticket.receiver"
label="تحویل گیرنده"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model="ticket.referral"
label="شماره پیگیری/قبض"
variant="outlined"
density="compact"
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="ticket.senderTel"
label="تلفن تحویل دهنده"
variant="outlined"
density="compact"
type="number"
/>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-data-table
:headers="headers"
:items="items"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
>
<template v-slot:item.commdityCount="{ item, index }">
<v-text-field
v-model="items[index].ticketCount"
type="number"
variant="outlined"
density="compact"
:min="0"
:max="item.remain"
@blur="(event) => { if (items[index].ticketCount === '') { items[index].ticketCount = 0 } }"
@keypress="isNumber($event)"
/>
</template>
<template v-slot:item.des="{ item, index }">
<v-text-field
v-model="items[index].des"
variant="outlined"
density="compact"
/>
</template>
<template v-slot:item.referal="{ item, index }">
<v-text-field
v-model="items[index].referral"
variant="outlined"
density="compact"
/>
</template>
</v-data-table>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import axios from 'axios'
import { useRouter } from 'vue-router'
interface TransferType {
id: number;
name: string;
}
interface Person {
des: string;
mobile: string;
}
interface Store {
des: string;
name: string;
manager: string;
}
interface Commodity {
code: string;
name: string;
unit: string;
commdityCount: number;
docCount: number;
countBefore: number;
remain: number;
ticketCount: number;
des: string;
referral: string;
type: string;
}
interface Ticket {
type: string;
typeString: string;
date: string;
des: string;
transfer: string;
receiver: string;
code: string;
store: Store;
person: Person;
transferType: TransferType;
referral: string;
sms: boolean;
senderTel: number;
}
interface Year {
start: string;
end: string;
now: string;
}
const router = useRouter()
const loading = ref(false)
// Refs
const doc = ref({})
const ticket = ref<Ticket>({
type: 'output',
typeString: 'حواله خروج',
date: '',
des: '',
transfer: '',
receiver: '',
code: '',
store: {} as Store,
person: {} as Person,
transferType: {} as TransferType,
referral: '',
sms: false,
senderTel: 0
})
const transferTypes = ref<TransferType[]>([])
const year = ref<Year>({} as Year)
const items = ref<Commodity[]>([])
const plugins = ref({})
const headers = [
{ title: "کد", key: "commodity.code" },
{ title: "کالا", key: "commodity.name", sortable: true },
{ title: "واحد", key: "commodity.unit", sortable: true },
{ title: "مورد نیاز", key: "docCount" },
{ title: "از قبل", key: "countBefore" },
{ title: "باقی‌مانده", key: "remain" },
{ title: "تعداد", key: "commdityCount", sortable: true },
{ title: "ارجاع", key: "referal", sortable: true },
{ title: "توضیحات", key: "des" },
]
const snackbar = ref({
show: false,
message: '',
color: 'primary' as 'primary' | 'error' | 'success' | 'warning'
})
// Methods
const submit = async () => {
loading.value = true
try {
const errors: string[] = []
let rowsWithZeroCount = 0
let totalCount = 0
items.value.forEach((element, index) => {
if (element.ticketCount === 0) {
rowsWithZeroCount++
} else if (element.ticketCount === undefined || element.ticketCount === null) {
errors.push(`تعداد کالا در ردیف ${index + 1} وارد نشده است.`)
} else {
totalCount += element.ticketCount
}
})
if (totalCount === 0) {
errors.push('تعداد تمام کالاها صفر است!')
} }
},
methods: {
submit() {
this.loading = true;
let errors = [];
let rowsWithZeroCount = 0;
this.items.forEach((element, index) => {
if (element.ticketCount === '') {
errors.push('تعداد کالا در ردیف ' + (index + 1) + 'وارد نشده است.');
}
else if (element.ticketCount === 0) {
rowsWithZeroCount++;
}
});
//check all values is zero
if (rowsWithZeroCount != 0) {
errors.push('تعداد تمام کالاها صفر است!');
}
if (errors.length != 0) {
let errorStr = '<ul>';
errors.forEach((item) => { errorStr += '<li>' + item + '</li>' })
errorStr += '</ul>'
Swal.fire({
html: errorStr,
icon: 'error',
confirmButtonText: 'قبول'
}).then((response) => {
this.loading = false;
});
}
else {
//going to save ticket
axios.post('/api/storeroom/ticket/insert', {
doc: this.doc,
ticket: this.ticket,
items: this.items
}).then((resp) => {
if (resp.data.result == 0) {
Swal.fire({
text: 'حواله انبار با موفقیت ثبت شد.',
icon: 'success',
confirmButtonText: 'قبول'
}).then((response) => {
this.$router.push('/acc/storeroom/tickets/list');
this.loading = false;
});
}
else if(resp.data.result == 2){
Swal.fire({
text: 'حواله انبار با موفقیت ثبت شد اما به دلیل کمبود اعتبار،پیامک به مشتری ارسال نشد.لطفا برای ارسال پیامک حساب خود را شارژ نمایید..',
icon: 'success',
confirmButtonText: 'قبول'
}).then((response) => {
this.$router.push('/acc/storeroom/tickets/list');
this.loading = false;
});
}
}); if (errors.length !== 0) {
snackbar.value = {
show: true,
message: errors.join('\n'),
color: 'error'
} }
}, return
autofill() { }
this.items.forEach((element, index) => {
this.items[index].ticketCount = this.items[index].remain; const response = await axios.post('/api/storeroom/ticket/insert', {
this.items[index].des = 'تعداد ' + this.items[index].remain + 'مورد تحویل شد. '; doc: doc.value,
this.items[index].type = 'output'; ticket: ticket.value,
}) items: items.value
}, })
isNumber(evt: KeyboardEvent): void {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; if (response.data.result === 0) {
const keyPressed: string = evt.key; snackbar.value = {
if (!keysAllowed.includes(keyPressed)) { show: true,
evt.preventDefault() message: 'حواله انبار با موفقیت ثبت شد.',
color: 'success'
} }
}, setTimeout(() => {
loadData() { router.push('/acc/storeroom/tickets/list')
axios.post('/api/storeroom/doc/get/info/' + this.$route.params.doc).then((res) => { }, 1000)
this.doc = res.data; } else if (response.data.result === 2) {
this.ticket.person = res.data.person; snackbar.value = {
this.ticket.des = 'حواله خروج از انبار برای فاکتور فروش شماره # ' + this.doc.code; show: true,
this.items = res.data.commodities; message: 'حواله انبار با موفقیت ثبت شد اما به دلیل کمبود اعتبار،پیامک به مشتری ارسال نشد.لطفا برای ارسال پیامک حساب خود را شارژ نمایید.',
this.items.forEach((element, index) => { color: 'warning'
this.items[index].ticketCount = 0; }
this.items[index].docCount = element.commdityCount; setTimeout(() => {
this.items[index].des = ''; router.push('/acc/storeroom/tickets/list')
this.items[index].type = 'output'; }, 1000)
}) }
}); } catch (error) {
axios.post('/api/storeroom/info/' + this.$route.params.storeID).then((res) => { console.error('Error submitting form:', error)
this.ticket.store = res.data; snackbar.value = {
this.ticket.store.des = this.ticket.store.name + ' انباردار : ' + this.ticket.store.manager show: true,
}); message: 'خطا در ثبت اطلاعات',
//load year color: 'error'
axios.post('/api/year/get').then((response) => { }
this.year = response.data; } finally {
this.ticket.date = response.data.now; loading.value = false
})
//load transfer types
axios.post('/api/storeroom/transfertype/list').then((response) => {
this.transferTypes = response.data;
this.ticket.transferType = response.data[0];
});
//load plugins
axios.post('/api/plugin/get/actives',).then((response) => {
this.plugins = response.data;
});
},
isPluginActive(plugName) {
return this.plugins[plugName] !== undefined;
},
},
mounted() {
this.loadData();
} }
}
const autofill = () => {
items.value.forEach((element, index) => {
const remain = Math.max(0, items.value[index].remain)
items.value[index].ticketCount = remain
items.value[index].des = remain > 0 ? `تعداد ${remain} مورد تحویل شد.` : ''
items.value[index].type = 'output'
})
}
const isNumber = (evt: KeyboardEvent): void => {
const keysAllowed: string[] = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
const keyPressed: string = evt.key
if (!keysAllowed.includes(keyPressed)) {
evt.preventDefault()
}
}
const loadData = async () => {
loading.value = true
try {
const [docResponse, storeResponse, yearResponse, transferTypesResponse, pluginsResponse] = await Promise.all([
axios.post(`/api/storeroom/doc/get/info/${router.currentRoute.value.params.doc}`),
axios.post(`/api/storeroom/info/${router.currentRoute.value.params.storeID}`),
axios.post('/api/year/get'),
axios.post('/api/storeroom/transfertype/list'),
axios.post('/api/plugin/get/actives')
])
doc.value = docResponse.data
ticket.value.person = docResponse.data.person
ticket.value.des = `حواله خروج از انبار برای فاکتور فروش شماره # ${docResponse.data.code}`
items.value = docResponse.data.commodities.map((element: Commodity) => ({
...element,
ticketCount: 0,
docCount: element.commdityCount,
des: '',
type: 'output'
}))
ticket.value.store = storeResponse.data
ticket.value.store.des = `${storeResponse.data.name} انباردار : ${storeResponse.data.manager}`
year.value = yearResponse.data
ticket.value.date = yearResponse.data.now
transferTypes.value = transferTypesResponse.data
ticket.value.transferType = transferTypesResponse.data[0]
plugins.value = pluginsResponse.data
} catch (error) {
console.error('Error loading data:', error)
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها',
color: 'error'
}
} finally {
loading.value = false
}
}
const isPluginActive = (plugName: string) => {
return plugins.value[plugName] !== undefined
}
// Lifecycle hooks
onMounted(() => {
loadData()
}) })
</script> </script>
<template> <style scoped>
<div class="block block-content-full "> .v-data-table {
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> border-radius: 8px;
<h3 class="block-title text-primary-dark"> }
<button @click="$router.back()" type="button" </style>
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning">
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="mx-2 fa fa-file-export"></i>
حواله خروج از انبار
</h3>
<div class="block-options">
<span v-if="isPluginActive('accpro')" class="form-check form-switch form-check-inline">
<input :disabled="this.ticket.person.mobile == ''" v-model="ticket.sms" class="form-check-input"
type="checkbox">
<label class="form-check-label"> پیامک</label>
</span>
<button @click="autofill()" class="btn btn-sm btn-outline-primary">
<i class="fa fa-list-check me-2"></i>
تکمیل خودکار
</button>
<button :disabled="this.loading" @click="submit()" type="button" class="mx-2 btn btn-sm btn-success">
<i class="fa fa-save me-2"></i>
ثبت حواله خروج
</button>
</div>
</div>
<div class="block-content pt-1 pb-3">
<div class="row">
<div class="col-sm-12 col-md-4">
<label class="form-label">تاریخ</label>
<date-picker class="" v-model="this.ticket.date" format="jYYYY/jMM/jDD" display-format="jYYYY/jMM/jDD"
:min="year.start" :max="year.end" />
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">انبار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.store.des" type="text"
class="form-control">
</div>
<div class="col-sm-12 col-md-4">
<label class="form-label">خریدار</label>
<input disabled="disabled" readonly="readonly" v-model="this.ticket.person.des" type="text"
class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-12">
<label class="form-label">شرح</label>
<input v-model="this.ticket.des" type="text" class="form-control">
</div>
</div>
<div class="row mt-1">
<div class="col-sm-12 col-md-2">
<label class="form-label">روش تحویل</label>
<select class="form-select" v-model="ticket.transferType">
<option v-for="transferType in transferTypes" :value="transferType">{{ transferType.name }}</option>
</select>
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">حملونقل/نام باربری</label>
<input v-model="this.ticket.transfer" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label">تحویل گیرنده</label>
<input v-model="this.ticket.receiver" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-3">
<label class="form-label">شماره پیگیری/قبض</label>
<input v-model="this.ticket.referral" type="text" class="form-control">
</div>
<div class="col-sm-12 col-md-2">
<label class="form-label">تلفن تحویل دهنده</label>
<input v-model="this.ticket.senderTel" type="text" class="form-control">
</div>
</div>
<div class="row mt-2">
<div class="col-sm-12 col-md-12">
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating :headers="headers"
:items="items" theme-color="#1d90ff" header-text-direction="center" body-text-direction="center"
rowsPerPageMessage="تعداد سطر" emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از"
:loading="this.loading">
<template #item-commdityCount="{ index, commdityCount, ticketCount }">
<input
@blur="(event) => { if (this.items[index - 1].ticketCount === '') { this.items[index - 1].ticketCount = 0 } }"
@keypress="isNumber($event)" class="form-control form-control-sm" type="number" min="0"
:max="this.items[index - 1].remain" v-model="this.items[index - 1].ticketCount" />
</template>
<template #item-des="{ index, des }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].des" />
</template>
<template #item-referal="{ index }">
<input class="form-control form-control-sm" type="text" v-model="this.items[index - 1].referral" />
</template>
</EasyDataTable>
</div>
</div>
</div>
</div>
</template>
<style scoped></style>

View file

@ -1,191 +1,358 @@
<script lang="ts">
import {defineComponent,ref} from 'vue'
import axios from "axios";
import Swal from "sweetalert2";
export default defineComponent({
name: "ticketList",
data: ()=>{return {
printID:'',
loading : ref(false),
inputItems:[],
inputSearchValue: '',
outputItems:[],
outputSearchValue: '',
headers: [
{ text: "عملیات", value: "operation", width: "120" },
{ text: "شماره", value: "code" },
{ text: "تاریخ", value: "date", sortable: true },
{ text: "شماره فاکتور", value: "doc.code", sortable: true, width: "100" },
{ text: "شخص", value: "person.nikename", sortable: true, width: "120" },
{ text: "توضیحات", value: "des", sortable: true, width: "300" },
]
}
},
methods: {
loadData() {
axios.post('/api/storeroom/tickets/list/input')
.then((response) => {
this.inputItems = response.data;
this.loading = false;
});
axios.post('/api/storeroom/tickets/list/output')
.then((response) => {
this.outputItems = response.data;
this.loading = false;
});
axios.post('/api/storeroom/tickets/list/input')
.then((response) => {
this.inputItems = response.data;
this.loading = false;
});
},
deleteTicket(type, code) {
Swal.fire({
text: 'آیا برای حذف این حواله مطمئن هستید؟',
icon: 'warning',
confirmButtonText: 'قبول',
showCancelButton: true,
cancelButtonText: 'انصراف'
}).then((result) => {
if (result.isConfirmed) {
this.loading = true;
axios.post('/api/storeroom/ticket/remove/' + code)
.then((response) => {
this.loading = false;
Swal.fire({
text: 'حواله انبار حذف شد.',
icon: 'success',
confirmButtonText: 'قبول'
}).then((result) => {
if (type == 'input') {
for (let z = 0; z < this.inputItems.length; z++) {
if (this.inputItems[z]['code'] == code) {
this.inputItems.splice(z, 1);
}
}
}
else if (type == 'output') {
for (let z = 0; z < this.outputItems.length; z++) {
if (this.outputItems[z]['code'] == code) {
this.outputItems.splice(z, 1);
}
}
}
});
});
}
});
}
},
beforeMount() {
this.loadData();
}
})
</script>
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" title="حواله‌های انبار">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip text="بازگشت" location="bottom">
<button @click="$router.back()" type="button" <template v-slot:activator="{ props }">
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
<i class="fa fw-bold fa-arrow-right"></i> icon="mdi-arrow-right" />
</button> </template>
<i class="mx-2 fa fa-folder-tree"></i> </v-tooltip>
حوالههای انبار </template>
</h3> <v-spacer />
<div class="block-options">
<router-link to="/acc/storeroom/new/ticket/type" class="btn btn-sm btn-primary ms-1"> <v-slide-group show-arrows>
<span class="fa fa-plus fw-bolder"></span> <v-slide-group-item>
</router-link> <v-tooltip text="حواله جدید" location="bottom">
</div> <template v-slot:activator="{ props }">
</div> <v-btn v-bind="props" color="primary" icon="mdi-plus" :to="'/acc/storeroom/new/ticket/type'" />
<div class="block-content p-0"> </template>
<div class="col-sm-12 col-md-12 m-0 p-0"> </v-tooltip>
<ul class="nav nav-pills flex-column flex-sm-row border border-secondary" id="myTab" role="tablist"> </v-slide-group-item>
<button class="flex-sm-fill text-sm-center nav-link rounded-0 active" id="profile-tab" data-bs-toggle="tab"
data-bs-target="#profile" type="button" role="tab" aria-controls="profile" aria-selected="true"> <v-slide-group-item>
<i class="fa fa-file-export me-2"></i> <v-tooltip text="تنظیمات ستون‌ها" location="bottom">
حوالههای خروج <template v-slot:activator="{ props }">
</button> <v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true" />
<button class="flex-sm-fill text-sm-center nav-link rounded-0" id="pays-tab" data-bs-toggle="tab" </template>
data-bs-target="#pays" type="button" role="tab" aria-controls="pays" aria-selected="false"> </v-tooltip>
<i class="fa fa-file-import me-2"></i> </v-slide-group-item>
حوالههای ورود </v-slide-group>
</button> </v-toolbar>
</ul>
<div class="tab-content p-0" id="myTabContent"> <v-tabs v-model="activeTab" color="primary" grow class="mb-3">
<div class="tab-pane fade show active" id="profile" role="tabpanel" aria-labelledby="profile-tab"> <v-tab value="output">
<div class="m-1"> <v-icon start>mdi-file-export</v-icon>
<div class="input-group input-group-sm"> حوالههای خروج
<span class="input-group-text"><i class="fa fa-search"></i></span> </v-tab>
<input v-model="outputSearchValue" class="form-control" type="text" placeholder="جست و جو ..."> <v-tab value="input">
</div> <v-icon start>mdi-file-import</v-icon>
</div> حوالههای ورود
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating </v-tab>
:search-value="outputSearchValue" :headers="headers" :items="outputItems" theme-color="#1d90ff" </v-tabs>
header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading"> <v-window v-model="activeTab">
<template #item-operation="{ code }"> <!-- تب حوالههای خروج -->
<div class="dropdown-center"> <v-window-item value="output">
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link" <v-text-field v-model="outputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
data-bs-toggle="dropdown" id="dropdown-align-center-alt-primary" type="button"> density="compact" hide-details class="mb-1"></v-text-field>
<i class="fa-solid fa-ellipsis"></i>
</button> <v-data-table :headers="visibleHeaders" :items="outputItems" :search="outputSearchValue" :loading="loading"
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end" hover density="compact" class="elevation-1 text-center"
style=""> :header-props="{ class: 'custom-header' }">
<router-link class="dropdown-item" :to="'/acc/storeroom/ticket/view/' + code"> <template v-slot:item="{ item }">
<i class="fa fa-eye text-success pe-2"></i> <tr>
مشاهده <td v-if="isColumnVisible('operation')" class="text-center">
</router-link> <v-menu>
<button type="button" @click="deleteTicket('output', code)" class="dropdown-item text-danger"> <template v-slot:activator="{ props }">
<i class="fa fa-trash pe-2"></i> <v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
حذف </template>
</button> <v-list>
</div> <v-list-item :to="'/acc/storeroom/ticket/view/' + item.code">
</div> <template v-slot:prepend>
</template> <v-icon color="success">mdi-eye</v-icon>
</EasyDataTable> </template>
</div> <v-list-item-title>مشاهده</v-list-item-title>
<div class="tab-pane fade" id="pays" role="tabpanel" aria-labelledby="pays-tab"> </v-list-item>
<div class="m-1"> <v-list-item @click="deleteTicket('output', item.code)">
<div class="input-group input-group-sm"> <template v-slot:prepend>
<span class="input-group-text"><i class="fa fa-search"></i></span> <v-icon color="error">mdi-delete</v-icon>
<input v-model="inputSearchValue" class="form-control" type="text" placeholder="جست و جو ..."> </template>
</div> <v-list-item-title>حذف</v-list-item-title>
</div> </v-list-item>
<EasyDataTable table-class-name="customize-table" multi-sort show-index alternating </v-list>
:search-value="inputSearchValue" :headers="headers" :items="inputItems" theme-color="#1d90ff" </v-menu>
header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر" </td>
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading"> <td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
<template #item-operation="{ code }"> <td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
<div class="dropdown-center"> <td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
<button aria-expanded="false" aria-haspopup="true" class="btn btn-sm btn-link" <td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
data-bs-toggle="dropdown" id="dropdown-align-center-alt-primary" type="button"> <td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
<i class="fa-solid fa-ellipsis"></i> </tr>
</button> </template>
<div aria-labelledby="dropdown-align-center-outline-primary" class="dropdown-menu dropdown-menu-end" </v-data-table>
style=""> </v-window-item>
<router-link class="dropdown-item" :to="'/acc/storeroom/ticket/view/' + code">
<i class="fa fa-eye text-success pe-2"></i> <!-- تب حوالههای ورود -->
مشاهده <v-window-item value="input">
</router-link> <v-text-field v-model="inputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
<button type="button" @click="deleteTicket('input', code)" class="dropdown-item text-danger"> density="compact" hide-details class="mb-1"></v-text-field>
<i class="fa fa-trash pe-2"></i>
حذف <v-data-table :headers="visibleHeaders" :items="inputItems" :search="inputSearchValue" :loading="loading" hover
</button> density="compact" class="elevation-1 text-center"
</div> :header-props="{ class: 'custom-header' }">
</div> <template v-slot:item="{ item }">
</template> <tr>
</EasyDataTable> <td v-if="isColumnVisible('operation')" class="text-center">
</div> <v-menu>
</div> <template v-slot:activator="{ props }">
</div> <v-btn variant="text" size="small" color="error" icon="mdi-menu" v-bind="props" />
</div> </template>
</div> <v-list>
<v-list-item :to="'/acc/storeroom/ticket/view/' + item.code">
<template v-slot:prepend>
<v-icon color="success">mdi-eye</v-icon>
</template>
<v-list-item-title>مشاهده</v-list-item-title>
</v-list-item>
<v-list-item @click="deleteTicket('input', item.code)">
<template v-slot:prepend>
<v-icon color="error">mdi-delete</v-icon>
</template>
<v-list-item-title>حذف</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</td>
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
</tr>
</template>
</v-data-table>
</v-window-item>
</v-window>
<v-dialog v-model="showColumnDialog" max-width="500">
<v-card>
<v-toolbar color="toolbar" title="مدیریت ستون‌ها">
<v-spacer></v-spacer>
<v-btn icon @click="showColumnDialog = false">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col v-for="header in allHeaders" :key="header.key" cols="12" sm="6">
<v-checkbox v-model="header.visible" :label="header.title" @change="updateColumnVisibility" hide-details />
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title class="text-h6">
تأیید حذف
</v-card-title>
<v-card-text>
آیا برای حذف حواله مطمئن هستید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">خیر</v-btn>
<v-btn color="error" variant="text" @click="confirmDelete">بله</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000" location="bottom">
{{ snackbar.message }}
<template v-slot:actions>
<v-btn variant="text" @click="snackbar.show = false">
بستن
</v-btn>
</template>
</v-snackbar>
</template> </template>
<style scoped> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import axios from "axios";
interface Ticket {
code: string;
date: string;
doc: {
code: string;
};
person: {
nikename: string;
};
des: string;
}
interface Header {
title: string;
key: string;
align: string;
sortable: boolean;
width: number;
visible: boolean;
}
// Refs
const loading = ref(false);
const inputItems = ref<Ticket[]>([]);
const outputItems = ref<Ticket[]>([]);
const inputSearchValue = ref('');
const outputSearchValue = ref('');
const activeTab = ref('output');
const showColumnDialog = ref(false);
// دیالوگها
const deleteDialog = ref({
show: false,
type: null as 'input' | 'output' | null,
code: null as string | null
});
const snackbar = ref({
show: false,
message: '',
color: 'primary'
});
// تعریف همه ستونها
const allHeaders = ref<Header[]>([
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
{ title: "شماره", key: "code", align: 'center', sortable: true, width: 100, visible: true },
{ title: "تاریخ", key: "date", align: 'center', sortable: true, width: 120, visible: true },
{ title: "شماره فاکتور", key: "doc.code", align: 'center', sortable: true, width: 120, visible: true },
{ title: "شخص", key: "person.nikename", align: 'center', sortable: true, width: 120, visible: true },
{ title: "توضیحات", key: "des", align: 'center', sortable: true, width: 200, visible: true },
]);
// ستونهای قابل نمایش
const visibleHeaders = computed(() => {
return allHeaders.value.filter((header: Header) => header.visible);
});
// بررسی نمایش ستون
const isColumnVisible = (key: string) => {
return allHeaders.value.find((header: Header) => header.key === key)?.visible;
};
// کلید ذخیرهسازی در localStorage
const LOCAL_STORAGE_KEY = 'hesabix_storeroom_tickets_table_columns';
// لود تنظیمات ستونها
const loadColumnSettings = () => {
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSettings) {
const visibleColumns = JSON.parse(savedSettings);
allHeaders.value.forEach(header => {
header.visible = visibleColumns.includes(header.key);
});
}
};
// ذخیره تنظیمات ستونها
const updateColumnVisibility = () => {
const visibleColumns = allHeaders.value
.filter(header => header.visible)
.map(header => header.key);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(visibleColumns));
};
// تابع فرمتکننده اعداد
const formatNumber = (value: string | number) => {
if (!value) return '0';
return Number(value).toLocaleString('fa-IR');
};
// بارگذاری دادهها
const loadData = async () => {
loading.value = true;
try {
const [inputResponse, outputResponse] = await Promise.all([
axios.post('/api/storeroom/tickets/list/input'),
axios.post('/api/storeroom/tickets/list/output')
]);
inputItems.value = inputResponse.data;
outputItems.value = outputResponse.data;
} catch (error) {
console.error('Error loading data:', error);
snackbar.value = {
show: true,
message: 'خطا در بارگذاری داده‌ها: ' + error.message,
color: 'error'
};
} finally {
loading.value = false;
}
};
// حذف حواله
const deleteTicket = (type: 'input' | 'output', code: string) => {
deleteDialog.value = {
show: true,
type,
code
};
};
// تأیید حذف
const confirmDelete = async () => {
if (!deleteDialog.value?.type || !deleteDialog.value?.code) return;
const { type, code } = deleteDialog.value;
deleteDialog.value.show = false;
try {
loading.value = true;
await axios.post('/api/storeroom/ticket/remove/' + code);
if (type === 'input') {
inputItems.value = inputItems.value.filter(item => item.code !== code);
} else {
outputItems.value = outputItems.value.filter(item => item.code !== code);
}
snackbar.value = {
show: true,
message: 'حواله انبار حذف شد.',
color: 'success'
};
} catch (error) {
console.error('Error deleting ticket:', error);
snackbar.value = {
show: true,
message: 'خطا در حذف حواله: ' + error.message,
color: 'error'
};
} finally {
loading.value = false;
deleteDialog.value = {
show: false,
type: null,
code: null
};
}
};
// مانت کامپوننت
onMounted(() => {
loadColumnSettings();
loadData();
});
</script>
<style scoped>
.v-data-table {
direction: rtl;
width: 100%;
overflow-x: auto;
}
/* استایل برای وسط‌چین کردن همه سلول‌های جدول */
:deep(.v-data-table-header th) {
text-align: center !important;
}
:deep(.v-data-table__wrapper table td) {
text-align: center !important;
}
</style> </style>

View file

@ -1,216 +1,310 @@
<script lang="ts"> <script setup lang="ts">
import {defineComponent, ref} from 'vue' import { ref, onMounted } from 'vue'
import axios from "axios"; import axios from 'axios'
import Swal from "sweetalert2"; import { useRouter } from 'vue-router'
export default defineComponent({
name: "viewInvoice",
components:{
}, interface Business {
watch:{ legal_name: string
}
}, interface Storeroom {
data:()=>{return{ manager: string
loading:ref(false), }
bid:{
legal_name:'',
},
item:{
ticket:{
id:0,
date:null,
code:null,
des:'',
storeroom:{
manager:''
}
},
rows:[],
person:{
nikename: null,
mobile:'',
address:'',
tel:'',
codeeqtesadi:'',
keshvar:'',
ostan:'',
shahr: ''
},
},
headers: [
{ text: "کالا", value: "commodity" },
{ text: "تعداد", value: "count" },
{ text: "مورد نیاز", value: "hesabdariCount" },
{ text: "باقی‌مانده", value: "remain" },
]
interface Ticket {
id: number
date: string | null
code: string | null
des: string
type: string
typeString: string
storeroom: Storeroom
}
interface Person {
nikename: string | null
mobile: string
address: string
tel: string
codeeqtesadi: string
keshvar: string
ostan: string
shahr: string
postalcode: string
}
interface Commodity {
code: string
name: string
unit: {
name: string
} }
}, }
methods: {
loadData() { interface Row {
this.loading = true; commodity: Commodity
axios.post('/api/storeroom/tickets/info/' + this.$route.params.id).then((response) => { count: number
this.item.ticket = response.data.ticket; hesabdariCount: number
this.item.person = response.data.person; remain: number
this.item.transferType = response.data.transferType; des: string
this.item.rows = response.data.commodities; referal: string
this.loading = false; }
});
axios.post('/api/business/get/info/' + localStorage.getItem('activeBid')).then((response) => { interface Item {
this.bid = response.data; ticket: Ticket
}); rows: Row[]
}, person: Person
printInvoice() { transferType: any
axios.post('/api/storeroom/print/ticket', { }
'code': this.$route.params.id,
'type': this.item.ticket.type const router = useRouter()
}).then((response) => { const loading = ref(false)
this.printID = response.data.id; const bid = ref<Business>({ legal_name: '' })
window.open(this.$API_URL + '/front/print/' + this.printID, '_blank', 'noreferrer'); const item = ref<Item>({
}); ticket: {
id: 0,
date: null,
code: null,
des: '',
type: '',
typeString: '',
storeroom: {
manager: ''
} }
}, },
mounted() { rows: [],
this.loadData(); person: {
nikename: null,
mobile: '',
address: '',
tel: '',
codeeqtesadi: '',
keshvar: '',
ostan: '',
shahr: '',
postalcode: ''
},
transferType: null
})
const headers = [
{ title: "", key: "data-table-expand" },
{ title: "کالا", key: "commodity" },
{ title: "تعداد", key: "count" },
{ title: "مورد نیاز", key: "hesabdariCount" },
{ title: "باقی‌مانده", key: "remain" },
]
const loadData = async () => {
loading.value = true
try {
const [ticketResponse, businessResponse] = await Promise.all([
axios.post(`/api/storeroom/tickets/info/${router.currentRoute.value.params.id}`),
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`)
])
item.value.ticket = ticketResponse.data.ticket
item.value.person = ticketResponse.data.person
item.value.transferType = ticketResponse.data.transferType
item.value.rows = ticketResponse.data.commodities
bid.value = businessResponse.data
} catch (error) {
console.error('Error loading data:', error)
} finally {
loading.value = false
} }
}
const printInvoice = async () => {
try {
const response = await axios.post('/api/storeroom/print/ticket', {
code: router.currentRoute.value.params.id,
type: item.value.ticket.type
})
window.open(`${import.meta.env.VITE_API_URL}/front/print/${response.data.id}`, '_blank', 'noreferrer')
} catch (error) {
console.error('Error printing invoice:', error)
}
}
onMounted(() => {
loadData()
}) })
</script> </script>
<template> <template>
<div class="block block-content-full"> <v-toolbar color="toolbar" title="مشاهده و چاپ حواله انبار">
<div id="fixed-header" class="block-header block-header-default bg-gray-light"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip text="بازگشت" location="bottom">
<button @click="$router.back()" type="button" <template v-slot:activator="{ props }">
class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
<i class="fa fw-bold fa-arrow-right"></i>
</button>
<i class="fas fa-file-invoice-dollar"></i>
مشاهده و چاپ حواله انبار
</h3>
<div class="block-options">
<button class="btn btn-sm btn-primary mx-2" @click="printInvoice()" type="button">
<i class="si si-printer me-1"></i>
<span class="d-none d-sm-inline-block">چاپ</span>
</button>
</div>
</div>
<div class="block-content pt-1 pb-3">
<b class="ps-2">اطلاعات حواله انبار</b>
<div class="row">
<div class="col-sm-6 col-md-2">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">شماره</span>
<input type="text" readonly="readonly" v-model="item.ticket.code" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-6 col-md-2">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">نوع</span>
<input type="text" readonly="readonly" v-model="item.ticket.typeString" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-6 col-md-2">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">تاریخ</span>
<input type="text" readonly="readonly" v-model="item.ticket.date" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-12 col-md-6">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">شرح</span>
<input type="text" readonly="readonly" v-model="item.ticket.des" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
</div>
<b class="ps-2">طرف حساب</b>
<div class="row">
<div class="col-sm-6 col-md-4">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">نام</span>
<input type="text" readonly="readonly" v-model="item.person.nikename" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">موبایل</span>
<input type="text" readonly="readonly" v-model="item.person.mobile" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-6 col-md-4">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">تلفن</span>
<input type="text" readonly="readonly" v-model="item.person.tel" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-6 col-md-3">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">کد پستی</span>
<input type="text" readonly="readonly" v-model="item.person.postalcode" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
<div class="col-sm-12 col-md-9">
<div class="input-group input-group-sm mb-3">
<span class="input-group-text" id="inputGroup-sizing-sm">آدرس</span>
<input type="text" readonly="readonly" v-model="item.person.address" class="form-control"
aria-describedby="inputGroup-sizing-sm">
</div>
</div>
</div>
<b class="ps-2">اقلام</b>
<EasyDataTable table-class-name="customize-table" :headers="headers" :items="item.rows" show-index alternating
theme-color="#1d90ff" header-text-direction="center" body-text-direction="center" rowsPerPageMessage="تعداد سطر"
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" rowsOfPageSeparatorMessage="از" :loading="loading">
<template #item-count="{ count, commodity }">
{{ count }} {{ commodity.unit.name }}
</template> </template>
<template #item-commodity="{ commodity }"> </v-tooltip>
{{ commodity.code }} {{ commodity.name }} </template>
</template> <v-spacer />
<template #expand="{ des, referal }"> <v-tooltip text="چاپ" location="bottom">
<div class="p-1 m-0 text-start"> <template v-slot:activator="{ props }">
شرح <v-btn
: v-bind="props"
{{ des }} @click="printInvoice"
<br /> color="primary"
ارجاع: icon="mdi-printer"
{{ referal }} />
</div> </template>
</template> </v-tooltip>
</EasyDataTable> </v-toolbar>
</div>
</div>
<v-container>
<v-card variant="outlined" class="mb-4">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-information</v-icon>
اطلاعات حواله انبار
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="2">
<v-text-field
v-model="item.ticket.code"
label="شماره"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="item.ticket.typeString"
label="نوع"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="2">
<v-text-field
v-model="item.ticket.date"
label="تاریخ"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="item.ticket.des"
label="شرح"
variant="outlined"
density="compact"
readonly
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card variant="outlined" class="mb-4">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-account</v-icon>
طرف حساب
</v-card-title>
<v-card-text>
<v-row>
<v-col cols="12" md="4">
<v-text-field
v-model="item.person.nikename"
label="نام"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.person.mobile"
label="موبایل"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="4">
<v-text-field
v-model="item.person.tel"
label="تلفن"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model="item.person.postalcode"
label="کد پستی"
variant="outlined"
density="compact"
readonly
/>
</v-col>
<v-col cols="12" md="9">
<v-text-field
v-model="item.person.address"
label="آدرس"
variant="outlined"
density="compact"
readonly
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
<v-card variant="outlined">
<v-card-title class="text-subtitle-1 font-weight-bold">
<v-icon start>mdi-package-variant</v-icon>
اقلام
</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="item.rows"
:loading="loading"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
density="compact"
show-expand
>
<template v-slot:item.count="{ item }">
{{ item.count }} {{ item.commodity.unit.name }}
</template>
<template v-slot:item.commodity="{ item }">
{{ item.commodity.code }} {{ item.commodity.name }}
</template>
<template v-slot:expanded-row="{ columns, item }">
<tr>
<td :colspan="columns.length">
<div class="pa-2 text-right">
<div>شرح: {{ item.des }}</div>
<div>ارجاع: {{ item.referal }}</div>
</div>
</td>
</tr>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-container>
</template> </template>
<style scoped> <style scoped>
table{ @media print {
font-size: small;
border: 1px solid gray;
}
.table-header{
background-color: lightgray;
}
.c-print{
background-color: white;
}
@media print
{
@page { @page {
margin-top: 0; margin-top: 0;
margin-bottom: 0; margin-bottom: 0;
} }
body { body {
padding-top: 72px; padding-top: 72px;
padding-bottom: 72px ; padding-bottom: 72px;
} }
} }
</style> </style>

View file

@ -1,96 +1,314 @@
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" title="انبارها">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip text="بازگشت" location="bottom">
<button @click="$router.back()" type="button" class="float-start d-none d-sm-none d-md-block btn btn-sm btn-link text-warning"> <template v-slot:activator="{ props }">
<i class="fa fw-bold fa-arrow-right"></i> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
</button> </template>
<i class="mx-2 fa fa-boxes-stacked"></i> </v-tooltip>
انبارها </h3> </template>
<div class="block-options"> <v-spacer />
<router-link to="/acc/storeroom/mod/" class="btn btn-sm btn-primary ms-1">
<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"
multi-sort
show-index
alternating
:search-value="searchValue" <v-slide-group show-arrows>
:headers="headers" <v-slide-group-item>
:items="items" <v-tooltip text="افزودن جدید" location="bottom">
theme-color="#1d90ff" <template v-slot:activator="{ props }">
header-text-direction="center" <v-btn v-bind="props" icon="mdi-plus" color="primary" to="/acc/storeroom/mod/" />
body-text-direction="center" </template>
rowsPerPageMessage="تعداد سطر" </v-tooltip>
emptyMessage="اطلاعاتی برای نمایش وجود ندارد" </v-slide-group-item>
rowsOfPageSeparatorMessage="از"
:loading="loading" <v-slide-group-item>
<v-tooltip text="تنظیمات ستون‌ها" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" icon="mdi-table-cog" color="primary" @click="showColumnDialog = true" />
</template>
</v-tooltip>
</v-slide-group-item>
</v-slide-group>
</v-toolbar>
<v-text-field
v-model="search"
:loading="loading"
color="green"
class="mb-0 pt-0 rounded-0"
hide-details="auto"
density="compact"
:rounded="false"
placeholder="جست و جو ..."
clearable
>
<template v-slot:prepend-inner>
<v-tooltip location="bottom" text="جستجو">
<template v-slot:activator="{ props }">
<v-icon v-bind="props" color="danger" icon="mdi-magnify" />
</template>
</v-tooltip>
</template>
</v-text-field>
<v-data-table
:headers="visibleHeaders"
:items="items"
:loading="loading"
:search="search"
class="elevation-1 text-center"
:header-props="{ class: 'custom-header' }"
>
<template v-slot:item="{ item }">
<tr>
<td v-if="isColumnVisible('operation')" class="text-center">
<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 :to="'/acc/storeroom/mod/' + item.id">
<template v-slot:prepend>
<v-icon icon="mdi-pencil" />
</template>
<v-list-item-title>ویرایش</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmDelete(item.id)">
<template v-slot:prepend>
<v-icon color="error" icon="mdi-delete" />
</template>
<v-list-item-title>حذف</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</td>
<td v-if="isColumnVisible('id')" class="text-center">{{ formatNumber(item.id) }}</td>
<td v-if="isColumnVisible('name')" class="text-center">{{ item.name }}</td>
<td v-if="isColumnVisible('manager')" class="text-center">{{ item.manager }}</td>
<td v-if="isColumnVisible('tel')" class="text-center">{{ item.tel }}</td>
<td v-if="isColumnVisible('adr')" class="text-center">{{ item.adr }}</td>
<td v-if="isColumnVisible('active')" class="text-center">
<v-chip
:color="item.active ? 'success' : 'error'"
size="small"
> >
<template #item-operation="{ id }"> {{ item.active ? 'فعال' : 'غیرفعال' }}
<router-link :to="'/acc/storeroom/mod/' + id"> </v-chip>
<i class="fa fa-edit px-2"></i> </td>
</router-link> </tr>
</template> </template>
<template #item-active="{ active }"> </v-data-table>
<label class="text-primary" v-if="active">فعال</label>
<label class="text-danger" v-else>غیرفعال</label> <v-dialog v-model="showColumnDialog" max-width="500">
</template> <v-card>
</EasyDataTable> <v-toolbar color="toolbar" title="مدیریت ستون‌ها">
</div> <v-spacer></v-spacer>
</div> <v-btn icon @click="showColumnDialog = false">
</div> <v-icon>mdi-close</v-icon>
</div> </v-btn>
</v-toolbar>
<v-card-text>
<v-row>
<v-col v-for="header in allHeaders" :key="header.key" cols="12" sm="6">
<v-checkbox
v-model="header.visible"
:label="header.title"
@change="updateColumnVisibility"
hide-details
/>
</v-col>
</v-row>
</v-card-text>
</v-card>
</v-dialog>
<v-dialog v-model="deleteDialog.show" max-width="400">
<v-card>
<v-card-title class="text-h6">
تأیید حذف
</v-card-title>
<v-card-text>
آیا برای حذف انبار مطمئن هستید؟
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="deleteDialog.show = false">خیر</v-btn>
<v-btn color="error" variant="text" @click="deleteItem">بله</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="messageDialog.show" max-width="400">
<v-card>
<v-card-title :class="messageDialog.color + ' text-h6'">
{{ messageDialog.title }}
</v-card-title>
<v-card-text class="pt-4">
{{ messageDialog.message }}
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn color="primary" variant="text" @click="messageDialog.show = false">قبول</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template> </template>
<script> <script setup>
import axios from "axios"; import { ref, computed, onMounted } from 'vue';
import Swal from "sweetalert2"; import axios from 'axios';
import {ref} from "vue";
export default { // Refs
name: "list", const loading = ref(false);
data: ()=>{return { const items = ref([]);
printID:'', const search = ref('');
searchValue: '', const showColumnDialog = ref(false);
loading : ref(true),
items:[], // دیالوگها
headers: [ const deleteDialog = ref({
{ text: "کد", value: "id" }, show: false,
{ text: "نام انبار", value: "name", sortable: true}, id: null
{ text: "انباردار", value: "manager", sortable: true}, });
{ text: "تلفن", value: "tel", sortable: true},
{ text: "آدرس", value: "adr"}, const messageDialog = ref({
{ text: "وضعیت", value: "active"}, show: false,
{ text: "عملیات", value: "operation"}, title: '',
] message: '',
}}, color: 'primary'
methods: { });
loadData(){
axios.post('/api/storeroom/list/all') // تابع فرمتکننده اعداد
.then((response)=>{ const formatNumber = (value) => {
this.items = response.data.data; if (!value) return '0';
this.loading = false; return Number(value).toLocaleString('fa-IR');
}) };
},
}, // تعریف همه ستونها
beforeMount() { const allHeaders = ref([
this.loadData(); { title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
{ title: "کد", key: "id", align: 'center', sortable: true, width: 100, visible: true },
{ title: "نام انبار", key: "name", align: 'center', sortable: true, width: 140, visible: true },
{ title: "انباردار", key: "manager", align: 'center', sortable: true, width: 120, visible: true },
{ title: "تلفن", key: "tel", align: 'center', sortable: true, width: 120, visible: true },
{ title: "آدرس", key: "adr", align: 'center', sortable: true, width: 160, visible: true },
{ title: "وضعیت", key: "active", align: 'center', sortable: true, width: 100, visible: true },
]);
// ستونهای قابل نمایش
const visibleHeaders = computed(() => {
return allHeaders.value.filter(header => header.visible);
});
// بررسی نمایش ستون
const isColumnVisible = (key) => {
return allHeaders.value.find(header => header.key === key)?.visible;
};
// کلید ذخیرهسازی در localStorage
const LOCAL_STORAGE_KEY = 'hesabix_storeroom_table_columns';
// لود تنظیمات ستونها
const loadColumnSettings = () => {
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
if (savedSettings) {
const visibleColumns = JSON.parse(savedSettings);
allHeaders.value.forEach(header => {
header.visible = visibleColumns.includes(header.key);
});
} }
} };
// ذخیره تنظیمات ستونها
const updateColumnVisibility = () => {
const visibleColumns = allHeaders.value
.filter(header => header.visible)
.map(header => header.key);
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(visibleColumns));
};
// نمایش پیام
const showMessage = (message, title = 'پیام', color = 'primary') => {
messageDialog.value = {
show: true,
title,
message,
color
};
};
// تأیید حذف
const confirmDelete = (id) => {
deleteDialog.value = {
show: true,
id
};
};
// بارگذاری دادهها
const loadData = async () => {
loading.value = true;
try {
const response = await axios.post('/api/storeroom/list/all');
items.value = response.data.data;
} catch (error) {
console.error('Error loading data:', error);
showMessage('خطا در بارگذاری داده‌ها: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// حذف آیتم
const deleteItem = async () => {
const id = deleteDialog.value.id;
deleteDialog.value.show = false;
if (!id) return;
try {
loading.value = true;
const response = await axios.post(`/api/storeroom/delete/${id}`);
if (response.data.result === 1) {
items.value = items.value.filter(item => item.id !== id);
showMessage('انبار با موفقیت حذف شد.', 'موفقیت', 'success');
} else if (response.data.result === 2) {
showMessage('انبار به دلیل داشتن موجودی قابل حذف نیست.', 'خطا', 'error');
}
} catch (error) {
console.error('Error deleting item:', error);
showMessage('خطا در حذف آیتم: ' + error.message, 'خطا', 'error');
} finally {
loading.value = false;
}
};
// مانت کامپوننت
onMounted(() => {
loadColumnSettings();
loadData();
});
</script> </script>
<style scoped> <style>
.v-data-table {
width: 100%;
overflow-x: auto;
}
/* استایل برای وسط‌چین کردن همه سلول‌های جدول */
:deep(.v-data-table-header th) {
text-align: center !important;
}
:deep(.v-data-table__wrapper table td) {
text-align: center !important;
}
/* استایل برای رنگ‌های متن */
.text-success {
color: #4caf50 !important;
}
.text-error {
color: #ff5252 !important;
}
</style> </style>

View file

@ -1,142 +1,195 @@
<template> <template>
<div class="block block-content-full "> <v-toolbar color="toolbar" title="مشخصات انبار">
<div id="fixed-header" class="block-header block-header-default bg-gray-light pt-2 pb-1"> <template v-slot:prepend>
<h3 class="block-title text-primary-dark"> <v-tooltip text="بازگشت" location="bottom">
<button type="button" @click="$router.back()" class="btn text-warning mx-2 px-2"> <template v-slot:activator="{ props }">
<i class="fa fw-bold fa-arrow-right"></i> <v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
</button> icon="mdi-arrow-right" />
مشخصات انبار </h3> </template>
<div class="block-options"> </v-tooltip>
<button @click="save()" type="button" class="btn btn-sm btn-alt-primary"> </template>
<i class="fa fa-save me-2"></i> <v-spacer />
ثبت <v-btn-toggle
</button> v-model="data.active"
</div> mandatory
</div> density="compact"
<div class="block-content py-3 vl-parent"> class="mx-2"
<loading color="blue" loader="dots" v-model:active="isLoading" :is-full-page="false"/> >
<div class="container"> <v-btn
<div class="row py-3"> :value="true"
<div class="col-sm-12 col-md-12"> size="small"
<div> :class="data.active ? 'bg-success' : ''"
<label class="me-4 text-primary">وضعیت انبار</label> >
<div class="form-check form-check-inline"> <v-icon size="small" start>mdi-check-circle</v-icon>
<input v-model="this.data.active" class="form-check-input" type="radio" value="true"> فعال
<label class="form-check-label" for="inlineCheckbox1">فعال</label> </v-btn>
</div> <v-btn
<div class="form-check form-check-inline"> :value="false"
<input v-model="this.data.active" class="form-check-input" type="radio" value="false"> size="small"
<label class="form-check-label" for="inlineCheckbox2">غیرفعال</label> :class="!data.active ? 'bg-error' : ''"
</div> >
</div> <v-icon size="small" start>mdi-close-circle</v-icon>
</div> غیرفعال
</div> </v-btn>
<div class="row"> </v-btn-toggle>
<div class="col-sm-12 col-md-6"> <v-tooltip text="ثبت" location="bottom">
<div class="form-floating mb-4"> <template v-slot:activator="{ props }">
<input v-model="data.name" class="form-control" type="text"> <v-btn v-bind="props" color="primary" icon="mdi-content-save" @click="save" :loading="isLoading" />
<label class="form-label"><span class="text-danger">(لازم)</span> نام انبار</label> </template>
</div> </v-tooltip>
<div class="form-floating mb-4"> </v-toolbar>
<input v-model="data.tel" class="form-control" type="text"> <v-container fluid>
<label class="form-label">تلفن</label> <v-row>
</div> <v-col cols="12" md="6">
</div> <v-text-field
<div class="col-sm-12 col-md-6"> v-model="data.name"
<div class="form-floating mb-4"> label="نام انبار"
<input v-model="data.manager" class="form-control" type="text"> :rules="[v => !!v || 'نام انبار الزامی است']"
<label class="form-label">انباردار</label> required
</div> variant="outlined"
</div> density="compact"
<div class="col-sm-12 col-md-12"> >
<div class="form-floating mb-4"> <template v-slot:label>
<input v-model="data.adr" class="form-control" type="text"> <span class="text-danger">(لازم)</span> نام انبار
<label class="form-label">آدرس</label> </template>
</div> </v-text-field>
</div> </v-col>
</div>
</div> <v-col cols="12" md="6">
</div> <v-text-field
</div> v-model="data.manager"
label="انباردار"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field
v-model="data.tel"
label="تلفن"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
<v-col cols="12">
<v-text-field
v-model="data.adr"
label="آدرس"
variant="outlined"
density="compact"
></v-text-field>
</v-col>
</v-row>
</v-container>
<v-snackbar
v-model="snackbar.show"
:color="snackbar.color"
:timeout="3000"
location="bottom"
>
{{ snackbar.message }}
<template v-slot:actions>
<v-btn
variant="text"
@click="snackbar.show = false"
>
بستن
</v-btn>
</template>
</v-snackbar>
</template> </template>
<script> <script setup>
import axios from "axios"; import { ref, onMounted } from 'vue';
import Swal from "sweetalert2"; import { useRoute, useRouter } from 'vue-router';
import Loading from 'vue-loading-overlay'; import axios from 'axios';
import 'vue-loading-overlay/dist/css/index.css';
import {Money3} from "v-money3";
export default { const route = useRoute();
name: "mod", const router = useRouter();
components: {
Loading,
Money3
},
data: ()=>{return{
isLoading: false,
units:'',
data: {
id:0,
name: '',
manager: '',
active: true,
tel: '',
adr: '',
},
}},
mounted() {
this.loadData(this.$route.params.id);
},
beforeRouteUpdate(to,from){
this.loadData(to.params.id);
},
methods: {
loadData(id = '') {
this.isLoading = true;
if (id != '') {
//load user info
this.isLoading = true;
axios.post('/api/storeroom/info/' + id).then((response) => {
this.data = response.data;
});
}
this.isLoading = false;
},
save() {
if (this.data.name.length === 0)
Swal.fire({
text: 'نام کالا یا خدمات الزامی است.',
icon: 'error',
confirmButtonText: 'قبول'
});
else {
this.isLoading = true;
axios.post('/api/storeroom/mod/' + this.data.id, this.data).then((response) => {
this.isLoading = false;
if (response.data.result == 2) {
Swal.fire({
text: 'قبلا ثبت شده است.',
icon: 'error',
confirmButtonText: 'قبول'
});
} else {
Swal.fire({
text: 'مشخصات انبار ثبت شد.',
icon: 'success',
confirmButtonText: 'قبول'
}).then(() => {
this.$router.push('/acc/storeroom/list')
});
}
})
}
} // Refs
const isLoading = ref(false);
const data = ref({
id: 0,
name: '',
manager: '',
active: true,
tel: '',
adr: '',
});
const snackbar = ref({
show: false,
message: '',
color: 'primary'
});
// نمایش پیام
const showMessage = (message, color = 'primary') => {
snackbar.value = {
show: true,
message,
color
};
};
// بارگذاری دادهها
const loadData = async (id = '') => {
if (!id) return;
isLoading.value = true;
try {
const response = await axios.post('/api/storeroom/info/' + id);
data.value = response.data;
} catch (error) {
console.error('Error loading data:', error);
showMessage('خطا در بارگذاری داده‌ها: ' + error.message, 'error');
} finally {
isLoading.value = false;
} }
} };
// ذخیره دادهها
const save = async () => {
if (!data.value.name) {
showMessage('نام انبار الزامی است.', 'error');
return;
}
isLoading.value = true;
try {
const response = await axios.post('/api/storeroom/mod/' + data.value.id, data.value);
if (response.data.result === 2) {
showMessage('قبلا ثبت شده است.', 'error');
} else {
showMessage('مشخصات انبار ثبت شد.', 'success');
// تاخیر در انتقال به صفحه لیست برای نمایش اسنکبار
setTimeout(() => {
router.push('/acc/storeroom/list');
}, 1000);
}
} catch (error) {
console.error('Error saving data:', error);
showMessage('خطا در ذخیره داده‌ها: ' + error.message, 'error');
} finally {
isLoading.value = false;
}
};
// مانت کامپوننت
onMounted(() => {
loadData(route.params.id);
});
</script> </script>
<style scoped> <style>
.v-radio-group {
display: flex;
justify-content: center;
gap: 1rem;
}
</style> </style>