update Warranty plugin / add storeroom ticket changes
This commit is contained in:
parent
2e4b0a68f2
commit
2d6919c660
53
hesabixCore/migrations/Version20250820232952.php
Normal file
53
hesabixCore/migrations/Version20250820232952.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250820232952 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket ADD completed TINYINT(1) DEFAULT NULL, ADD completed_at DATETIME DEFAULT NULL, ADD completed_by_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_9B4CC0F785ECDE76 FOREIGN KEY (completed_by_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket (completed_by_id)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE import_workflow_payment CHANGE amount amount NUMERIC(15, 2) DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_9B4CC0F785ECDE76
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_9B4CC0F785ECDE76 ON storeroom_ticket
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket DROP completed, DROP completed_at, DROP completed_by_id
|
||||
SQL);
|
||||
}
|
||||
}
|
47
hesabixCore/migrations/Version20250820233206.php
Normal file
47
hesabixCore/migrations/Version20250820233206.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250820233206 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL, ADD allocated_to_document_type VARCHAR(50) DEFAULT NULL, ADD allocated_by_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6802B588 FOREIGN KEY (allocated_by_id) REFERENCES user (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial (allocated_by_id)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F6802B588
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_1A5DC26F6802B588 ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP device_serial, DROP allocated_to_document_type, DROP allocated_by_id
|
||||
SQL);
|
||||
}
|
||||
}
|
35
hesabixCore/migrations/Version20250820235141.php
Normal file
35
hesabixCore/migrations/Version20250820235141.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250820235141 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP device_serial
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD device_serial VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ use App\Entity\HesabdariRow;
|
|||
use App\Entity\Log;
|
||||
use App\Entity\Permission;
|
||||
use App\Entity\User;
|
||||
use App\Entity\Year;
|
||||
use App\Service\Access;
|
||||
use App\Service\Log as LogService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
@ -55,6 +56,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$ticket->setIsPreview(false);
|
||||
$ticket->setIsApproved(true);
|
||||
$ticket->setApprovedBy($user);
|
||||
|
@ -117,6 +122,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$ticket->setIsPreview(true);
|
||||
$ticket->setIsApproved(false);
|
||||
$ticket->setApprovedBy(null);
|
||||
|
@ -179,6 +188,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
@ -262,6 +275,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'فاکتور فروش تایید شده است']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
@ -338,6 +355,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(true);
|
||||
$document->setIsApproved(false);
|
||||
$document->setApprovedBy(null);
|
||||
|
@ -413,6 +434,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
@ -483,6 +508,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
@ -547,6 +576,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(true);
|
||||
$document->setIsApproved(false);
|
||||
$document->setApprovedBy(null);
|
||||
|
@ -617,6 +650,10 @@ class ApprovalController extends AbstractController
|
|||
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']);
|
||||
}
|
||||
|
||||
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||
}
|
||||
|
||||
$document->setIsPreview(true);
|
||||
$document->setIsApproved(false);
|
||||
$document->setApprovedBy(null);
|
||||
|
@ -646,6 +683,15 @@ class ApprovalController extends AbstractController
|
|||
}
|
||||
}
|
||||
|
||||
private function checkDocumentYear(HesabdariDoc $document, Business $business, EntityManagerInterface $entityManager): bool
|
||||
{
|
||||
$year = $entityManager->getRepository(Year::class)->findOneBy([
|
||||
'bid' => $business,
|
||||
'head' => true
|
||||
]);
|
||||
return $document->getYear()->getId() == $year->getId();
|
||||
}
|
||||
|
||||
private function canUserApproveDocument(User $user, Business $business, string $documentType): bool
|
||||
{
|
||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||
|
|
|
@ -583,10 +583,10 @@ class BusinessController extends AbstractController
|
|||
'plugGhestaManager' => true,
|
||||
'plugTaxSettings' => true,
|
||||
'plugWarranty' => true,
|
||||
'plugImportWorkflow' => true,
|
||||
'inquiry' => true,
|
||||
'ai' => true,
|
||||
'warehouseManager' => true,
|
||||
'importWorkflow' => true,
|
||||
'storehelper' => true,
|
||||
];
|
||||
} elseif ($perm) {
|
||||
$result = [
|
||||
|
@ -632,18 +632,11 @@ class BusinessController extends AbstractController
|
|||
'plugGhestaManager' => $perm->isPlugGhestaManager(),
|
||||
'plugTaxSettings' => $perm->isPlugTaxSettings(),
|
||||
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
||||
'plugImportWorkflow' => $perm->isImportWorkflow(),
|
||||
'inquiry' => $perm->isInquiry(),
|
||||
'ai' => $perm->isAi(),
|
||||
'warehouseManager' => $perm->isWarehouseManager(),
|
||||
'importWorkflow' => $perm->isImportWorkflow(),
|
||||
'storehelper' => $perm->isStorehelper()
|
||||
];
|
||||
|
||||
if ($perm->isWarehouseManager()) {
|
||||
$result['commodity'] = true;
|
||||
$result['store'] = true;
|
||||
$result['plugWarranty'] = true;
|
||||
$result['permission'] = true;
|
||||
}
|
||||
}
|
||||
return $this->json($result);
|
||||
}
|
||||
|
@ -714,10 +707,10 @@ class BusinessController extends AbstractController
|
|||
$perm->setPlugGhestaManager($params['plugGhestaManager']);
|
||||
$perm->setPlugWarrantyManager($params['plugWarranty'] ?? false);
|
||||
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
||||
$perm->setImportWorkflow($params['plugImportWorkflow'] ?? false);
|
||||
$perm->setInquiry($params['inquiry']);
|
||||
$perm->setAi($params['ai']);
|
||||
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
|
||||
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
|
||||
$perm->setStorehelper($params['storehelper'] ?? false);
|
||||
$entityManager->persist($perm);
|
||||
$entityManager->flush();
|
||||
$log->insert('تنظیمات پایه', 'ویرایش دسترسیهای کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
|
||||
|
|
|
@ -71,9 +71,9 @@ class PlugWarrantyController extends AbstractController
|
|||
|
||||
$serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s')
|
||||
->andWhere('s.business = :bid')
|
||||
->andWhere('s.allocatedToDocumentId = :docId')
|
||||
->andWhere('s.activationTicketCode = :code')
|
||||
->setParameter('bid', $acc['bid'])
|
||||
->setParameter('docId', $doc->getId())
|
||||
->setParameter('code', $code)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
|
||||
|
@ -81,6 +81,7 @@ class PlugWarrantyController extends AbstractController
|
|||
$commodity = $s->getCommodity();
|
||||
return [
|
||||
'serialNumber' => $s->getSerialNumber(),
|
||||
'commoditySerial' => $s->getCommoditySerial(),
|
||||
'commodity' => $commodity ? [
|
||||
'id' => $commodity->getId(),
|
||||
'name' => $commodity->getName(),
|
||||
|
|
|
@ -60,16 +60,18 @@ class StoreroomController extends AbstractController
|
|||
public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
||||
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
||||
if (!$acc)
|
||||
throw $this->createAccessDeniedException();
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
|
||||
if (!$ticket)
|
||||
throw $this->createNotFoundException('حواله یافت نشد');
|
||||
|
||||
$file = $request->files->get('file');
|
||||
if (!$file) {
|
||||
return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400);
|
||||
return $this->json(['result' => -1, 'message' => 'فایل ارسال نشده است'], 400);
|
||||
}
|
||||
|
||||
$stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments');
|
||||
|
||||
$stored = $storage->store($file, (string) $acc['bid']->getId(), 'storeroom_attachments');
|
||||
|
||||
$archive = new ArchiveFile();
|
||||
$archive->setBid($acc['bid']);
|
||||
|
@ -82,33 +84,35 @@ class StoreroomController extends AbstractController
|
|||
$archive->setDes($request->request->get('des'));
|
||||
$archive->setRelatedDocType('storeroom_ticket');
|
||||
$archive->setRelatedDocCode($ticket->getCode());
|
||||
$archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null);
|
||||
$archive->setFileSize($stored['size'] !== null ? (string) $stored['size'] : null);
|
||||
$entityManager->persist($archive);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->json(['result'=>0]);
|
||||
return $this->json(['result' => 0]);
|
||||
}
|
||||
|
||||
#[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])]
|
||||
public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
||||
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
||||
if (!$acc)
|
||||
throw $this->createAccessDeniedException();
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
|
||||
if (!$ticket)
|
||||
throw $this->createNotFoundException('حواله یافت نشد');
|
||||
$items = $entityManager->getRepository(ArchiveFile::class)->findBy([
|
||||
'bid'=>$acc['bid'],
|
||||
'relatedDocType'=>'storeroom_ticket',
|
||||
'relatedDocCode'=>$ticket->getCode()
|
||||
], ['id'=>'DESC']);
|
||||
return $this->json(array_map(function(ArchiveFile $a){
|
||||
'bid' => $acc['bid'],
|
||||
'relatedDocType' => 'storeroom_ticket',
|
||||
'relatedDocCode' => $ticket->getCode()
|
||||
], ['id' => 'DESC']);
|
||||
return $this->json(array_map(function (ArchiveFile $a) {
|
||||
return [
|
||||
'id'=>$a->getId(),
|
||||
'filename'=>$a->getFilename(),
|
||||
'fileType'=>$a->getFileType(),
|
||||
'fileSize'=>$a->getFileSize(),
|
||||
'des'=>$a->getDes(),
|
||||
'dateSubmit'=>$a->getDateSubmit(),
|
||||
'id' => $a->getId(),
|
||||
'filename' => $a->getFilename(),
|
||||
'fileType' => $a->getFileType(),
|
||||
'fileSize' => $a->getFileSize(),
|
||||
'des' => $a->getDes(),
|
||||
'dateSubmit' => $a->getDateSubmit(),
|
||||
];
|
||||
}, $items));
|
||||
}
|
||||
|
@ -117,12 +121,13 @@ class StoreroomController extends AbstractController
|
|||
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
if (!$acc)
|
||||
throw $this->createAccessDeniedException();
|
||||
$a = $entityManager->getRepository(ArchiveFile::class)->find($id);
|
||||
if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) {
|
||||
throw $this->createNotFoundException('فایل یافت نشد');
|
||||
}
|
||||
$abs = $storage->absolutePath((string)$a->getFilename());
|
||||
$abs = $storage->absolutePath((string) $a->getFilename());
|
||||
if (!is_file($abs) || !is_readable($abs)) {
|
||||
throw $this->createNotFoundException('فایل موجود نیست');
|
||||
}
|
||||
|
@ -201,7 +206,7 @@ class StoreroomController extends AbstractController
|
|||
* @throws ReflectionException
|
||||
*/
|
||||
#[Route('/api/storeroom/docs/get', name: 'app_storeroom_get_docs')]
|
||||
public function app_storeroom_get_docs(Provider $provider,Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
|
||||
public function app_storeroom_get_docs(Provider $provider, Extractor $extractor, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc)
|
||||
|
@ -466,10 +471,8 @@ class StoreroomController extends AbstractController
|
|||
if ($content = $request->getContent()) {
|
||||
$params = json_decode($content, true);
|
||||
}
|
||||
//check parameters exist
|
||||
if ((!array_key_exists('ticket', $params)) || (!array_key_exists('items', $params)) || (!array_key_exists('doc', $params)))
|
||||
$this->createNotFoundException();
|
||||
//going to save
|
||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'id' => $params['doc']['id'],
|
||||
'bid' => $acc['bid'],
|
||||
|
@ -479,14 +482,11 @@ class StoreroomController extends AbstractController
|
|||
throw $this->createNotFoundException('سند یافت نشد');
|
||||
if ($doc->getBid()->getId() != $acc['bid']->getId())
|
||||
throw $this->createAccessDeniedException('دسترسی به این سند را ندارید.');
|
||||
//find transfer type
|
||||
if (!array_key_exists('transferType', $params['ticket']))
|
||||
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
||||
$transferType = $entityManager->getRepository(StoreroomTransferType::class)->find($params['ticket']['transferType']['id']);
|
||||
if (!$transferType)
|
||||
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
||||
|
||||
//find storeroom
|
||||
if (!array_key_exists('store', $params['ticket']))
|
||||
throw $this->createNotFoundException('انبار یافت نشد');
|
||||
$storeroom = $entityManager->getRepository(Storeroom::class)->find($params['ticket']['store']['id']);
|
||||
|
@ -494,7 +494,6 @@ class StoreroomController extends AbstractController
|
|||
throw $this->createNotFoundException('انبار یافت نشد');
|
||||
elseif ($storeroom->getBid()->getId() != $acc['bid']->getId())
|
||||
throw $this->createAccessDeniedException('دسترسی به این انبار ممکن نیست!');
|
||||
//find person
|
||||
if (!array_key_exists('person', $params['ticket']))
|
||||
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
||||
$person = $entityManager->getRepository(Person::class)->find($params['ticket']['person']['id']);
|
||||
|
@ -502,7 +501,6 @@ class StoreroomController extends AbstractController
|
|||
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
||||
elseif ($person->getBid()->getId() != $acc['bid']->getId())
|
||||
throw $this->createAccessDeniedException('دسترسی به این طرف حساب ممکن نیست!');
|
||||
|
||||
$ticket = new StoreroomTicket();
|
||||
$ticket->setSubmitter($this->getUser());
|
||||
$ticket->setDate($params['ticket']['date']);
|
||||
|
@ -515,7 +513,9 @@ class StoreroomController extends AbstractController
|
|||
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
$rand = '';
|
||||
for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; }
|
||||
for ($i = 0; $i < 8; $i++) {
|
||||
$rand .= $alphabet[random_int(0, strlen($alphabet) - 1)];
|
||||
}
|
||||
$ticket->setActivationCode($rand);
|
||||
$ticket->setReceiver($params['ticket']['receiver']);
|
||||
$ticket->setTransferType($transferType);
|
||||
|
@ -529,32 +529,25 @@ class StoreroomController extends AbstractController
|
|||
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
|
||||
}
|
||||
$entityManager->persist($ticket);
|
||||
//$entityManager->flush();
|
||||
|
||||
//going to save rows
|
||||
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
||||
'doc' => $doc
|
||||
]);
|
||||
|
||||
// Determine if warranty serials are required based on flag or provided lines
|
||||
$hasSerialLines = false;
|
||||
foreach (($params['items'] ?? []) as $it) {
|
||||
if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; }
|
||||
}
|
||||
$requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines;
|
||||
$requireWarrantySerial = (
|
||||
isset($params['ticket']['requireWarrantySerial'])
|
||||
&& $params['ticket']['requireWarrantySerial'] === true
|
||||
&& $pluginService->isActive('warranty', $acc['bid'])
|
||||
);
|
||||
if ($requireWarrantySerial) {
|
||||
if (!$pluginService->isActive('warranty', $acc['bid'])) {
|
||||
return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403);
|
||||
}
|
||||
// Validate counts up-front
|
||||
foreach ($params['items'] as $item) {
|
||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||
if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) {
|
||||
return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400);
|
||||
if ((int) ($item['ticketCount'] ?? 0) > 0 && count($lines) < (int) $item['ticketCount']) {
|
||||
return $this->json([
|
||||
'result' => -3,
|
||||
'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($params['items'] as $item) {
|
||||
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
||||
'bid' => $acc['bid'],
|
||||
|
@ -565,7 +558,6 @@ class StoreroomController extends AbstractController
|
|||
throw $this->createNotFoundException('کالا یافت نشد!');
|
||||
if (!$row->getCommodity())
|
||||
throw $this->createNotFoundException('کالا یافت نشد!');
|
||||
//check row count not upper ticket count
|
||||
if ($row->getCommdityCount() < $item['ticketCount'])
|
||||
throw $this->createNotFoundException('تعداد کالای اضافه شده بیشتر از تعداد کالا در فاکتور است.');
|
||||
$ticketItem = new StoreroomItem();
|
||||
|
@ -578,21 +570,17 @@ class StoreroomController extends AbstractController
|
|||
$ticketItem->setCommodity($row->getCommodity());
|
||||
$ticketItem->setType($item['type']);
|
||||
$entityManager->persist($ticketItem);
|
||||
|
||||
// Bind warranty serials per item if provided
|
||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||
if ($requireWarrantySerial) {
|
||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||
if ((int)$item['ticketCount'] > 0) {
|
||||
// Ensure we have an id to bind to
|
||||
if ((int) $item['ticketCount'] > 0) {
|
||||
$entityManager->flush();
|
||||
$lines = array_slice($lines, 0, (int)$item['ticketCount']);
|
||||
$lines = array_slice($lines, 0, (int) $item['ticketCount']);
|
||||
foreach ($lines as $ln) {
|
||||
$warrantyCode = $ln['warranty'] ?? null;
|
||||
$deviceSerial = $ln['serial'] ?? null;
|
||||
if (!$warrantyCode) {
|
||||
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
|
||||
}
|
||||
/** @var PlugWarrantySerial|null $serial */
|
||||
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $acc['bid'],
|
||||
'serialNumber' => $warrantyCode,
|
||||
|
@ -613,27 +601,51 @@ class StoreroomController extends AbstractController
|
|||
$entityManager->persist($serial);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!empty($lines)) {
|
||||
$entityManager->flush();
|
||||
foreach ($lines as $ln) {
|
||||
$warrantyCode = $ln['warranty'] ?? null;
|
||||
$deviceSerial = $ln['serial'] ?? null;
|
||||
if (!$warrantyCode) {
|
||||
continue;
|
||||
}
|
||||
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $acc['bid'],
|
||||
'serialNumber' => $warrantyCode,
|
||||
'commodity' => $row->getCommodity(),
|
||||
]);
|
||||
if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) {
|
||||
continue;
|
||||
}
|
||||
$serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED);
|
||||
$serial->setCommoditySerial($deviceSerial);
|
||||
$serial->setBuyer($person);
|
||||
$serial->setAllocatedToDocumentId($doc->getId());
|
||||
$serial->setAllocatedAt(new \DateTimeImmutable());
|
||||
$serial->setBoundToItemId($ticketItem->getId());
|
||||
$serial->setBoundAt(new \DateTimeImmutable());
|
||||
$serial->setActivationTicketCode($ticket->getCode());
|
||||
$serial->setActivationTicketSecret($ticket->getActivationCode());
|
||||
$entityManager->persist($serial);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire) {
|
||||
$ticket->setIsPreview(true);
|
||||
$ticket->setIsApproved(false);
|
||||
$ticket->setApprovedBy(null); // هنوز تأیید نشده
|
||||
$ticket->setApprovedBy(null);
|
||||
} else {
|
||||
$ticket->setIsPreview(false);
|
||||
$ticket->setIsApproved(true);
|
||||
$ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی
|
||||
$ticket->setApprovedBy($this->getUser());
|
||||
}
|
||||
//save logs
|
||||
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
||||
if ($pluginService->isActive('accpro', $acc['bid'])) {
|
||||
//notification to person
|
||||
if ($params['ticket']['sms'] == true) {
|
||||
$ticket->setCanShare(true);
|
||||
$entityManager->persist($ticket);
|
||||
|
@ -676,7 +688,6 @@ class StoreroomController extends AbstractController
|
|||
3
|
||||
);
|
||||
}
|
||||
|
||||
if ($smsres == 2) {
|
||||
return $this->json([
|
||||
'result' => 2
|
||||
|
@ -684,7 +695,6 @@ class StoreroomController extends AbstractController
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->json([
|
||||
'result' => 0
|
||||
]);
|
||||
|
@ -698,7 +708,7 @@ class StoreroomController extends AbstractController
|
|||
throw $this->createAccessDeniedException();
|
||||
$params = json_decode($request->getContent() ?: '{}', true);
|
||||
$status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval
|
||||
if (!in_array($status, ['in_progress','done','rejected','approved','pending_approval'])) {
|
||||
if (!in_array($status, ['in_progress', 'done', 'rejected', 'approved', 'pending_approval'])) {
|
||||
return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400);
|
||||
}
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
|
||||
|
@ -729,7 +739,7 @@ class StoreroomController extends AbstractController
|
|||
$criteria['status'] = $status;
|
||||
}
|
||||
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']);
|
||||
return $this->json(array_map(function(StoreroomTicket $t){
|
||||
return $this->json(array_map(function (StoreroomTicket $t) {
|
||||
return [
|
||||
'code' => $t->getCode(),
|
||||
'date' => $t->getDate(),
|
||||
|
@ -764,7 +774,8 @@ class StoreroomController extends AbstractController
|
|||
'getDoc',
|
||||
'getTypeString',
|
||||
'isPreview',
|
||||
'isApproved'
|
||||
'isApproved',
|
||||
'isCompleted'
|
||||
], 2);
|
||||
|
||||
foreach ($result as $key => &$ticket) {
|
||||
|
@ -779,6 +790,16 @@ class StoreroomController extends AbstractController
|
|||
} else {
|
||||
$ticket['approvedBy'] = null;
|
||||
}
|
||||
if ($ticketEntity->getCompletedBy()) {
|
||||
$completedBy = $ticketEntity->getCompletedBy();
|
||||
$ticket['completedBy'] = [
|
||||
'id' => $completedBy->getId(),
|
||||
'fullName' => $completedBy->getFullName(),
|
||||
'email' => $completedBy->getEmail()
|
||||
];
|
||||
} else {
|
||||
$ticket['completedBy'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->json($result);
|
||||
|
@ -799,7 +820,7 @@ class StoreroomController extends AbstractController
|
|||
//get items
|
||||
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
||||
$res = [];
|
||||
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2);
|
||||
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved', 'isCompleted'], 2);
|
||||
$res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0);
|
||||
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
||||
//get rows
|
||||
|
@ -916,9 +937,9 @@ class StoreroomController extends AbstractController
|
|||
} else {
|
||||
$title = 'حواله خروج از انبار';
|
||||
}
|
||||
|
||||
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool) $business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire && $doc->isApproved() !== true && $doc->isPreview() == true) {
|
||||
if ($doc->isPreview()) {
|
||||
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
|
||||
|
@ -973,4 +994,103 @@ class StoreroomController extends AbstractController
|
|||
);
|
||||
return $this->json(['id' => $pdfPid]);
|
||||
}
|
||||
|
||||
#[Route('/api/storeroom/ticket/complete/{id}', name: 'app_storeroom_ticket_complete', methods: ['POST'])]
|
||||
public function app_storeroom_ticket_complete(string $id, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, PluginService $pluginService): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc)
|
||||
throw $this->createAccessDeniedException();
|
||||
|
||||
$params = [];
|
||||
if ($content = $request->getContent()) {
|
||||
$params = json_decode($content, true);
|
||||
}
|
||||
|
||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
|
||||
'code' => $id,
|
||||
'bid' => $acc['bid']
|
||||
]);
|
||||
if (!$ticket)
|
||||
throw $this->createNotFoundException('حواله یافت نشد');
|
||||
|
||||
$requireWarrantySerial = (
|
||||
isset($params['requireWarrantySerial'])
|
||||
&& $params['requireWarrantySerial'] === true
|
||||
&& $pluginService->isActive('warranty', $acc['bid'])
|
||||
);
|
||||
|
||||
if ($pluginService->isActive('warranty', $acc['bid'])) {
|
||||
$warrantyAllocations = $params['warrantyAllocations'] ?? [];
|
||||
foreach ($warrantyAllocations as $allocation) {
|
||||
$commodityId = $allocation['commodityId'] ?? null;
|
||||
$warrantyLines = $allocation['warrantyLines'] ?? [];
|
||||
if (!$commodityId || empty($warrantyLines)) {
|
||||
if ($requireWarrantySerial) {
|
||||
return $this->json(['result' => -3, 'message' => 'سریال گارانتی برای کالا ارسال نشده است'], 400);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$commodity = $entityManager->getRepository(Commodity::class)->find($commodityId);
|
||||
if (!$commodity) {
|
||||
if ($requireWarrantySerial) {
|
||||
return $this->json(['result' => -3, 'message' => 'کالا معتبر نیست برای گارانتی'], 400);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
foreach ($warrantyLines as $line) {
|
||||
$warrantySerial = $line['warrantySerial'] ?? null;
|
||||
$deviceSerial = $line['serialNumber'] ?? null;
|
||||
$isBeforeAllocated = $line['isBeforeAllocated'] ?? false;
|
||||
if (!$warrantySerial) {
|
||||
if ($requireWarrantySerial) {
|
||||
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if ($isBeforeAllocated) {
|
||||
continue;
|
||||
}
|
||||
$warrantySerialEntity = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $acc['bid'],
|
||||
'serialNumber' => $warrantySerial,
|
||||
'commodity' => $commodity,
|
||||
'status' => PlugWarrantySerial::STATUS_AVAILABLE
|
||||
]);
|
||||
if (!$warrantySerialEntity) {
|
||||
if ($requireWarrantySerial) {
|
||||
return $this->json(['result' => -2, 'message' => "گارانتی {$warrantySerial} یافت نشد یا در دسترس نیست"], 400);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
$warrantySerialEntity->setStatus(PlugWarrantySerial::STATUS_ALLOCATED);
|
||||
$warrantySerialEntity->setAllocatedToDocumentId($ticket->getId());
|
||||
$warrantySerialEntity->setActivationTicketCode($ticket->getCode());
|
||||
$warrantySerialEntity->setActivationTicketSecret($ticket->getActivationCode());
|
||||
$warrantySerialEntity->setAllocatedToDocumentType('storeroom_ticket');
|
||||
$warrantySerialEntity->setAllocatedAt(new \DateTimeImmutable());
|
||||
$warrantySerialEntity->setAllocatedBy($this->getUser());
|
||||
if ($deviceSerial) {
|
||||
$warrantySerialEntity->setCommoditySerial($deviceSerial);
|
||||
}
|
||||
$entityManager->persist($warrantySerialEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$ticket->setCompleted(true);
|
||||
$ticket->setCompletedAt(new \DateTimeImmutable());
|
||||
$ticket->setCompletedBy($this->getUser());
|
||||
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
|
||||
$log->insert('انبارداری', 'پروسه حواله انبار با شماره ' . $ticket->getCode() . ' تکمیل شد.', $this->getUser(), $acc['bid']);
|
||||
|
||||
return $this->json([
|
||||
'result' => 0,
|
||||
'message' => 'پروسه با موفقیت تکمیل شد'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -142,7 +142,7 @@ class Permission
|
|||
private ?bool $ai = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $warehouseManager = null;
|
||||
private ?bool $storehelper = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $importWorkflow = null;
|
||||
|
@ -656,14 +656,14 @@ class Permission
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function isWarehouseManager(): ?bool
|
||||
public function isStorehelper(): ?bool
|
||||
{
|
||||
return $this->warehouseManager;
|
||||
return $this->storehelper;
|
||||
}
|
||||
|
||||
public function setWarehouseManager(?bool $warehouseManager): static
|
||||
public function setStorehelper(?bool $storehelper): static
|
||||
{
|
||||
$this->warehouseManager = $warehouseManager;
|
||||
$this->storehelper = $storehelper;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
|
|
@ -82,6 +82,12 @@ class PlugWarrantySerial
|
|||
#[ORM\ManyToOne]
|
||||
private ?Person $buyer = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 50, nullable: true)]
|
||||
private ?string $allocatedToDocumentType = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?User $allocatedBy = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 32, nullable: true)]
|
||||
private ?string $activationTicketCode = null;
|
||||
|
||||
|
@ -152,6 +158,12 @@ class PlugWarrantySerial
|
|||
public function getBuyer(): ?Person { return $this->buyer; }
|
||||
public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; }
|
||||
|
||||
public function getAllocatedToDocumentType(): ?string { return $this->allocatedToDocumentType; }
|
||||
public function setAllocatedToDocumentType(?string $type): self { $this->allocatedToDocumentType = $type; return $this; }
|
||||
|
||||
public function getAllocatedBy(): ?User { return $this->allocatedBy; }
|
||||
public function setAllocatedBy(?User $user): self { $this->allocatedBy = $user; return $this; }
|
||||
|
||||
public function getActivationTicketCode(): ?string { return $this->activationTicketCode; }
|
||||
public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; }
|
||||
public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; }
|
||||
|
|
|
@ -96,6 +96,17 @@ class StoreroomTicket
|
|||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $approvedBy = null;
|
||||
|
||||
// Completion fields
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $completed = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $completedAt = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $completedBy = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->storeroomItems = new ArrayCollection();
|
||||
|
@ -409,4 +420,38 @@ class StoreroomTicket
|
|||
$this->approvedBy = $approvedBy;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Completion methods
|
||||
public function isCompleted(): ?bool
|
||||
{
|
||||
return $this->completed;
|
||||
}
|
||||
|
||||
public function setCompleted(?bool $completed): static
|
||||
{
|
||||
$this->completed = $completed;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompletedAt(): ?\DateTimeImmutable
|
||||
{
|
||||
return $this->completedAt;
|
||||
}
|
||||
|
||||
public function setCompletedAt(?\DateTimeImmutable $completedAt): static
|
||||
{
|
||||
$this->completedAt = $completedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCompletedBy(): ?User
|
||||
{
|
||||
return $this->completedBy;
|
||||
}
|
||||
|
||||
public function setCompletedBy(?User $completedBy): static
|
||||
{
|
||||
$this->completedBy = $completedBy;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -137,7 +137,7 @@ class Access
|
|||
'user'=>$this->user
|
||||
]);
|
||||
|
||||
if($warehousePermission && $warehousePermission->isWarehouseManager()){
|
||||
if($warehousePermission && $warehousePermission->isStorehelper()){
|
||||
$warehouseRoles = ['commodity', 'store', 'plugWarrantyManager'];
|
||||
if(in_array($roll, $warehouseRoles)){
|
||||
return $accessArray;
|
||||
|
|
|
@ -12,35 +12,32 @@
|
|||
<v-col cols="12" md="6">
|
||||
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
||||
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
||||
<template #append>
|
||||
<v-btn icon small @click="openScanner" :disabled="isEdit" color="primary" variant="text">
|
||||
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
||||
</v-btn>
|
||||
<template #prepend>
|
||||
<v-tooltip bottom size="small">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-icon v-bind="props" color="primary" @click.stop="showQrScanner = true">mdi-barcode-scan</v-icon>
|
||||
</template>
|
||||
<span>اسکن بارکد</span>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<Hcommoditysearch
|
||||
:model-value="formData.commodity_id ?? undefined"
|
||||
<Hcommoditysearch :model-value="formData.commodity_id ?? undefined"
|
||||
@update:modelValue="(val: number | Record<string, any>) => { formData.commodity_id = typeof val === 'number' ? val : (val as any)?.id ?? null }"
|
||||
:return-object="false"
|
||||
label="محصول *"
|
||||
:rules="[rules.commodity]"
|
||||
required
|
||||
class="serial-commodity"
|
||||
/>
|
||||
:return-object="false" label="محصول *" :rules="[rules.commodity]" required class="serial-commodity" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]" :ignore-year-range="true" dense
|
||||
outlined hide-details="auto" />
|
||||
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]"
|
||||
:ignore-year-range="true" dense outlined hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
||||
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense outlined
|
||||
hide-details="auto" />
|
||||
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense
|
||||
outlined hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
|
@ -70,35 +67,7 @@
|
|||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="showQrScanner" :max-width="isMobile ? '95vw' : 560" persistent>
|
||||
<v-card class="qr-card">
|
||||
<v-card-title class="qr-title">
|
||||
<v-icon left color="primary">mdi-qrcode-scan</v-icon>
|
||||
اسکن کد QR/بارکد
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="qr-wrap">
|
||||
<div id="reader" ref="readerRef" class="qr-reader"></div>
|
||||
</div>
|
||||
<div class="qr-status">
|
||||
<v-alert v-if="scanError" type="error" variant="tonal" density="comfortable">
|
||||
{{ scanError }}
|
||||
</v-alert>
|
||||
<v-progress-circular v-if="loadingScan" indeterminate size="28" class="mt-3" color="primary" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="qr-actions">
|
||||
<v-btn :disabled="loadingScan" variant="outlined" color="primary" prepend-icon="mdi-camera-switch"
|
||||
@click="switchCamera">
|
||||
تغییر دوربین
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeScanner">انصراف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<BarcodeScanner v-model="showQrScanner" @detected="handleBarcodeScan" />
|
||||
|
||||
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
|
||||
{{ notificationText }}
|
||||
|
@ -110,9 +79,9 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { Html5Qrcode, Html5QrcodeSupportedFormats, Html5QrcodeScannerState } from 'html5-qrcode'
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
||||
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
|
@ -195,93 +164,6 @@ const showNotify = (t: string, c: 'success' | 'error' | 'warning' | 'info' = 'su
|
|||
}
|
||||
|
||||
const showQrScanner = ref(false)
|
||||
const readerRef = ref<HTMLElement | null>(null)
|
||||
let qr: Html5Qrcode | null = null
|
||||
const loadingScan = ref(false)
|
||||
const scanError = ref('')
|
||||
const cameras = ref<{ id: string; label: string }[]>([])
|
||||
const currentCamIndex = ref(0)
|
||||
const qrboxSize = ref(240)
|
||||
|
||||
const computeQrBox = () => {
|
||||
const el = readerRef.value
|
||||
if (!el) return 260
|
||||
const w = Math.max(320, Math.floor(el.clientWidth))
|
||||
const size = Math.max(220, Math.min(380, Math.floor(w * 0.66)))
|
||||
return size
|
||||
}
|
||||
|
||||
const openScanner = async () => {
|
||||
showQrScanner.value = true
|
||||
await nextTick()
|
||||
await startScanner()
|
||||
}
|
||||
|
||||
const startScanner = async () => {
|
||||
try {
|
||||
loadingScan.value = true
|
||||
scanError.value = ''
|
||||
qrboxSize.value = computeQrBox()
|
||||
|
||||
const devices = await Html5Qrcode.getCameras()
|
||||
if (!devices.length) { throw new Error('دوربین یافت نشد') }
|
||||
cameras.value = devices.map(d => ({ id: d.id, label: d.label }))
|
||||
if (currentCamIndex.value >= cameras.value.length) currentCamIndex.value = 0
|
||||
|
||||
if (qr && (qr.getState?.() === Html5QrcodeScannerState.SCANNING)) await stopScanner()
|
||||
if (!qr) qr = new Html5Qrcode('reader', {
|
||||
verbose: false,
|
||||
formatsToSupport: [
|
||||
Html5QrcodeSupportedFormats.QR_CODE,
|
||||
Html5QrcodeSupportedFormats.CODE_128,
|
||||
Html5QrcodeSupportedFormats.CODE_39,
|
||||
Html5QrcodeSupportedFormats.EAN_13,
|
||||
Html5QrcodeSupportedFormats.UPC_A,
|
||||
Html5QrcodeSupportedFormats.DATA_MATRIX
|
||||
],
|
||||
experimentalFeatures: { useBarCodeDetectorIfSupported: true }
|
||||
})
|
||||
|
||||
await qr.start(
|
||||
{ deviceId: { exact: cameras.value[currentCamIndex.value].id } },
|
||||
{ fps: 12, qrbox: { width: qrboxSize.value, height: qrboxSize.value }, aspectRatio: 1.333 },
|
||||
(decodedText: string) => {
|
||||
if (decodedText) {
|
||||
formData.value.serialNumber = decodedText.trim()
|
||||
showNotify('کد با موفقیت اسکن شد', 'success')
|
||||
closeScanner()
|
||||
}
|
||||
},
|
||||
(_err: string) => { }
|
||||
)
|
||||
} catch (e: any) {
|
||||
scanError.value = e?.message || 'خطا در راهاندازی دوربین'
|
||||
} finally {
|
||||
loadingScan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopScanner = async () => {
|
||||
if (!qr) return
|
||||
try {
|
||||
const state = qr.getState?.()
|
||||
if (state === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
||||
await qr.clear()
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const closeScanner = async () => {
|
||||
await stopScanner()
|
||||
showQrScanner.value = false
|
||||
}
|
||||
|
||||
const switchCamera = async () => {
|
||||
if (!cameras.value.length) return
|
||||
currentCamIndex.value = (currentCamIndex.value + 1) % cameras.value.length
|
||||
await stopScanner()
|
||||
await nextTick()
|
||||
await startScanner()
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const res = await form.value?.validate()
|
||||
|
@ -297,7 +179,6 @@ const save = async () => {
|
|||
}
|
||||
|
||||
const close = () => {
|
||||
closeScanner()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
|
@ -346,13 +227,24 @@ const handleCommoditySelect = (c: any) => {
|
|||
|
||||
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
||||
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
||||
onBeforeUnmount(() => { closeScanner() })
|
||||
|
||||
const handleBarcodeScan = (val: string) => {
|
||||
formData.value.serialNumber = val
|
||||
showQrScanner.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* normalize Hcommoditysearch height with other inputs */
|
||||
.serial-commodity :deep(.v-field) { min-height: 56px; }
|
||||
.serial-commodity :deep(.v-field__input) { padding-top: 14px; padding-bottom: 14px; }
|
||||
.serial-commodity :deep(.v-field) {
|
||||
min-height: 56px;
|
||||
}
|
||||
|
||||
.serial-commodity :deep(.v-field__input) {
|
||||
padding-top: 14px;
|
||||
padding-bottom: 14px;
|
||||
}
|
||||
|
||||
#qr-shaded-region {
|
||||
display: none !important;
|
||||
}
|
||||
|
@ -453,7 +345,11 @@ video {
|
|||
}
|
||||
}
|
||||
|
||||
.v-input.v-input--horizontal.v-input--center-affix.v-input--density-compact.v-theme--light.v-locale--is-rtl.v-input--error.v-text-field.my-0 {
|
||||
height: 3rem;
|
||||
.v-input--density-compact .v-field--variant-outlined {
|
||||
height: 3rem !important;
|
||||
}
|
||||
|
||||
.mdi-barcode-scan::before {
|
||||
font-size: 25px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
199
webUI/src/components/widgets/BarcodeScanner.vue
Normal file
199
webUI/src/components/widgets/BarcodeScanner.vue
Normal file
|
@ -0,0 +1,199 @@
|
|||
<template>
|
||||
<v-dialog v-model="internalShow" max-width="500" persistent>
|
||||
<v-card>
|
||||
<v-toolbar color="primary" dark>
|
||||
<v-toolbar-title>اسکن بارکد</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="close"><v-icon>mdi-close</v-icon></v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<div class="scanner-container">
|
||||
<div v-if="overlay" class="scanner-overlay">
|
||||
<div class="scanner-line"></div>
|
||||
<div class="scanner-corner top-left"></div>
|
||||
<div class="scanner-corner top-right"></div>
|
||||
<div class="scanner-corner bottom-left"></div>
|
||||
<div class="scanner-corner bottom-right"></div>
|
||||
</div>
|
||||
<qrcode-stream :camera="camera" :torch="isFlashOn" :formats="formats"
|
||||
:track="overlay ? paintOutline : undefined" @detect="onDetect" @init="onInit" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions class="pa-2">
|
||||
<v-btn @click="toggleFlash" :color="isFlashOn ? 'primary' : 'grey'">
|
||||
<v-icon start>mdi-flash</v-icon> فلش
|
||||
</v-btn>
|
||||
<v-btn @click="toggleCamera" :disabled="availableCameras.length < 2">
|
||||
<v-icon start>mdi-camera-flip</v-icon>
|
||||
{{ isBackCamera ? 'عقب' : 'جلو' }}
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" variant="tonal" @click="close">بستن</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { QrcodeStream } from 'vue3-qrcode-reader'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: Boolean,
|
||||
formats: { type: Array as () => string[], default: () => ['QR_CODE', 'EAN_13', 'CODE_128'] },
|
||||
overlay: { type: Boolean, default: true }
|
||||
})
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: boolean): void
|
||||
(e: 'detected', barcode: string): void
|
||||
(e: 'error', error: Error): void
|
||||
(e: 'close'): void
|
||||
}>()
|
||||
|
||||
const internalShow = ref(props.modelValue)
|
||||
watch(() => props.modelValue, val => internalShow.value = val)
|
||||
watch(internalShow, val => emit('update:modelValue', val))
|
||||
|
||||
const camera = ref<'auto' | 'off' | 'user' | 'environment'>('environment')
|
||||
const isFlashOn = ref(false)
|
||||
const availableCameras = ref<MediaDeviceInfo[]>([])
|
||||
const isBackCamera = ref(true)
|
||||
|
||||
const paintOutline = (location: any, ctx: CanvasRenderingContext2D) => {
|
||||
if (!location) return
|
||||
const { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } = location
|
||||
ctx.strokeStyle = '#00ff00'
|
||||
ctx.lineWidth = 4
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(topLeftCorner.x, topLeftCorner.y)
|
||||
ctx.lineTo(topRightCorner.x, topRightCorner.y)
|
||||
ctx.lineTo(bottomRightCorner.x, bottomRightCorner.y)
|
||||
ctx.lineTo(bottomLeftCorner.x, bottomLeftCorner.y)
|
||||
ctx.closePath()
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
const onInit = (promise: Promise<void>) => {
|
||||
promise.catch((err: Error) => {
|
||||
emit('error', err)
|
||||
close()
|
||||
})
|
||||
}
|
||||
|
||||
const onDetect = (promise: Promise<any>) => {
|
||||
promise.then(result => {
|
||||
if (result?.content) {
|
||||
emit('detected', result.content)
|
||||
close()
|
||||
}
|
||||
}).catch(err => console.error('Error decoding QR:', err))
|
||||
}
|
||||
|
||||
const toggleFlash = () => isFlashOn.value = !isFlashOn.value
|
||||
|
||||
const toggleCamera = () => {
|
||||
camera.value = camera.value === 'environment' ? 'user' : 'environment'
|
||||
isBackCamera.value = camera.value === 'environment'
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
internalShow.value = false
|
||||
emit('close')
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const devices = await navigator.mediaDevices.enumerateDevices();
|
||||
availableCameras.value = devices.filter(device => device.kind === 'videoinput')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scanner-container {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
background: #000;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scanner-container video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.scanner-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scanner-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background: #00ff00;
|
||||
animation: scan 2s linear infinite;
|
||||
}
|
||||
|
||||
.scanner-corner {
|
||||
position: absolute;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-color: #00ff00;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
}
|
||||
|
||||
.scanner-corner.top-left {
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
border-top-width: 4px;
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.scanner-corner.top-right {
|
||||
top: 20%;
|
||||
right: 20%;
|
||||
border-top-width: 4px;
|
||||
border-right-width: 4px;
|
||||
}
|
||||
|
||||
.scanner-corner.bottom-left {
|
||||
bottom: 20%;
|
||||
left: 20%;
|
||||
border-bottom-width: 4px;
|
||||
border-left-width: 4px;
|
||||
}
|
||||
|
||||
.scanner-corner.bottom-right {
|
||||
bottom: 20%;
|
||||
right: 20%;
|
||||
border-bottom-width: 4px;
|
||||
border-right-width: 4px;
|
||||
}
|
||||
|
||||
@keyframes scan {
|
||||
0% {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateY(50px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(-50px);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -153,6 +153,7 @@ const fa_lang = {
|
|||
storeroom: "انبار",
|
||||
storeroom_title: "انبارداری",
|
||||
storeroom_ticket: "حواله انبار",
|
||||
storeroom_ticket_helper: "کمک انباردار",
|
||||
storerooms: "انبارها",
|
||||
commodity_exist_count: "موجودی کالا",
|
||||
inventory: "موجودی کالا",
|
||||
|
|
|
@ -974,12 +974,24 @@ const router = createRouter({
|
|||
component: () =>
|
||||
import('../views/acc/storeroom/io/ticketList.vue'),
|
||||
},
|
||||
{
|
||||
path: 'storeroom/tickets/list/helper',
|
||||
name: 'storeroom_tickets_list_helper',
|
||||
component: () =>
|
||||
import('../views/acc/storeroom/io/ticketListHelper.vue'),
|
||||
},
|
||||
{
|
||||
path: 'storeroom/ticket/view/:id',
|
||||
name: 'storeroom_ticket_view',
|
||||
component: () =>
|
||||
import('../views/acc/storeroom/io/view.vue'),
|
||||
},
|
||||
{
|
||||
path: 'storeroom/ticket/complete/:id',
|
||||
name: 'storeroom_ticket_complete',
|
||||
component: () =>
|
||||
import('../views/acc/storeroom/io/complete.vue'),
|
||||
},
|
||||
{
|
||||
path: 'storeroom/new/ticket/buy/:doc/:storeID',
|
||||
name: 'storeroom_new_ticket_buy',
|
||||
|
|
|
@ -217,6 +217,7 @@ export default {
|
|||
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||
{ path: '/acc/plugins/custominvoice/templates', key: 'I', label: 'قالبهای فاکتور', ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('custominvoice') },
|
||||
{ path: '/acc/plugins/import-workflow', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.importWorkflow },
|
||||
{ path: '/acc/storeroom/tickets/list/helper', key: 'I', label: this.$t('drawer.storeroom_ticket_helper'), ctrl: true, shift: true, permission: () => (this.permissions.storehelper || this.permissions.store) && this.isPluginActive('accpro') },
|
||||
];
|
||||
},
|
||||
restorePermissions(shortcuts) {
|
||||
|
@ -555,7 +556,7 @@ export default {
|
|||
</v-list-item>
|
||||
</v-list-group>
|
||||
<v-list-subheader color="primary">{{ $t('drawer.acc_store_tools') }}</v-list-subheader>
|
||||
<v-list-group v-show="permissions.store">
|
||||
<v-list-group v-show="permissions.store || permissions.storehelper">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-list-item class="text-dark" v-bind="props" prepend-icon="mdi-store"
|
||||
:title="$t('drawer.storeroom_title')"></v-list-item>
|
||||
|
@ -587,6 +588,13 @@ export default {
|
|||
</v-tooltip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="(permissions.store || permissions.storehelper) && this.isPluginActive('accpro')" to="/acc/storeroom/tickets/list/helper">
|
||||
<v-list-item-title>
|
||||
{{ $t('drawer.storeroom_ticket_helper') }}
|
||||
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/storeroom/tickets/list/helper')
|
||||
}}</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="permissions.store" to="/acc/storeroom/commodity/check/exist">
|
||||
<v-list-item-title>
|
||||
{{ $t('drawer.commodity_exist_count') }}
|
||||
|
|
|
@ -11,8 +11,8 @@
|
|||
</p>
|
||||
<div class="plugin-version">
|
||||
<span class="version-badge">نسخه 1.0.0</span>
|
||||
<span v-if="isPluginActive('importWorkflow')" class="status-badge active">فعال</span>
|
||||
<RouterLink to="/acc/plugin-center/view-end/import-workflow" v-if="!isPluginActive('importWorkflow')">
|
||||
<span v-if="isPluginActive('import-workflow')" class="status-badge active">فعال</span>
|
||||
<RouterLink to="/acc/plugin-center/view-end/import-workflow" v-if="!isPluginActive('import-workflow')">
|
||||
<span class="status-badge active text-white d-flex align-items-center">
|
||||
<i class="fa fa-shopping-cart me-1"></i>
|
||||
خرید
|
||||
|
@ -123,7 +123,7 @@
|
|||
</div>
|
||||
|
||||
<div class="intro-footer">
|
||||
<div v-if="isPluginActive('importWorkflow')" class="action-buttons">
|
||||
<div v-if="isPluginActive('import-workflow')" class="action-buttons">
|
||||
<router-link to="/acc/plugins/import-workflow/list" class="btn btn-success">
|
||||
<i class="fas fa-list"></i>
|
||||
ورود به مدیریت واردات
|
||||
|
|
|
@ -11,10 +11,14 @@
|
|||
<v-spacer></v-spacer>
|
||||
<v-btn color="info" variant="outlined" prepend-icon="mdi-share-variant" @click="openActivationLinkDialog"
|
||||
class="ml-2">
|
||||
اشتراک گذاری لینک فعالسازی
|
||||
<div class="button-title">
|
||||
اشتراک گذاری لینک فعالسازی
|
||||
</div>
|
||||
</v-btn>
|
||||
<v-btn color="primary" variant="outlined" prepend-icon="mdi-cog" @click="goToWarrantySettings">
|
||||
تنظیمات گارانتی
|
||||
<div class="button-title">
|
||||
تنظیمات گارانتی
|
||||
</div>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<div class="warranty-plugin">
|
||||
|
@ -154,55 +158,76 @@
|
|||
@close="closeBulkImportDialog" />
|
||||
|
||||
<!-- Activation Link Dialog -->
|
||||
<v-dialog v-model="showActivationLinkDialog" max-width="600">
|
||||
<v-dialog v-model="showActivationLinkDialog" :max-width="$vuetify.display.smAndDown ? '95%' : '600'" :fullscreen="$vuetify.display.xs">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center" style="padding: 20px !important;">
|
||||
<v-card-title class="d-flex align-center pa-4 pa-sm-6">
|
||||
<v-icon class="ml-2" color="info">mdi-share-variant</v-icon>
|
||||
اشتراک گذاری لینک فعالسازی گارانتی
|
||||
<span class="text-h6 text-sm-h5">اشتراک گذاری لینک فعالسازی گارانتی</span>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-card-text class="pa-4 pa-sm-6">
|
||||
<v-alert type="info" variant="tonal" class="mb-4">
|
||||
<strong>نکته:</strong> این لینک برای فعالسازی گارانتی توسط مشتریان استفاده میشود.
|
||||
</v-alert>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="text-body-2 font-weight-medium mb-2 d-block">لینک فعالسازی گارانتی:</label>
|
||||
<div class="d-flex align-center flex-row-reverse gap-2">
|
||||
<v-text-field :model-value="activationLink" readonly variant="outlined" density="compact"
|
||||
class="flex-grow-1 text-left" hide-details></v-text-field>
|
||||
<v-btn color="primary" variant="tonal" @click="copyActivationLink" class="ml-2" :loading="copying">
|
||||
<div class="d-flex flex-column flex-sm-row align-stretch align-sm-center gap-2">
|
||||
<v-text-field
|
||||
:model-value="activationLink"
|
||||
readonly
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
class="flex-grow-1 text-left"
|
||||
hide-details
|
||||
:class="$vuetify.display.xs ? 'mb-2' : ''"
|
||||
></v-text-field>
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="tonal"
|
||||
@click="copyActivationLink"
|
||||
:loading="copying"
|
||||
:class="$vuetify.display.xs ? 'w-100' : ''"
|
||||
:size="$vuetify.display.xs ? 'large' : 'default'"
|
||||
>
|
||||
<v-icon>mdi-content-copy</v-icon>
|
||||
<span v-if="$vuetify.display.xs" class="mr-2">کپی لینک</span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<h4 class="text-h6 mb-3">راهنمای استفاده:</h4>
|
||||
<v-list density="compact">
|
||||
<v-list-item>
|
||||
<v-list density="compact" class="bg-transparent">
|
||||
<v-list-item class="px-0">
|
||||
<template #prepend>
|
||||
<v-icon color="primary" size="small">mdi-numeric-1-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>این لینک را برای مشتریان ارسال کنید</v-list-item-title>
|
||||
<v-list-item-title class="text-body-2 text-sm-body-1">این لینک را برای مشتریان ارسال کنید</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item class="px-0">
|
||||
<template #prepend>
|
||||
<v-icon color="primary" size="small">mdi-numeric-2-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>مشتری با مراجعه به لینک میتواند گارانتی خود را فعال کند</v-list-item-title>
|
||||
<v-list-item-title class="text-body-2 text-sm-body-1">مشتری با مراجعه به لینک میتواند گارانتی خود را فعال کند</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-list-item class="px-0">
|
||||
<template #prepend>
|
||||
<v-icon color="primary" size="small">mdi-numeric-3-circle</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>پس از فعالسازی، وضعیت گارانتی در سیستم بهروزرسانی میشود</v-list-item-title>
|
||||
<v-list-item-title class="text-body-2 text-sm-body-1">پس از فعالسازی، وضعیت گارانتی در سیستم بهروزرسانی میشود</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-card-actions class="pa-4 pa-sm-6">
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showActivationLinkDialog = false">بستن</v-btn>
|
||||
<v-btn
|
||||
@click="showActivationLinkDialog = false"
|
||||
:size="$vuetify.display.xs ? 'large' : 'default'"
|
||||
:class="$vuetify.display.xs ? 'w-100' : ''"
|
||||
>
|
||||
بستن
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
@ -841,4 +866,24 @@ onMounted(async () => {
|
|||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
.button-title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.button-title {
|
||||
display: none !important;
|
||||
}
|
||||
.v-btn__prepend {
|
||||
margin: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.v-list-item-title {
|
||||
white-space: unset !important;
|
||||
text-overflow: unset !important;
|
||||
}
|
||||
</style>
|
|
@ -378,7 +378,7 @@
|
|||
<v-card-text>
|
||||
<v-switch
|
||||
v-model="content.requireWarrantyOnDelivery"
|
||||
label="الزام ثبت گارانتی هنگام صدور حواله خروج"
|
||||
label="الزام ثبت گارانتی هنگام تکمیل پروسه حواله خروج"
|
||||
color="primary"
|
||||
hide-details
|
||||
class="mb-2"
|
||||
|
|
|
@ -372,6 +372,18 @@
|
|||
:disabled="loadingSwitches.store"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.storehelper"
|
||||
label="کمک انباردار"
|
||||
@change="savePerms('storehelper')"
|
||||
hide-details
|
||||
color="success"
|
||||
density="comfortable"
|
||||
:loading="loadingSwitches.storehelper"
|
||||
:disabled="loadingSwitches.storehelper"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.wallet"
|
||||
|
@ -401,53 +413,6 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- بخش دسترسی انباردار -->
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card-title class="text-h6 font-weight-bold mb-4 text-primary">
|
||||
<v-icon color="primary" class="me-2">mdi-warehouse</v-icon>
|
||||
دسترسی انباردار
|
||||
</v-card-title>
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="warehouse-alert mr-4"
|
||||
density="compact"
|
||||
border="start"
|
||||
border-color="primary"
|
||||
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-information</v-icon>
|
||||
</template>
|
||||
<div class="text-body-2">
|
||||
دسترسی انباردار شامل تمام بخشهای انبارداری مانند مدیریت انبارها، کالاها، حوالهها، موجودی و گارانتی میباشد.
|
||||
</div>
|
||||
</v-alert>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card variant="outlined" class="h-100">
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.warehouseManager"
|
||||
label="دسترسی کامل انباردار"
|
||||
@change="savePerms('warehouseManager')"
|
||||
hide-details
|
||||
color="primary"
|
||||
density="comfortable"
|
||||
:loading="loadingSwitches.warehouseManager"
|
||||
:disabled="loadingSwitches.warehouseManager"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isPluginActive('accpro')" class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card-title class="text-h6 font-weight-bold mb-4">بسته حسابداری پیشرفته</v-card-title>
|
||||
|
@ -698,6 +663,58 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isPluginActive('warranty')" class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه گارانتی</v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card variant="outlined" class="h-100">
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.plugWarranty"
|
||||
label="مدیریت گارانتی"
|
||||
@change="savePerms('plugWarranty')"
|
||||
hide-details
|
||||
color="success"
|
||||
density="comfortable"
|
||||
:loading="loadingSwitches.plugWarranty"
|
||||
:disabled="loadingSwitches.plugWarranty"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="isPluginActive('import-workflow')" class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card-title class="text-h6 font-weight-bold mb-4">افزونه مدیریت واردات کالا</v-card-title>
|
||||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card variant="outlined" class="h-100">
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<v-switch
|
||||
v-model="info.plugImportWorkflow"
|
||||
label="مدیریت واردات کالا"
|
||||
@change="savePerms('plugImportWorkflow')"
|
||||
hide-details
|
||||
color="success"
|
||||
density="comfortable"
|
||||
:loading="loadingSwitches.plugImportWorkflow"
|
||||
:disabled="loadingSwitches.plugImportWorkflow"
|
||||
></v-switch>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</v-container>
|
||||
<v-snackbar
|
||||
|
@ -779,7 +796,9 @@ export default {
|
|||
plugTaxSettings: false,
|
||||
inquiry: false,
|
||||
ai: false,
|
||||
warehouseManager: false
|
||||
storehelper: false,
|
||||
plugWarranty: false,
|
||||
plugImportWorkflow: false
|
||||
};
|
||||
|
||||
axios.post('/api/business/get/user/permissions',
|
||||
|
|
830
webUI/src/views/acc/storeroom/io/complete.vue
Normal file
830
webUI/src/views/acc/storeroom/io/complete.vue
Normal file
|
@ -0,0 +1,830 @@
|
|||
<template>
|
||||
<v-toolbar color="toolbar" title="تکمیل پروسه حواله انبار">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip text="بازگشت" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text" icon="mdi-arrow-right" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="success"
|
||||
prepend-icon="mdi-check-decagram"
|
||||
@click="completeTicket"
|
||||
:loading="completing"
|
||||
:disabled="!canComplete"
|
||||
>
|
||||
تکمیل پروسه
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
|
||||
<v-container fluid>
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="d-flex justify-center align-center" style="min-height: 400px;">
|
||||
<v-card class="pa-8 text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
class="mb-4"
|
||||
></v-progress-circular>
|
||||
<div class="text-h6 text-grey-darken-1">در حال بارگذاری اطلاعات حواله...</div>
|
||||
<div class="text-body-2 text-grey mt-2">لطفاً صبر کنید</div>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- Content when loaded -->
|
||||
<div v-else>
|
||||
<!-- Ticket Information -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title class="d-flex align-center mb-4">
|
||||
<v-icon class="ml-2" color="primary">mdi-file-document</v-icon>
|
||||
اطلاعات حواله
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">شماره حواله:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ item.ticket?.code }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">تاریخ:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ (item.ticket?.date) }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">نوع:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ item.ticket?.typeString }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">انبار:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ item.ticket?.storeroom?.manager }}</div>
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="item.ticket?.des">
|
||||
<div class="text-subtitle-2 text-grey">توضیحات:</div>
|
||||
<div class="text-body-1">{{ item.ticket.des }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
<v-card class="mb-4">
|
||||
<v-card-title class="d-flex align-center mb-4">
|
||||
<v-icon class="ml-2" color="primary">mdi-account</v-icon>
|
||||
اطلاعات طرف حساب
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">نام:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ item.person?.nikename }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">موبایل:</div>
|
||||
<div class="text-body-1 font-weight-medium">{{ item.person?.mobile }}</div>
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="item.person?.address">
|
||||
<div class="text-subtitle-2 text-grey">آدرس:</div>
|
||||
<div class="text-body-1">{{ item.person.address }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-card class="mb-4">
|
||||
<v-card-title class="d-flex align-center mb-4">
|
||||
<v-icon class="ml-2" color="primary">mdi-information</v-icon>
|
||||
وضعیت
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">وضعیت تایید:</div>
|
||||
<v-chip :color="item.ticket?.approved ? 'success' : 'warning'" size="small">
|
||||
{{ item.ticket?.approved ? 'تایید شده' : 'در انتظار تایید' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">وضعیت تکمیل:</div>
|
||||
<v-chip :color="item.ticket?.completed ? 'success' : 'info'" size="small">
|
||||
{{ item.ticket?.completed ? 'تکمیل شده' : 'در انتظار تکمیل' }}
|
||||
</v-chip>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="4">
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Items with Warranty Allocation - Full Width -->
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center mb-4">
|
||||
<v-icon class="ml-2" color="primary">mdi-package-variant</v-icon>
|
||||
اقلام حواله و تخصیص گارانتی
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div v-for="(row, rowIndex) in item.rows" :key="rowIndex" class="mb-6">
|
||||
<v-card variant="outlined" class="mb-3">
|
||||
<v-card-title class="text-subtitle-1 pa-4 mb-4">
|
||||
{{ row.commodity?.name }}
|
||||
<v-chip size="small" color="primary" class="ml-2">
|
||||
{{ row.count }} عدد
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<!-- Warranty Allocation Section - Like sell.vue -->
|
||||
<div v-if="isWarrantyRequired(row)" class="mb-4">
|
||||
<div class="lines-wrap">
|
||||
<div class="line-row header">
|
||||
<div class="col-idx">#</div>
|
||||
<div class="col-serial">سریال کالا</div>
|
||||
<div class="col-actions"></div>
|
||||
<div class="col-warranty">گارانتی آزاد این کالا</div>
|
||||
<div class="col-actions"></div>
|
||||
</div>
|
||||
|
||||
<div v-for="(line, lineIndex) in getWarrantyLines(row)" :key="`ln-${rowIndex}-${lineIndex}`" class="line-row">
|
||||
<div class="col-idx">{{ lineIndex + 1 }}</div>
|
||||
|
||||
<div class="col-serial">
|
||||
<v-text-field
|
||||
v-model.trim="line.serialNumber"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
placeholder="شماره سریال را وارد کنید"
|
||||
:disabled="line.isAllocated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<v-chip v-if="line.isAllocated" color="success" size="small">
|
||||
تخصیص داده شده
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="col-warranty">
|
||||
<v-autocomplete
|
||||
v-model="line.warrantySerial"
|
||||
:items="filteredWarranties(rowIndex, lineIndex)"
|
||||
item-title="label"
|
||||
item-value="serialNumber"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
clearable
|
||||
:loading="row.loadingWarranties"
|
||||
:no-data-text="row.loadingWarranties ? 'در حال بارگذاری...' : (row.warrantiesLoaded ? 'موردی یافت نشد' : 'برای مشاهده لیست، کلیک کنید')"
|
||||
:filter="(i: any, q: any) => String(i.label || '').toLowerCase().includes(String(q || '').toLowerCase())"
|
||||
placeholder="انتخاب/وارد کردن کد گارانتی"
|
||||
:disabled="line.isAllocated"
|
||||
@update:menu="val => { if (val) ensureWarrantiesLoaded(rowIndex) }"
|
||||
@update:search="ensureWarrantiesLoaded(rowIndex)"
|
||||
@blur="onWarrantyBlur(rowIndex, lineIndex)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<v-btn
|
||||
v-if="!line.isAllocated"
|
||||
icon size="small"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
@click="searchWarranty(rowIndex, lineIndex)"
|
||||
:loading="line.searching"
|
||||
class="mr-1"
|
||||
>
|
||||
<v-icon>mdi-magnify</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="!line.isAllocated"
|
||||
icon size="small"
|
||||
color="primary"
|
||||
variant="text"
|
||||
@click="openBarcodeScanner(rowIndex, lineIndex)"
|
||||
title="اسکن بارکد"
|
||||
>
|
||||
<v-icon>mdi-barcode-scan</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Item Details -->
|
||||
<v-row>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">کد کالا:</div>
|
||||
<div class="text-body-2">{{ row.commodity?.code }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6">
|
||||
<div class="text-subtitle-2 text-grey">واحد:</div>
|
||||
<div class="text-body-2">{{ row.commodity?.unit?.name }}</div>
|
||||
</v-col>
|
||||
<v-col cols="12" v-if="row.des">
|
||||
<div class="text-subtitle-2 text-grey">توضیحات:</div>
|
||||
<div class="text-body-2">{{ row.des }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</v-container>
|
||||
|
||||
<!-- Barcode Scanner Dialog -->
|
||||
<v-dialog v-model="showBarcodeScanner" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title>اسکن بارکد</v-card-title>
|
||||
<v-card-text>
|
||||
<BarcodeScanner
|
||||
@barcode-scanned="onBarcodeScanned"
|
||||
@close="showBarcodeScanner = false"
|
||||
/>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<BarcodeScanner v-model="showBarcodeScanner" @detected="onBarcodeScanned" />
|
||||
|
||||
<!-- Warranty Search Dialog -->
|
||||
<v-dialog v-model="showWarrantySearch" max-width="600">
|
||||
<v-card>
|
||||
<v-card-title>جستجوی گارانتی</v-card-title>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="warrantySearchQuery"
|
||||
label="جستجو بر اساس کد گارانتی یا نام محصول"
|
||||
variant="outlined"
|
||||
prepend-icon="mdi-magnify"
|
||||
@input="searchWarranties"
|
||||
></v-text-field>
|
||||
|
||||
<v-list v-if="warrantySearchResults.length > 0">
|
||||
<v-list-item
|
||||
v-for="warranty in warrantySearchResults"
|
||||
:key="warranty.id"
|
||||
@click="selectWarranty(warranty)"
|
||||
class="mb-2"
|
||||
>
|
||||
<v-list-item-title>{{ warranty.serialNumber }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ warranty.commodity?.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
|
||||
<v-alert v-else-if="warrantySearchQuery" type="info">
|
||||
نتیجهای یافت نشد
|
||||
</v-alert>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn @click="showWarrantySearch = false">بستن</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Success Dialog -->
|
||||
<v-dialog v-model="showSuccessDialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-title class="text-center" style="padding: 30px !important;">
|
||||
<v-icon color="success" size="large" class="mb-2">mdi-check-circle</v-icon>
|
||||
<div>پروسه با موفقیت تکمیل شد</div>
|
||||
</v-card-title>
|
||||
<v-card-text class="text-center">
|
||||
حواله انبار با موفقیت تکمیل شد و گارانتیها تخصیص داده شدند.
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="goBack">بازگشت</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Error Snackbar -->
|
||||
<v-snackbar v-model="showError" color="error" timeout="5000">
|
||||
{{ errorMessage }}
|
||||
</v-snackbar>
|
||||
|
||||
<!-- Success Snackbar -->
|
||||
<v-snackbar v-model="showSuccess" color="success" timeout="3000">
|
||||
{{ successMessage }}
|
||||
</v-snackbar>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import axios from 'axios'
|
||||
import moment from 'jalali-moment'
|
||||
import Swal from 'sweetalert2'
|
||||
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
interface Business {
|
||||
legal_name: string
|
||||
}
|
||||
|
||||
interface Storeroom {
|
||||
manager: string
|
||||
}
|
||||
|
||||
interface Ticket {
|
||||
id: number
|
||||
date: string | null
|
||||
code: string | null
|
||||
des: string
|
||||
type: string
|
||||
typeString: string
|
||||
storeroom: Storeroom
|
||||
preview: boolean
|
||||
approved: boolean
|
||||
completed?: boolean
|
||||
}
|
||||
|
||||
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
|
||||
id: number
|
||||
unit: {
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
interface Row {
|
||||
commodity: Commodity
|
||||
count: number
|
||||
hesabdariCount: number
|
||||
remain: number
|
||||
des: string
|
||||
referal: string
|
||||
warrantyLines?: WarrantyLine[]
|
||||
loadingWarranties?: boolean
|
||||
warrantiesLoaded?: boolean
|
||||
availableWarranties?: Array<{ label: string; serialNumber: string }>
|
||||
}
|
||||
|
||||
interface WarrantyLine {
|
||||
warrantySerial: string
|
||||
serialNumber: string
|
||||
warrantyError?: string
|
||||
serialError?: string
|
||||
searching?: boolean
|
||||
isAllocated?: boolean
|
||||
}
|
||||
|
||||
interface Item {
|
||||
ticket: Ticket
|
||||
rows: Row[]
|
||||
person: Person
|
||||
transferType: any
|
||||
}
|
||||
|
||||
const loading = ref(false)
|
||||
const completing = ref(false)
|
||||
const bid = ref<Business>({ legal_name: '' })
|
||||
const warrantySettings = ref({
|
||||
requireWarrantyOnDelivery: false,
|
||||
matchWarrantyToSerial: false
|
||||
})
|
||||
|
||||
const item = ref<Item>({
|
||||
ticket: {
|
||||
id: 0,
|
||||
date: null,
|
||||
code: null,
|
||||
des: '',
|
||||
type: '',
|
||||
typeString: '',
|
||||
storeroom: {
|
||||
manager: ''
|
||||
},
|
||||
preview: false,
|
||||
approved: false
|
||||
},
|
||||
rows: [],
|
||||
person: {
|
||||
nikename: null,
|
||||
mobile: '',
|
||||
address: '',
|
||||
tel: '',
|
||||
codeeqtesadi: '',
|
||||
keshvar: '',
|
||||
ostan: '',
|
||||
shahr: '',
|
||||
postalcode: ''
|
||||
},
|
||||
transferType: null
|
||||
})
|
||||
|
||||
// Warranty search
|
||||
const showWarrantySearch = ref(false)
|
||||
const warrantySearchQuery = ref('')
|
||||
const warrantySearchResults = ref<any[]>([])
|
||||
const currentSearchContext = ref<{ rowIndex: number; lineIndex: number } | null>(null)
|
||||
|
||||
// Success/Error handling
|
||||
const showSuccessDialog = ref(false)
|
||||
const showError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const showSuccess = ref(false)
|
||||
const successMessage = ref('')
|
||||
|
||||
// Barcode Scanner
|
||||
const showBarcodeScanner = ref(false)
|
||||
const currentBarcodeScannerContext = ref<{ rowIndex: number; lineIndex: number } | null>(null)
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ticketResponse, businessResponse, warrantyResponse, allocatedWarrantiesResponse] = await Promise.all([
|
||||
axios.post(`/api/storeroom/tickets/info/${route.params.id}`),
|
||||
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`),
|
||||
axios.get('/api/plugins/warranty/settings/get').catch(() => ({ data: { requireWarrantyOnDelivery: false, matchWarrantyToSerial: false } })),
|
||||
axios.get(`/api/plugins/warranty/serials/by-storeroom-ticket/${route.params.id}`).catch(() => ({ data: { items: [] } }))
|
||||
])
|
||||
|
||||
item.value.ticket = ticketResponse.data.ticket
|
||||
item.value.person = ticketResponse.data.person
|
||||
item.value.transferType = ticketResponse.data.transferType
|
||||
item.value.rows = ticketResponse.data.commodities.map((row: Row) => ({
|
||||
...row,
|
||||
warrantyLines: [],
|
||||
loadingWarranties: false,
|
||||
warrantiesLoaded: false,
|
||||
availableWarranties: []
|
||||
}))
|
||||
|
||||
bid.value = businessResponse.data
|
||||
warrantySettings.value.requireWarrantyOnDelivery = !!(warrantyResponse?.data?.requireWarrantyOnDelivery)
|
||||
warrantySettings.value.matchWarrantyToSerial = !!(warrantyResponse?.data?.matchWarrantyToSerial)
|
||||
|
||||
// Get already allocated warranties
|
||||
const allocatedWarranties = allocatedWarrantiesResponse?.data?.items || []
|
||||
|
||||
|
||||
|
||||
// Initialize warranty lines for each row
|
||||
item.value.rows.forEach((row, rowIndex) => {
|
||||
if (isWarrantyRequired(row)) {
|
||||
// Get allocated warranties for this specific commodity
|
||||
const commodityAllocatedWarranties = allocatedWarranties.filter((w: any) =>
|
||||
w.commodity?.id === row.commodity?.id
|
||||
)
|
||||
|
||||
// Always create warranty lines based on row.count, regardless of allocated warranties
|
||||
row.warrantyLines = Array.from({ length: row.count }, (_, lineIndex) => {
|
||||
// Check if this line already has an allocated warranty
|
||||
const allocatedWarranty = commodityAllocatedWarranties[lineIndex] || null
|
||||
|
||||
return {
|
||||
warrantySerial: allocatedWarranty?.serialNumber || '',
|
||||
serialNumber: allocatedWarranty?.commoditySerial || '',
|
||||
warrantyError: '',
|
||||
serialError: '',
|
||||
searching: false,
|
||||
isAllocated: !!allocatedWarranty
|
||||
}
|
||||
})
|
||||
} else {
|
||||
row.warrantyLines = []
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
showErrorMessage('خطا در بارگذاری اطلاعات حواله')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const isWarrantyRequired = (row: Row) => {
|
||||
// Always show warranty allocation if count > 0, regardless of settings
|
||||
return row.count > 0
|
||||
}
|
||||
|
||||
const getWarrantyLines = (row: Row) => {
|
||||
return row.warrantyLines || []
|
||||
}
|
||||
|
||||
const ensureWarrantiesLoaded = async (rowIndex: number) => {
|
||||
const row = item.value.rows[rowIndex]
|
||||
if (row.warrantiesLoaded || row.loadingWarranties) return
|
||||
await loadAvailableWarranties(rowIndex)
|
||||
}
|
||||
|
||||
const loadAvailableWarranties = async (rowIndex: number) => {
|
||||
const row = item.value.rows[rowIndex]
|
||||
const commodity = row.commodity
|
||||
if (!commodity?.id) return
|
||||
try {
|
||||
row.loadingWarranties = true
|
||||
const res = await axios.get('/api/plugins/warranty/serials', {
|
||||
params: { status: 'available', commodity_id: commodity.id }
|
||||
})
|
||||
const list = Array.isArray(res.data) ? res.data : []
|
||||
row.availableWarranties = list
|
||||
.filter((x: any) => x.status === 'available' && x.commodity?.id === commodity.id)
|
||||
.map((x: any) => ({ label: `${x.serialNumber}`, serialNumber: x.serialNumber }))
|
||||
row.warrantiesLoaded = true
|
||||
} catch {
|
||||
row.availableWarranties = []
|
||||
row.warrantiesLoaded = true
|
||||
} finally {
|
||||
row.loadingWarranties = false
|
||||
}
|
||||
}
|
||||
|
||||
const filteredWarranties = (rowIndex: number, lineIndex: number) => {
|
||||
const row = item.value.rows[rowIndex]
|
||||
const selected = new Set(
|
||||
row.warrantyLines
|
||||
?.map((ln, idx) => (idx === lineIndex ? null : (ln.warrantySerial || null)))
|
||||
.filter((v: any) => !!v) || []
|
||||
)
|
||||
return (row.availableWarranties || []).filter(w => !selected.has(w.serialNumber))
|
||||
}
|
||||
|
||||
const onWarrantyBlur = async (rowIndex: number, lineIndex: number) => {
|
||||
const row = item.value.rows[rowIndex]
|
||||
const line = row.warrantyLines?.[lineIndex]
|
||||
if (!line || !line.warrantySerial) return
|
||||
|
||||
try {
|
||||
line.searching = true
|
||||
const response = await axios.get('/api/plugins/warranty/serials', {
|
||||
params: {
|
||||
serialNumber: line.warrantySerial,
|
||||
status: 'available'
|
||||
}
|
||||
})
|
||||
|
||||
const warranties = Array.isArray(response.data) ? response.data : []
|
||||
const warranty = warranties.find((w: any) => w.serialNumber === line.warrantySerial)
|
||||
|
||||
if (warranty) {
|
||||
line.warrantyError = ''
|
||||
if (warrantySettings.value.matchWarrantyToSerial && !line.serialNumber) {
|
||||
line.serialNumber = warranty.serialNumber
|
||||
}
|
||||
} else {
|
||||
line.warrantyError = 'گارانتی یافت نشد یا در دسترس نیست'
|
||||
}
|
||||
} catch (error) {
|
||||
line.warrantyError = 'خطا در بررسی گارانتی'
|
||||
} finally {
|
||||
line.searching = false
|
||||
}
|
||||
}
|
||||
|
||||
const searchWarranty = (rowIndex: number, lineIndex: number) => {
|
||||
currentSearchContext.value = { rowIndex, lineIndex }
|
||||
showWarrantySearch.value = true
|
||||
warrantySearchQuery.value = ''
|
||||
warrantySearchResults.value = []
|
||||
}
|
||||
|
||||
const searchWarranties = async () => {
|
||||
if (!warrantySearchQuery.value) {
|
||||
warrantySearchResults.value = []
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.get('/api/plugins/warranty/serials', {
|
||||
params: {
|
||||
search: warrantySearchQuery.value,
|
||||
status: 'available'
|
||||
}
|
||||
})
|
||||
|
||||
warrantySearchResults.value = Array.isArray(response.data) ? response.data : []
|
||||
} catch (error) {
|
||||
warrantySearchResults.value = []
|
||||
}
|
||||
}
|
||||
|
||||
const selectWarranty = (warranty: any) => {
|
||||
if (!currentSearchContext.value) return
|
||||
|
||||
const { rowIndex, lineIndex } = currentSearchContext.value
|
||||
const row = item.value.rows[rowIndex]
|
||||
const line = row.warrantyLines?.[lineIndex]
|
||||
|
||||
if (line) {
|
||||
line.warrantySerial = warranty.serialNumber
|
||||
line.warrantyError = ''
|
||||
if (warrantySettings.value.matchWarrantyToSerial && !line.serialNumber) {
|
||||
line.serialNumber = warranty.serialNumber
|
||||
}
|
||||
}
|
||||
|
||||
showWarrantySearch.value = false
|
||||
currentSearchContext.value = null
|
||||
}
|
||||
|
||||
const openBarcodeScanner = (rowIndex: number, lineIndex: number) => {
|
||||
currentBarcodeScannerContext.value = { rowIndex, lineIndex }
|
||||
showBarcodeScanner.value = true
|
||||
}
|
||||
|
||||
const onBarcodeScanned = (serialNumber: string) => {
|
||||
if (!currentBarcodeScannerContext.value) return
|
||||
|
||||
const { rowIndex, lineIndex } = currentBarcodeScannerContext.value
|
||||
const row = item.value.rows[rowIndex]
|
||||
const line = row.warrantyLines?.[lineIndex]
|
||||
|
||||
if (line && !line.isAllocated) {
|
||||
line.serialNumber = serialNumber
|
||||
line.warrantyError = ''
|
||||
line.searching = false
|
||||
|
||||
// Auto-fill warranty serial if matchWarrantyToSerial is enabled
|
||||
if (warrantySettings.value.matchWarrantyToSerial && !line.warrantySerial) {
|
||||
line.warrantySerial = serialNumber
|
||||
}
|
||||
|
||||
// Move to next line if available
|
||||
if (lineIndex + 1 < (row.warrantyLines?.length || 0)) {
|
||||
currentBarcodeScannerContext.value = { rowIndex, lineIndex: lineIndex + 1 }
|
||||
} else {
|
||||
showBarcodeScanner.value = false
|
||||
currentBarcodeScannerContext.value = null
|
||||
}
|
||||
} else {
|
||||
showBarcodeScanner.value = false
|
||||
currentBarcodeScannerContext.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const canComplete = computed(() => {
|
||||
if (loading.value || completing.value) return false
|
||||
|
||||
// Only check warranty requirements if requireWarrantyOnDelivery is enabled
|
||||
if (warrantySettings.value.requireWarrantyOnDelivery) {
|
||||
for (const row of item.value.rows) {
|
||||
if (isWarrantyRequired(row)) {
|
||||
for (const line of row.warrantyLines || []) {
|
||||
if (!line.warrantySerial) {
|
||||
return false
|
||||
}
|
||||
if (line.warrantyError) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
const completeTicket = async () => {
|
||||
if (!canComplete.value) return
|
||||
|
||||
try {
|
||||
completing.value = true
|
||||
|
||||
// Prepare warranty allocation data
|
||||
const warrantyData = item.value.rows.map((row, rowIndex) => ({
|
||||
commodityId: row.commodity?.id,
|
||||
count: row.count,
|
||||
warrantyLines: row.warrantyLines?.map(line => ({
|
||||
warrantySerial: line.warrantySerial,
|
||||
serialNumber: line.serialNumber,
|
||||
isBeforeAllocated: line.isAllocated
|
||||
})) || []
|
||||
})).filter(item => item.warrantyLines.length > 0)
|
||||
|
||||
// Call API to complete the process
|
||||
await axios.post(`/api/storeroom/ticket/complete/${route.params.id}`, {
|
||||
warrantyAllocations: warrantyData
|
||||
})
|
||||
|
||||
showSuccessDialog.value = true
|
||||
} catch (error: any) {
|
||||
console.error('Error completing ticket:', error)
|
||||
const message = error.response?.data?.message || 'خطا در تکمیل پروسه'
|
||||
showErrorMessage(message)
|
||||
} finally {
|
||||
completing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.push('/acc/storeroom/tickets/list/helper')
|
||||
}
|
||||
|
||||
const showErrorMessage = (message: string) => {
|
||||
errorMessage.value = message
|
||||
showError.value = true
|
||||
}
|
||||
|
||||
const formatDate = (dateVal: any) => {
|
||||
if (!dateVal) return '-'
|
||||
try {
|
||||
const d = typeof dateVal === 'string' ? new Date(dateVal) : dateVal
|
||||
const valid = d instanceof Date && !isNaN(d.getTime())
|
||||
if (!valid) return '-'
|
||||
const j = moment(d)
|
||||
const y = j.jYear()
|
||||
const m = j.jMonth() + 1
|
||||
const day = j.jDate()
|
||||
return `${y}/${String(m).padStart(2, '0')}/${String(day).padStart(2, '0')}`
|
||||
} catch {
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.v-card-title {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.lines-wrap {
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.line-row {
|
||||
display: grid;
|
||||
grid-template-columns: 60px 1fr 120px 1fr 80px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.line-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.line-row.header {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.col-idx {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.col-serial {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-warranty {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.col-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.no-lines {
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
</style>
|
|
@ -88,7 +88,7 @@
|
|||
<v-text-field v-model="items[index].referral" variant="outlined" density="compact" />
|
||||
</template>
|
||||
|
||||
<template v-slot:expanded-row="{ item, index }">
|
||||
<template v-slot:expanded-row="{ item, index }" v-if="isPluginActive('warranty')">
|
||||
<td :colspan="headers.length" class="py-4 px-3">
|
||||
<div class="lines-wrap">
|
||||
<div class="line-row header">
|
||||
|
@ -105,40 +105,31 @@
|
|||
<div class="col-serial">
|
||||
<v-text-field v-model.trim="items[index].lines[lidx].serialNumber" variant="outlined"
|
||||
density="compact" hide-details="auto" placeholder="شماره سریال را وارد یا اسکن کنید"
|
||||
:rules="serialRules"
|
||||
@blur="onSerialBlur(index, lidx)" />
|
||||
:rules="serialRules" @blur="onSerialBlur(index, lidx)" />
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<v-btn icon size="small" color="primary" variant="text"
|
||||
@click="openScanner({ mode: 'serial', itemIndex: index, lineIndex: lidx })">
|
||||
@click="scanner.open = true, scanner.mode = 'serial', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
||||
<v-icon>mdi-barcode-scan</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="col-warranty">
|
||||
<v-autocomplete
|
||||
v-model="items[index].lines[lidx].warrantySerial"
|
||||
:items="filteredWarranties(index, lidx)"
|
||||
item-title="label"
|
||||
item-value="serialNumber"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details="auto"
|
||||
clearable
|
||||
<v-autocomplete v-model="items[index].lines[lidx].warrantySerial"
|
||||
:items="filteredWarranties(index, lidx)" item-title="label" item-value="serialNumber"
|
||||
variant="outlined" density="compact" hide-details="auto" clearable
|
||||
:loading="items[index].loadingWarranties"
|
||||
:no-data-text="items[index].loadingWarranties ? 'در حال بارگذاری...' : (items[index].warrantiesLoaded ? 'موردی یافت نشد' : 'برای مشاهده لیست، کلیک کنید')"
|
||||
:filter="(i: any, q: any) => String(i.label || '').toLowerCase().includes(String(q || '').toLowerCase())"
|
||||
placeholder="انتخاب/وارد کردن کد گارانتی"
|
||||
:rules="warrantyRules"
|
||||
placeholder="انتخاب/وارد کردن کد گارانتی" :rules="warrantyRules"
|
||||
@update:menu="val => { if (val) ensureWarrantiesLoaded(index) }"
|
||||
@update:search="ensureWarrantiesLoaded(index)"
|
||||
/>
|
||||
@update:search="ensureWarrantiesLoaded(index)" />
|
||||
</div>
|
||||
|
||||
<div class="col-actions">
|
||||
<v-btn icon size="small" color="secondary" variant="text"
|
||||
@click="openScanner({ mode: 'warranty', itemIndex: index, lineIndex: lidx })">
|
||||
@click="scanner.open = true, scanner.mode = 'warranty', scanner.itemIndex = index, scanner.lineIndex = lidx">
|
||||
<v-icon>mdi-barcode-scan</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
@ -155,39 +146,7 @@
|
|||
</v-row>
|
||||
</v-container>
|
||||
|
||||
<v-dialog v-model="scanner.open" :max-width="isMobile ? '95vw' : 560" persistent>
|
||||
<v-card class="qr-card">
|
||||
<v-card-title class="qr-title">
|
||||
<v-icon left color="primary">mdi-qrcode-scan</v-icon>
|
||||
اسکن کد {{ scanner.mode === 'serial' ? 'سریال کالا' : 'گارانتی' }}
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="qr-wrap">
|
||||
<div :id="readerId" ref="readerWrap" class="qr-reader"></div>
|
||||
</div>
|
||||
<div class="qr-status">
|
||||
<v-alert v-if="scanner.error" type="error" variant="tonal" density="comfortable">
|
||||
{{ scanner.error }}
|
||||
</v-alert>
|
||||
<v-progress-circular v-if="loadingScan" indeterminate size="28" class="mt-3" color="primary"
|
||||
style="display: block; margin: 0 auto" />
|
||||
<div class="qr-supported-formats mt-3 text-center text-caption text-grey">
|
||||
پشتیبانی از QR Code، Code 128، Code 39، EAN-13، UPC-A، Data Matrix
|
||||
</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="qr-actions">
|
||||
<v-btn :disabled="!scanner.ready || loadingScan" variant="outlined" color="primary"
|
||||
prepend-icon="mdi-camera-switch" @click="switchCamera">
|
||||
تغییر دوربین
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeScanner">بستن</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
<BarcodeScanner v-model="scanner.open" @detected="handleBarcodeScan" />
|
||||
|
||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000" location="bottom">
|
||||
{{ snackbar.message }}
|
||||
|
@ -202,7 +161,7 @@ import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue'
|
|||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
|
||||
import { Html5Qrcode, Html5QrcodeSupportedFormats, Html5QrcodeScannerState } from 'html5-qrcode'
|
||||
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
|
||||
|
||||
interface TransferType { id: number; name: string }
|
||||
interface Person { des: string; mobile: string }
|
||||
|
@ -281,10 +240,6 @@ const warrantySettings = ref({ requireWarrantyOnDelivery: false, matchWarrantyTo
|
|||
const serialRules = computed(() => warrantySettings.value.requireWarrantyOnDelivery ? [rules.required] : [])
|
||||
const warrantyRules = computed(() => warrantySettings.value.requireWarrantyOnDelivery ? [rules.required] : [])
|
||||
|
||||
const readerId = 'qr-reader-' + Math.random().toString(36).slice(2)
|
||||
const readerWrap = ref<HTMLElement | null>(null)
|
||||
let qr: Html5Qrcode | null = null
|
||||
const loadingScan = ref(false)
|
||||
const scanner = reactive({
|
||||
open: false,
|
||||
ready: false,
|
||||
|
@ -297,122 +252,34 @@ const scanner = reactive({
|
|||
})
|
||||
const isMobile = computed(() => window.innerWidth <= 768)
|
||||
|
||||
const openScanner = async (opt: { mode: 'serial' | 'warranty'; itemIndex: number; lineIndex: number }) => {
|
||||
scanner.open = true
|
||||
scanner.mode = opt.mode
|
||||
scanner.itemIndex = opt.itemIndex
|
||||
scanner.lineIndex = opt.lineIndex
|
||||
scanner.error = ''
|
||||
scanner.ready = false
|
||||
await nextTick()
|
||||
try {
|
||||
loadingScan.value = true
|
||||
if (!qr) {
|
||||
qr = new Html5Qrcode(readerId, {
|
||||
verbose: false,
|
||||
formatsToSupport: [
|
||||
Html5QrcodeSupportedFormats.QR_CODE,
|
||||
Html5QrcodeSupportedFormats.CODE_128,
|
||||
Html5QrcodeSupportedFormats.CODE_39,
|
||||
Html5QrcodeSupportedFormats.EAN_13,
|
||||
Html5QrcodeSupportedFormats.UPC_A,
|
||||
Html5QrcodeSupportedFormats.DATA_MATRIX
|
||||
],
|
||||
experimentalFeatures: { useBarCodeDetectorIfSupported: true }
|
||||
})
|
||||
} else {
|
||||
if (qr.getState() === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
||||
await qr.clear()
|
||||
const handleBarcodeScan = async (text: string) => {
|
||||
if (scanner.mode === 'serial') {
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].serialNumber = text
|
||||
if (!warrantySettings.value.matchWarrantyToSerial) return
|
||||
const row = items.value[scanner.itemIndex]
|
||||
const ln = row.lines[scanner.lineIndex]
|
||||
try {
|
||||
const found = (row.availableWarranties || []).find(w => String(w.serialNumber).trim() === ln.serialNumber)
|
||||
if (found) {
|
||||
ln.warrantySerial = found.serialNumber
|
||||
return
|
||||
}
|
||||
if (!row.warrantiesLoaded) {
|
||||
await loadAvailableWarranties(scanner.itemIndex)
|
||||
}
|
||||
const foundAfter = (row.availableWarranties || []).find(w => String(w.serialNumber).trim() === ln.serialNumber)
|
||||
if (foundAfter) {
|
||||
ln.warrantySerial = foundAfter.serialNumber
|
||||
} else {
|
||||
showSnack('کد گارانتی متناظر با سریال یافت نشد', 'warning')
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
const devices = (await navigator.mediaDevices.enumerateDevices()).filter(d => d.kind === 'videoinput')
|
||||
if (!devices.length) throw new Error('دوربین یافت نشد')
|
||||
scanner.cameras = devices
|
||||
scanner.currentDeviceId =
|
||||
devices.find(d => /back|rear|environment/i.test(d.label))?.deviceId || devices[0].deviceId
|
||||
|
||||
const w = Math.min(520, (readerWrap.value?.clientWidth || 480))
|
||||
const size = Math.max(220, Math.round(w * 0.66))
|
||||
|
||||
await qr.start(
|
||||
{ deviceId: { exact: scanner.currentDeviceId } },
|
||||
{ fps: 12, qrbox: { width: size, height: size }, aspectRatio: 1.333 },
|
||||
async (decodedText: string) => {
|
||||
const text = decodedText.trim()
|
||||
if (!text) return
|
||||
if (scanner.mode === 'serial') {
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].serialNumber = text
|
||||
showSnack('سریال ثبت شد', 'success')
|
||||
await closeScanner()
|
||||
} else {
|
||||
const ok = await validateWarranty(text, items.value[scanner.itemIndex].commodity)
|
||||
if (!ok.valid) { showSnack(ok.message || 'گارانتی معتبر نیست', 'error'); return }
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].warrantySerial = text
|
||||
showSnack('گارانتی ثبت شد', 'success')
|
||||
await closeScanner()
|
||||
}
|
||||
},
|
||||
() => { }
|
||||
)
|
||||
scanner.ready = true
|
||||
} catch (e: any) {
|
||||
scanner.error = e?.message || 'خطا در راهاندازی دوربین'
|
||||
} finally {
|
||||
loadingScan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const switchCamera = async () => {
|
||||
if (!qr || !scanner.cameras.length || !scanner.ready) return
|
||||
try {
|
||||
loadingScan.value = true
|
||||
const idx = scanner.cameras.findIndex(c => c.deviceId === scanner.currentDeviceId)
|
||||
const next = scanner.cameras[(idx + 1) % scanner.cameras.length]
|
||||
if (qr.getState() === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
||||
await qr.clear()
|
||||
scanner.currentDeviceId = next.deviceId
|
||||
|
||||
const w = Math.min(520, (readerWrap.value?.clientWidth || 480))
|
||||
const size = Math.max(220, Math.round(w * 0.66))
|
||||
|
||||
await qr.start(
|
||||
{ deviceId: { exact: scanner.currentDeviceId } },
|
||||
{ fps: 12, qrbox: { width: size, height: size }, aspectRatio: 1.333 },
|
||||
async (decodedText: string) => {
|
||||
const text = decodedText.trim()
|
||||
if (!text) return
|
||||
if (scanner.mode === 'serial') {
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].serialNumber = text
|
||||
showSnack('سریال ثبت شد', 'success')
|
||||
await closeScanner()
|
||||
} else {
|
||||
const ok = await validateWarranty(text, items.value[scanner.itemIndex].commodity)
|
||||
if (!ok.valid) { showSnack(ok.message || 'گارانتی معتبر نیست', 'error'); return }
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].warrantySerial = text
|
||||
showSnack('گارانتی ثبت شد', 'success')
|
||||
await closeScanner()
|
||||
}
|
||||
},
|
||||
() => { }
|
||||
)
|
||||
} catch (e: any) {
|
||||
scanner.error = e?.message || 'خطا در تغییر دوربین'
|
||||
} finally {
|
||||
loadingScan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const closeScanner = async () => {
|
||||
try {
|
||||
loadingScan.value = true
|
||||
if (qr) {
|
||||
if (qr.getState() === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
||||
await qr.clear()
|
||||
}
|
||||
} finally {
|
||||
scanner.open = false
|
||||
scanner.ready = false
|
||||
scanner.error = ''
|
||||
loadingScan.value = false
|
||||
} else {
|
||||
const ok = await validateWarranty(text, items.value[scanner.itemIndex].commodity)
|
||||
if (!ok.valid) { showSnack(ok.message || 'گارانتی معتبر نیست', 'error'); return }
|
||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].warrantySerial = text
|
||||
showSnack('گارانتی ثبت شد', 'success')
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -476,20 +343,6 @@ const submit = async () => {
|
|||
const errors: string[] = []
|
||||
let totalCount = 0
|
||||
|
||||
items.value.forEach((row) => {
|
||||
totalCount += Number(row.ticketCount || 0)
|
||||
if (warrantySettings.value.requireWarrantyOnDelivery) {
|
||||
for (let i = 0; i < row.ticketCount; i++) {
|
||||
const ln = row.lines[i]
|
||||
if (!ln || !ln.serialNumber || !ln.warrantySerial) {
|
||||
errors.push('پر کردن فیلد های گارانتی و سریال کالا الزامی است')
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (totalCount === 0) errors.push('هیچ کالایی برای خروج انتخاب نشده است')
|
||||
|
||||
if (errors.length) {
|
||||
|
@ -507,8 +360,7 @@ const submit = async () => {
|
|||
serialLines: r.lines.slice(0, r.ticketCount).map(l => ({ serial: l.serialNumber, warranty: l.warrantySerial }))
|
||||
}))
|
||||
|
||||
const requireWarrantySerial = warrantySettings.value.requireWarrantyOnDelivery || payloadItems.some(it => (it.serialLines || []).length > 0)
|
||||
const payloadTicket = { ...ticket.value, requireWarrantySerial }
|
||||
const payloadTicket = { ...ticket.value, requireWarrantySerial: false }
|
||||
|
||||
const response = await axios.post('/api/storeroom/ticket/insert', {
|
||||
doc: doc.value,
|
||||
|
@ -591,8 +443,10 @@ const loadData = async () => {
|
|||
ticket.value.transferType = transferTypesResponse.data[0]
|
||||
|
||||
plugins.value = pluginsResponse.data
|
||||
warrantySettings.value.requireWarrantyOnDelivery = !!(warrantyResp?.data?.requireWarrantyOnDelivery)
|
||||
warrantySettings.value.matchWarrantyToSerial = !!(warrantyResp?.data?.matchWarrantyToSerial)
|
||||
if (isPluginActive('warranty')) {
|
||||
warrantySettings.value.requireWarrantyOnDelivery = !!(warrantyResp?.data?.requireWarrantyOnDelivery)
|
||||
warrantySettings.value.matchWarrantyToSerial = !!(warrantyResp?.data?.matchWarrantyToSerial)
|
||||
}
|
||||
} catch {
|
||||
showSnack('خطا در بارگذاری دادهها', 'error')
|
||||
} finally {
|
||||
|
|
|
@ -34,15 +34,15 @@
|
|||
<v-icon start>mdi-file-export</v-icon>
|
||||
حوالههای خروج
|
||||
</v-tab>
|
||||
<v-tab value="input">
|
||||
<v-tab v-if="permissions.store" value="input">
|
||||
<v-icon start>mdi-file-import</v-icon>
|
||||
حوالههای ورود
|
||||
</v-tab>
|
||||
<v-tab value="transfer">
|
||||
<v-tab v-if="permissions.store" value="transfer">
|
||||
<v-icon start>mdi-swap-horizontal</v-icon>
|
||||
حوالههای انتقال
|
||||
</v-tab>
|
||||
<v-tab value="waste">
|
||||
<v-tab v-if="permissions.store" value="waste">
|
||||
<v-icon start>mdi-delete-empty</v-icon>
|
||||
ضایعات
|
||||
</v-tab>
|
||||
|
@ -55,7 +55,7 @@
|
|||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="outputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
<v-tab value="pending">در انتظار تایید</v-tab>
|
||||
<v-tab v-if="permissions.store" value="pending">در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="displayOutputItems" :search="outputSearchValue" :loading="loading"
|
||||
|
@ -75,6 +75,12 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="item.approved" @click="printTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-printer</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>چاپ PDF</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
|
@ -87,12 +93,18 @@
|
|||
</template>
|
||||
<v-list-item-title>لغو تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="deleteTicket('output', item.code)">
|
||||
<v-list-item v-if="permissions.store" @click="deleteTicket('output', 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-item v-if="permissions.store && (!item.completed || currentUser.owner) && item.approved" @click="completeProcess('output', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تکمیل پروسه</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</td>
|
||||
|
@ -356,7 +368,11 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from "axios";
|
||||
import Swal from 'sweetalert2';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Ticket {
|
||||
code: string;
|
||||
|
@ -377,6 +393,7 @@ interface Ticket {
|
|||
email: string;
|
||||
id: number;
|
||||
};
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
interface Header {
|
||||
|
@ -388,6 +405,12 @@ interface Header {
|
|||
visible: boolean;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
store?: boolean;
|
||||
storehelper?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const inputItems = ref<Ticket[]>([]);
|
||||
const outputItems = ref<Ticket[]>([]);
|
||||
|
@ -398,6 +421,7 @@ const outputSearchValue = ref('');
|
|||
const transferSearchValue = ref('');
|
||||
const wasteSearchValue = ref('');
|
||||
const activeTab = ref('output');
|
||||
const permissions = ref<Permissions>({});
|
||||
const inputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const outputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const transferSubTab = ref<'approved' | 'pending'>('approved');
|
||||
|
@ -454,6 +478,25 @@ const isColumnVisible = (key: string) => {
|
|||
|
||||
const LOCAL_STORAGE_KEY = 'hesabix_storeroom_tickets_table_columns';
|
||||
|
||||
const printTicket = async (code: string) => {
|
||||
try {
|
||||
const response = await axios.post('/api/storeroom/print/ticket', {
|
||||
code: code,
|
||||
type: 'output'
|
||||
})
|
||||
window.open(`${import.meta.env.VITE_API_URL}/front/print/${response.data.id}`, '_blank', 'noreferrer')
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
Swal.fire({
|
||||
text: error?.response?.data?.message,
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'قبول'
|
||||
});
|
||||
}
|
||||
console.error('Error printing ticket:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadColumnSettings = () => {
|
||||
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (savedSettings) {
|
||||
|
@ -524,6 +567,16 @@ const loadCurrentUser = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const loadPermissions = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
permissions.value = response.data || {};
|
||||
} catch (error: any) {
|
||||
console.error('Error loading permissions:', error);
|
||||
permissions.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
|
@ -667,10 +720,15 @@ const confirmDelete = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
const completeProcess = (type: 'input' | 'output' | 'transfer' | 'waste', code: string) => {
|
||||
router.push(`/acc/storeroom/ticket/complete/${code}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadColumnSettings();
|
||||
loadBusinessInfo();
|
||||
loadCurrentUser();
|
||||
loadPermissions();
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
|
504
webUI/src/views/acc/storeroom/io/ticketListHelper.vue
Normal file
504
webUI/src/views/acc/storeroom/io/ticketListHelper.vue
Normal file
|
@ -0,0 +1,504 @@
|
|||
<template>
|
||||
<v-toolbar color="toolbar" title="کمک انباردار">
|
||||
<template v-slot:prepend>
|
||||
<v-tooltip text="بازگشت" location="bottom">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
|
||||
icon="mdi-arrow-right" />
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-spacer />
|
||||
|
||||
<v-slide-group show-arrows>
|
||||
<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-tabs v-model="activeTab" color="primary" grow class="mb-3">
|
||||
<v-tab value="output">
|
||||
<v-icon start>mdi-file-export</v-icon>
|
||||
حوالههای خروج
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-window v-model="activeTab">
|
||||
<!-- تب حوالههای خروج -->
|
||||
<v-window-item value="output">
|
||||
<v-text-field v-model="outputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="outputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="displayOutputItems" :search="outputSearchValue" :loading="loading"
|
||||
hover density="compact" 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/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 v-if="item.approved" @click="printTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-printer</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>چاپ PDF</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="(permissions.store || permissions.storehelper) && (!item.completed || currentUser.owner) && item.approved" @click="completeProcess('output', item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-check-decagram</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">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('completedStatus')" class="text-center">
|
||||
<v-chip size="small" :color="getCompletedStatusColor(item)">
|
||||
{{ getCompletedStatusText(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('completedBy')" class="text-center">
|
||||
{{ item.completedBy?.fullName || '-' }}
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? 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>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import axios from "axios";
|
||||
import Swal from 'sweetalert2';
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
interface Ticket {
|
||||
code: string;
|
||||
date: string;
|
||||
doc: {
|
||||
code: string;
|
||||
preview?: boolean;
|
||||
approved?: boolean;
|
||||
};
|
||||
person: {
|
||||
nikename: string;
|
||||
};
|
||||
des: string;
|
||||
preview?: boolean;
|
||||
approved?: boolean;
|
||||
completedBy?: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
id: number;
|
||||
};
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
interface Header {
|
||||
title: string;
|
||||
key: string;
|
||||
align?: 'start' | 'center' | 'end';
|
||||
sortable: boolean;
|
||||
width: number;
|
||||
visible: boolean;
|
||||
}
|
||||
|
||||
interface Permissions {
|
||||
store?: boolean;
|
||||
storehelper?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
const inputItems = ref<Ticket[]>([]);
|
||||
const outputItems = ref<Ticket[]>([]);
|
||||
const transferItems = ref<Ticket[]>([]);
|
||||
const wasteItems = ref<Ticket[]>([]);
|
||||
const inputSearchValue = ref('');
|
||||
const outputSearchValue = ref('');
|
||||
const transferSearchValue = ref('');
|
||||
const wasteSearchValue = ref('');
|
||||
const activeTab = ref('output');
|
||||
const permissions = ref<Permissions>({});
|
||||
const inputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const outputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const transferSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const wasteSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const showColumnDialog = ref(false);
|
||||
const business = ref({
|
||||
requireTwoStepApproval: false,
|
||||
approvers: { warehouseTransfer: null }
|
||||
});
|
||||
const currentUser = ref({ email: '', owner: false });
|
||||
|
||||
const deleteDialog = ref({
|
||||
show: false,
|
||||
type: null as 'input' | 'output' | 'transfer' | 'waste' | null,
|
||||
code: null as string | null
|
||||
});
|
||||
|
||||
const snackbar = ref({
|
||||
show: false,
|
||||
message: '',
|
||||
color: 'primary'
|
||||
});
|
||||
|
||||
const allHeaders = ref<Header[]>([
|
||||
{ title: "عملیات", key: "operation", align: 'center' as const, sortable: false, width: 100, visible: true },
|
||||
{ title: "شماره", key: "code", align: 'center' as const, sortable: true, width: 100, visible: true },
|
||||
{ title: "تاریخ", key: "date", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "وضعیت ارسال", key: "completedStatus", align: 'center' as const, sortable: true, width: 150, visible: true },
|
||||
{ title: "ارسال کننده", key: "completedBy", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شماره فاکتور", key: "doc.code", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شخص", key: "person.nikename", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "توضیحات", key: "des", align: 'center' as const, sortable: true, width: 200, visible: true },
|
||||
]);
|
||||
|
||||
const visibleHeaders = computed(() => {
|
||||
return allHeaders.value.filter((header: Header) => {
|
||||
if ((header.key === 'completedStatus' || header.key === 'completedBy') && !business.value.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
return header.visible;
|
||||
}) as any;
|
||||
});
|
||||
|
||||
const isColumnVisible = (key: string) => {
|
||||
const header = allHeaders.value.find((header: Header) => header.key === key);
|
||||
if (!header) return false;
|
||||
|
||||
if ((key === 'completedStatus' || key === 'completedBy') && !business.value.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return header.visible;
|
||||
};
|
||||
|
||||
const LOCAL_STORAGE_KEY = 'hesabix_storeroom_tickets_table_columns';
|
||||
|
||||
const printTicket = async (code: string) => {
|
||||
try {
|
||||
const response = await axios.post('/api/storeroom/print/ticket', {
|
||||
code: code,
|
||||
type: 'output'
|
||||
})
|
||||
window.open(`${import.meta.env.VITE_API_URL}/front/print/${response.data.id}`, '_blank', 'noreferrer')
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
Swal.fire({
|
||||
text: error?.response?.data?.message,
|
||||
icon: 'warning',
|
||||
confirmButtonText: 'قبول'
|
||||
});
|
||||
}
|
||||
console.error('Error printing ticket:', error)
|
||||
}
|
||||
}
|
||||
|
||||
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 displayInputItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return inputItems.value;
|
||||
return inputSubTab.value === 'pending'
|
||||
? inputItems.value.filter(i => i.preview && !i.approved)
|
||||
: inputItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayOutputItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return outputItems.value;
|
||||
return outputSubTab.value === 'pending'
|
||||
? outputItems.value.filter(i => i.preview && !i.approved)
|
||||
: outputItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayTransferItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return transferItems.value;
|
||||
return transferSubTab.value === 'pending'
|
||||
? transferItems.value.filter(i => i.preview && !i.approved)
|
||||
: transferItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayWasteItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return wasteItems.value;
|
||||
return wasteSubTab.value === 'pending'
|
||||
? wasteItems.value.filter(i => i.preview && !i.approved)
|
||||
: wasteItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const loadBusinessInfo = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
|
||||
business.value = response.data || { requireTwoStepApproval: false, approvers: { warehouseTransfer: null } };
|
||||
} catch (error: any) {
|
||||
console.error('Error loading business info:', error);
|
||||
business.value = { requireTwoStepApproval: false, approvers: { warehouseTransfer: null } };
|
||||
}
|
||||
};
|
||||
|
||||
const loadCurrentUser = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
currentUser.value = response.data || { email: '', owner: false };
|
||||
} catch (error: any) {
|
||||
console.error('Error loading current user:', error);
|
||||
currentUser.value = { email: '', owner: false };
|
||||
}
|
||||
};
|
||||
|
||||
const loadPermissions = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
permissions.value = response.data || {};
|
||||
} catch (error: any) {
|
||||
console.error('Error loading permissions:', error);
|
||||
permissions.value = {};
|
||||
}
|
||||
};
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const [outputResponse] = await Promise.all([
|
||||
axios.post('/api/storeroom/tickets/list/output'),
|
||||
]);
|
||||
outputItems.value = (outputResponse.data || []);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading data:', error);
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: 'خطا در بارگذاری دادهها: ' + error.message,
|
||||
color: 'error'
|
||||
};
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const checkApprover = () => {
|
||||
return business.value.requireTwoStepApproval && (business.value.approvers.warehouseTransfer === currentUser.value.email || currentUser.value.owner === true);
|
||||
};
|
||||
|
||||
const canShowApprovalButton = (item: Ticket) => {
|
||||
if (!checkApprover()) return false;
|
||||
if (item?.approved) return false;
|
||||
return true;
|
||||
};
|
||||
|
||||
const canShowUnapproveButton = (item: Ticket) => {
|
||||
return !canShowApprovalButton(item) && checkApprover();
|
||||
};
|
||||
|
||||
const approveTicket = async (code: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await axios.post(`/api/approval/approve/storeroom/${code}`);
|
||||
|
||||
await loadData();
|
||||
|
||||
if (response.data.success) {
|
||||
snackbar.value = { show: true, message: 'حواله تایید شد', color: 'success' };
|
||||
} else {
|
||||
snackbar.value = { show: true, message: response.data.message, color: 'error' };
|
||||
}
|
||||
} catch (error: any) {
|
||||
snackbar.value = { show: true, message: 'خطا در تایید حواله: ' + (error.response?.data?.message || error.message), color: 'error' };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const unapproveTicket = async (code: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
const response = await axios.post(`/api/approval/unapprove/storeroom/${code}`);
|
||||
|
||||
await loadData();
|
||||
|
||||
if (response.data.success) {
|
||||
snackbar.value = { show: true, message: 'حواله لغو تایید شد', color: 'success' };
|
||||
} else {
|
||||
snackbar.value = { show: true, message: response.data.message, color: 'error' };
|
||||
}
|
||||
} catch (error: any) {
|
||||
snackbar.value = { show: true, message: 'خطا در لغو تایید حواله: ' + (error.response?.data?.message || error.message), color: 'error' };
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const getCompletedStatusText = (item: Ticket) => {
|
||||
if (item?.completed) return 'ارسال شده';
|
||||
return 'در انتظار ارسال';
|
||||
};
|
||||
|
||||
const getCompletedStatusColor = (item: Ticket) => {
|
||||
if (item?.completed) return 'success';
|
||||
return 'warning';
|
||||
};
|
||||
|
||||
const deleteTicket = (type: 'input' | 'output' | 'transfer' | 'waste', 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 if (type === 'output') {
|
||||
outputItems.value = outputItems.value.filter(item => item.code !== code);
|
||||
} else if (type === 'transfer') {
|
||||
transferItems.value = transferItems.value.filter(item => item.code !== code);
|
||||
} else {
|
||||
wasteItems.value = wasteItems.value.filter(item => item.code !== code);
|
||||
}
|
||||
|
||||
snackbar.value = {
|
||||
show: true,
|
||||
message: 'حواله انبار حذف شد.',
|
||||
color: 'success'
|
||||
};
|
||||
} catch (error: any) {
|
||||
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
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const completeProcess = (type: 'input' | 'output' | 'transfer' | 'waste', code: string) => {
|
||||
router.push(`/acc/storeroom/ticket/complete/${code}`);
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
loadColumnSettings();
|
||||
loadBusinessInfo();
|
||||
loadCurrentUser();
|
||||
loadPermissions();
|
||||
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>
|
|
@ -106,10 +106,11 @@ const headers = [
|
|||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [ticketResponse, businessResponse, warrantyResponse] = await Promise.all([
|
||||
const [ticketResponse, businessResponse, warrantyResponse, pluginsResponse] = await Promise.all([
|
||||
axios.post(`/api/storeroom/tickets/info/${router.currentRoute.value.params.id}`),
|
||||
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`),
|
||||
axios.get(`/api/plugins/warranty/serials/by-storeroom-ticket/${router.currentRoute.value.params.id}`)
|
||||
axios.get(`/api/plugins/warranty/serials/by-storeroom-ticket/${router.currentRoute.value.params.id}`),
|
||||
axios.post('/api/plugin/get/actives')
|
||||
])
|
||||
|
||||
item.value.ticket = ticketResponse.data.ticket
|
||||
|
@ -122,6 +123,8 @@ const loadData = async () => {
|
|||
// warranty data
|
||||
warrantySerials.value = (warrantyResponse.data && warrantyResponse.data.items) ? warrantyResponse.data.items : []
|
||||
ticketActivationCode.value = warrantyResponse.data ? (warrantyResponse.data.ticketActivationCode || null) : null
|
||||
|
||||
plugins.value = pluginsResponse.data
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error)
|
||||
} finally {
|
||||
|
@ -136,7 +139,7 @@ const printInvoice = async () => {
|
|||
type: item.value.ticket.type
|
||||
})
|
||||
window.open(`${import.meta.env.VITE_API_URL}/front/print/${response.data.id}`, '_blank', 'noreferrer')
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error?.response?.data?.message) {
|
||||
Swal.fire({
|
||||
text: error?.response?.data?.message,
|
||||
|
@ -333,6 +336,10 @@ const previewAttachment = async (att: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
const plugins = ref<any>({})
|
||||
|
||||
const isPluginActive = (plugName: string) => plugins.value[plugName] !== undefined
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAttachments()
|
||||
|
@ -502,9 +509,9 @@ onMounted(() => {
|
|||
</v-card>
|
||||
|
||||
<!-- Warranty Section -->
|
||||
<v-card variant="outlined" class="mt-4">
|
||||
<v-card variant="outlined" class="mt-4" v-if="isPluginActive('warranty')">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
<v-icon start>mdi-shield-check</v-icon>
|
||||
<v-icon start>mdi-shield-check</v-icon>
|
||||
گارانتیهای حواله و کد فعالسازی
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue