Resolve merge conflicts in BusinessController.php
This commit is contained in:
commit
1418591120
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\Log;
|
||||||
use App\Entity\Permission;
|
use App\Entity\Permission;
|
||||||
use App\Entity\User;
|
use App\Entity\User;
|
||||||
|
use App\Entity\Year;
|
||||||
use App\Service\Access;
|
use App\Service\Access;
|
||||||
use App\Service\Log as LogService;
|
use App\Service\Log as LogService;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
@ -55,6 +56,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$ticket->setIsPreview(false);
|
$ticket->setIsPreview(false);
|
||||||
$ticket->setIsApproved(true);
|
$ticket->setIsApproved(true);
|
||||||
$ticket->setApprovedBy($user);
|
$ticket->setApprovedBy($user);
|
||||||
|
@ -117,6 +122,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($ticket->getDoc(), $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'حواله مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$ticket->setIsPreview(true);
|
$ticket->setIsPreview(true);
|
||||||
$ticket->setIsApproved(false);
|
$ticket->setIsApproved(false);
|
||||||
$ticket->setApprovedBy(null);
|
$ticket->setApprovedBy(null);
|
||||||
|
@ -179,6 +188,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(false);
|
$document->setIsPreview(false);
|
||||||
$document->setIsApproved(true);
|
$document->setIsApproved(true);
|
||||||
$document->setApprovedBy($user);
|
$document->setApprovedBy($user);
|
||||||
|
@ -262,6 +275,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'فاکتور فروش تایید شده است']);
|
return $this->json(['success' => false, 'message' => 'فاکتور فروش تایید شده است']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(false);
|
$document->setIsPreview(false);
|
||||||
$document->setIsApproved(true);
|
$document->setIsApproved(true);
|
||||||
$document->setApprovedBy($user);
|
$document->setApprovedBy($user);
|
||||||
|
@ -338,6 +355,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(true);
|
$document->setIsPreview(true);
|
||||||
$document->setIsApproved(false);
|
$document->setIsApproved(false);
|
||||||
$document->setApprovedBy(null);
|
$document->setApprovedBy(null);
|
||||||
|
@ -413,6 +434,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(false);
|
$document->setIsPreview(false);
|
||||||
$document->setIsApproved(true);
|
$document->setIsApproved(true);
|
||||||
$document->setApprovedBy($user);
|
$document->setApprovedBy($user);
|
||||||
|
@ -483,6 +508,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']);
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید شده است']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(false);
|
$document->setIsPreview(false);
|
||||||
$document->setIsApproved(true);
|
$document->setIsApproved(true);
|
||||||
$document->setApprovedBy($user);
|
$document->setApprovedBy($user);
|
||||||
|
@ -547,6 +576,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(true);
|
$document->setIsPreview(true);
|
||||||
$document->setIsApproved(false);
|
$document->setIsApproved(false);
|
||||||
$document->setApprovedBy(null);
|
$document->setApprovedBy(null);
|
||||||
|
@ -617,6 +650,10 @@ class ApprovalController extends AbstractController
|
||||||
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']);
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید تایید نشده است']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDocumentYear($document, $business, $entityManager)) {
|
||||||
|
return $this->json(['success' => false, 'message' => 'فاکتور خرید مربوط به این سال مالی نیست']);
|
||||||
|
}
|
||||||
|
|
||||||
$document->setIsPreview(true);
|
$document->setIsPreview(true);
|
||||||
$document->setIsApproved(false);
|
$document->setIsApproved(false);
|
||||||
$document->setApprovedBy(null);
|
$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
|
private function canUserApproveDocument(User $user, Business $business, string $documentType): bool
|
||||||
{
|
{
|
||||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||||
|
|
|
@ -583,11 +583,13 @@ class BusinessController extends AbstractController
|
||||||
'plugGhestaManager' => true,
|
'plugGhestaManager' => true,
|
||||||
'plugTaxSettings' => true,
|
'plugTaxSettings' => true,
|
||||||
'plugWarranty' => true,
|
'plugWarranty' => true,
|
||||||
|
'plugImportWorkflow' => true,
|
||||||
'inquiry' => true,
|
'inquiry' => true,
|
||||||
'ai' => true,
|
'ai' => true,
|
||||||
'warehouseManager' => true,
|
'warehouseManager' => true,
|
||||||
'importWorkflow' => true,
|
'importWorkflow' => true,
|
||||||
'plugHrmAttendance' => true,
|
'plugHrmAttendance' => true,
|
||||||
|
'storehelper' => true,
|
||||||
];
|
];
|
||||||
} elseif ($perm) {
|
} elseif ($perm) {
|
||||||
$result = [
|
$result = [
|
||||||
|
@ -633,19 +635,14 @@ class BusinessController extends AbstractController
|
||||||
'plugGhestaManager' => $perm->isPlugGhestaManager(),
|
'plugGhestaManager' => $perm->isPlugGhestaManager(),
|
||||||
'plugTaxSettings' => $perm->isPlugTaxSettings(),
|
'plugTaxSettings' => $perm->isPlugTaxSettings(),
|
||||||
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
||||||
|
'plugImportWorkflow' => $perm->isImportWorkflow(),
|
||||||
'inquiry' => $perm->isInquiry(),
|
'inquiry' => $perm->isInquiry(),
|
||||||
'ai' => $perm->isAi(),
|
'ai' => $perm->isAi(),
|
||||||
'warehouseManager' => $perm->isWarehouseManager(),
|
'warehouseManager' => $perm->isWarehouseManager(),
|
||||||
'importWorkflow' => $perm->isImportWorkflow(),
|
'importWorkflow' => $perm->isImportWorkflow(),
|
||||||
'plugHrmAttendance' => $perm->isPlugHrmAttendance(),
|
'plugHrmAttendance' => $perm->isPlugHrmAttendance(),
|
||||||
|
'storehelper' => $perm->isStorehelper()
|
||||||
];
|
];
|
||||||
|
|
||||||
if ($perm->isWarehouseManager()) {
|
|
||||||
$result['commodity'] = true;
|
|
||||||
$result['store'] = true;
|
|
||||||
$result['plugWarranty'] = true;
|
|
||||||
$result['permission'] = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return $this->json($result);
|
return $this->json($result);
|
||||||
}
|
}
|
||||||
|
@ -716,11 +713,13 @@ class BusinessController extends AbstractController
|
||||||
$perm->setPlugGhestaManager($params['plugGhestaManager']);
|
$perm->setPlugGhestaManager($params['plugGhestaManager']);
|
||||||
$perm->setPlugWarrantyManager($params['plugWarranty'] ?? false);
|
$perm->setPlugWarrantyManager($params['plugWarranty'] ?? false);
|
||||||
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
||||||
|
$perm->setImportWorkflow($params['plugImportWorkflow'] ?? false);
|
||||||
$perm->setInquiry($params['inquiry']);
|
$perm->setInquiry($params['inquiry']);
|
||||||
$perm->setAi($params['ai']);
|
$perm->setAi($params['ai']);
|
||||||
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
|
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
|
||||||
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
|
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
|
||||||
$perm->setPlugHrmAttendance($params['plugHrmAttendance'] ?? false);
|
$perm->setPlugHrmAttendance($params['plugHrmAttendance'] ?? false);
|
||||||
|
$perm->setStorehelper($params['storehelper'] ?? false);
|
||||||
$entityManager->persist($perm);
|
$entityManager->persist($perm);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
$log->insert('تنظیمات پایه', 'ویرایش دسترسیهای کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
|
$log->insert('تنظیمات پایه', 'ویرایش دسترسیهای کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
|
||||||
|
|
|
@ -71,9 +71,9 @@ class PlugWarrantyController extends AbstractController
|
||||||
|
|
||||||
$serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s')
|
$serials = $entityManager->getRepository(PlugWarrantySerial::class)->createQueryBuilder('s')
|
||||||
->andWhere('s.business = :bid')
|
->andWhere('s.business = :bid')
|
||||||
->andWhere('s.allocatedToDocumentId = :docId')
|
->andWhere('s.activationTicketCode = :code')
|
||||||
->setParameter('bid', $acc['bid'])
|
->setParameter('bid', $acc['bid'])
|
||||||
->setParameter('docId', $doc->getId())
|
->setParameter('code', $code)
|
||||||
->getQuery()
|
->getQuery()
|
||||||
->getResult();
|
->getResult();
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ class PlugWarrantyController extends AbstractController
|
||||||
$commodity = $s->getCommodity();
|
$commodity = $s->getCommodity();
|
||||||
return [
|
return [
|
||||||
'serialNumber' => $s->getSerialNumber(),
|
'serialNumber' => $s->getSerialNumber(),
|
||||||
|
'commoditySerial' => $s->getCommoditySerial(),
|
||||||
'commodity' => $commodity ? [
|
'commodity' => $commodity ? [
|
||||||
'id' => $commodity->getId(),
|
'id' => $commodity->getId(),
|
||||||
'name' => $commodity->getName(),
|
'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
|
public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse
|
||||||
{
|
{
|
||||||
$acc = $access->hasRole('store');
|
$acc = $access->hasRole('store');
|
||||||
if (!$acc) throw $this->createAccessDeniedException();
|
if (!$acc)
|
||||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
throw $this->createAccessDeniedException();
|
||||||
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
|
||||||
|
if (!$ticket)
|
||||||
|
throw $this->createNotFoundException('حواله یافت نشد');
|
||||||
|
|
||||||
$file = $request->files->get('file');
|
$file = $request->files->get('file');
|
||||||
if (!$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 = new ArchiveFile();
|
||||||
$archive->setBid($acc['bid']);
|
$archive->setBid($acc['bid']);
|
||||||
|
@ -82,33 +84,35 @@ class StoreroomController extends AbstractController
|
||||||
$archive->setDes($request->request->get('des'));
|
$archive->setDes($request->request->get('des'));
|
||||||
$archive->setRelatedDocType('storeroom_ticket');
|
$archive->setRelatedDocType('storeroom_ticket');
|
||||||
$archive->setRelatedDocCode($ticket->getCode());
|
$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->persist($archive);
|
||||||
$entityManager->flush();
|
$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'])]
|
#[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])]
|
||||||
public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
{
|
{
|
||||||
$acc = $access->hasRole('store');
|
$acc = $access->hasRole('store');
|
||||||
if (!$acc) throw $this->createAccessDeniedException();
|
if (!$acc)
|
||||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
throw $this->createAccessDeniedException();
|
||||||
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid' => $acc['bid'], 'code' => $code]);
|
||||||
|
if (!$ticket)
|
||||||
|
throw $this->createNotFoundException('حواله یافت نشد');
|
||||||
$items = $entityManager->getRepository(ArchiveFile::class)->findBy([
|
$items = $entityManager->getRepository(ArchiveFile::class)->findBy([
|
||||||
'bid'=>$acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
'relatedDocType'=>'storeroom_ticket',
|
'relatedDocType' => 'storeroom_ticket',
|
||||||
'relatedDocCode'=>$ticket->getCode()
|
'relatedDocCode' => $ticket->getCode()
|
||||||
], ['id'=>'DESC']);
|
], ['id' => 'DESC']);
|
||||||
return $this->json(array_map(function(ArchiveFile $a){
|
return $this->json(array_map(function (ArchiveFile $a) {
|
||||||
return [
|
return [
|
||||||
'id'=>$a->getId(),
|
'id' => $a->getId(),
|
||||||
'filename'=>$a->getFilename(),
|
'filename' => $a->getFilename(),
|
||||||
'fileType'=>$a->getFileType(),
|
'fileType' => $a->getFileType(),
|
||||||
'fileSize'=>$a->getFileSize(),
|
'fileSize' => $a->getFileSize(),
|
||||||
'des'=>$a->getDes(),
|
'des' => $a->getDes(),
|
||||||
'dateSubmit'=>$a->getDateSubmit(),
|
'dateSubmit' => $a->getDateSubmit(),
|
||||||
];
|
];
|
||||||
}, $items));
|
}, $items));
|
||||||
}
|
}
|
||||||
|
@ -117,12 +121,13 @@ class StoreroomController extends AbstractController
|
||||||
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
|
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
|
||||||
{
|
{
|
||||||
$acc = $access->hasRole('store');
|
$acc = $access->hasRole('store');
|
||||||
if (!$acc) throw $this->createAccessDeniedException();
|
if (!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
$a = $entityManager->getRepository(ArchiveFile::class)->find($id);
|
$a = $entityManager->getRepository(ArchiveFile::class)->find($id);
|
||||||
if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) {
|
if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) {
|
||||||
throw $this->createNotFoundException('فایل یافت نشد');
|
throw $this->createNotFoundException('فایل یافت نشد');
|
||||||
}
|
}
|
||||||
$abs = $storage->absolutePath((string)$a->getFilename());
|
$abs = $storage->absolutePath((string) $a->getFilename());
|
||||||
if (!is_file($abs) || !is_readable($abs)) {
|
if (!is_file($abs) || !is_readable($abs)) {
|
||||||
throw $this->createNotFoundException('فایل موجود نیست');
|
throw $this->createNotFoundException('فایل موجود نیست');
|
||||||
}
|
}
|
||||||
|
@ -201,7 +206,7 @@ class StoreroomController extends AbstractController
|
||||||
* @throws ReflectionException
|
* @throws ReflectionException
|
||||||
*/
|
*/
|
||||||
#[Route('/api/storeroom/docs/get', name: 'app_storeroom_get_docs')]
|
#[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');
|
$acc = $access->hasRole('store');
|
||||||
if (!$acc)
|
if (!$acc)
|
||||||
|
@ -466,10 +471,8 @@ class StoreroomController extends AbstractController
|
||||||
if ($content = $request->getContent()) {
|
if ($content = $request->getContent()) {
|
||||||
$params = json_decode($content, true);
|
$params = json_decode($content, true);
|
||||||
}
|
}
|
||||||
//check parameters exist
|
|
||||||
if ((!array_key_exists('ticket', $params)) || (!array_key_exists('items', $params)) || (!array_key_exists('doc', $params)))
|
if ((!array_key_exists('ticket', $params)) || (!array_key_exists('items', $params)) || (!array_key_exists('doc', $params)))
|
||||||
$this->createNotFoundException();
|
$this->createNotFoundException();
|
||||||
//going to save
|
|
||||||
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
$doc = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||||
'id' => $params['doc']['id'],
|
'id' => $params['doc']['id'],
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
|
@ -479,14 +482,11 @@ class StoreroomController extends AbstractController
|
||||||
throw $this->createNotFoundException('سند یافت نشد');
|
throw $this->createNotFoundException('سند یافت نشد');
|
||||||
if ($doc->getBid()->getId() != $acc['bid']->getId())
|
if ($doc->getBid()->getId() != $acc['bid']->getId())
|
||||||
throw $this->createAccessDeniedException('دسترسی به این سند را ندارید.');
|
throw $this->createAccessDeniedException('دسترسی به این سند را ندارید.');
|
||||||
//find transfer type
|
|
||||||
if (!array_key_exists('transferType', $params['ticket']))
|
if (!array_key_exists('transferType', $params['ticket']))
|
||||||
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
||||||
$transferType = $entityManager->getRepository(StoreroomTransferType::class)->find($params['ticket']['transferType']['id']);
|
$transferType = $entityManager->getRepository(StoreroomTransferType::class)->find($params['ticket']['transferType']['id']);
|
||||||
if (!$transferType)
|
if (!$transferType)
|
||||||
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
throw $this->createNotFoundException('نوع انتقال یافت نشد');
|
||||||
|
|
||||||
//find storeroom
|
|
||||||
if (!array_key_exists('store', $params['ticket']))
|
if (!array_key_exists('store', $params['ticket']))
|
||||||
throw $this->createNotFoundException('انبار یافت نشد');
|
throw $this->createNotFoundException('انبار یافت نشد');
|
||||||
$storeroom = $entityManager->getRepository(Storeroom::class)->find($params['ticket']['store']['id']);
|
$storeroom = $entityManager->getRepository(Storeroom::class)->find($params['ticket']['store']['id']);
|
||||||
|
@ -494,7 +494,6 @@ class StoreroomController extends AbstractController
|
||||||
throw $this->createNotFoundException('انبار یافت نشد');
|
throw $this->createNotFoundException('انبار یافت نشد');
|
||||||
elseif ($storeroom->getBid()->getId() != $acc['bid']->getId())
|
elseif ($storeroom->getBid()->getId() != $acc['bid']->getId())
|
||||||
throw $this->createAccessDeniedException('دسترسی به این انبار ممکن نیست!');
|
throw $this->createAccessDeniedException('دسترسی به این انبار ممکن نیست!');
|
||||||
//find person
|
|
||||||
if (!array_key_exists('person', $params['ticket']))
|
if (!array_key_exists('person', $params['ticket']))
|
||||||
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
||||||
$person = $entityManager->getRepository(Person::class)->find($params['ticket']['person']['id']);
|
$person = $entityManager->getRepository(Person::class)->find($params['ticket']['person']['id']);
|
||||||
|
@ -502,7 +501,6 @@ class StoreroomController extends AbstractController
|
||||||
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
throw $this->createNotFoundException('طرف حساب یافت نشد');
|
||||||
elseif ($person->getBid()->getId() != $acc['bid']->getId())
|
elseif ($person->getBid()->getId() != $acc['bid']->getId())
|
||||||
throw $this->createAccessDeniedException('دسترسی به این طرف حساب ممکن نیست!');
|
throw $this->createAccessDeniedException('دسترسی به این طرف حساب ممکن نیست!');
|
||||||
|
|
||||||
$ticket = new StoreroomTicket();
|
$ticket = new StoreroomTicket();
|
||||||
$ticket->setSubmitter($this->getUser());
|
$ticket->setSubmitter($this->getUser());
|
||||||
$ticket->setDate($params['ticket']['date']);
|
$ticket->setDate($params['ticket']['date']);
|
||||||
|
@ -515,7 +513,9 @@ class StoreroomController extends AbstractController
|
||||||
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
|
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
|
||||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||||
$rand = '';
|
$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->setActivationCode($rand);
|
||||||
$ticket->setReceiver($params['ticket']['receiver']);
|
$ticket->setReceiver($params['ticket']['receiver']);
|
||||||
$ticket->setTransferType($transferType);
|
$ticket->setTransferType($transferType);
|
||||||
|
@ -529,32 +529,25 @@ class StoreroomController extends AbstractController
|
||||||
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
|
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
|
||||||
}
|
}
|
||||||
$entityManager->persist($ticket);
|
$entityManager->persist($ticket);
|
||||||
//$entityManager->flush();
|
|
||||||
|
|
||||||
//going to save rows
|
|
||||||
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
||||||
'doc' => $doc
|
'doc' => $doc
|
||||||
]);
|
]);
|
||||||
|
$requireWarrantySerial = (
|
||||||
// Determine if warranty serials are required based on flag or provided lines
|
isset($params['ticket']['requireWarrantySerial'])
|
||||||
$hasSerialLines = false;
|
&& $params['ticket']['requireWarrantySerial'] === true
|
||||||
foreach (($params['items'] ?? []) as $it) {
|
&& $pluginService->isActive('warranty', $acc['bid'])
|
||||||
if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; }
|
);
|
||||||
}
|
|
||||||
$requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines;
|
|
||||||
if ($requireWarrantySerial) {
|
if ($requireWarrantySerial) {
|
||||||
if (!$pluginService->isActive('warranty', $acc['bid'])) {
|
|
||||||
return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403);
|
|
||||||
}
|
|
||||||
// Validate counts up-front
|
|
||||||
foreach ($params['items'] as $item) {
|
foreach ($params['items'] as $item) {
|
||||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||||
if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) {
|
if ((int) ($item['ticketCount'] ?? 0) > 0 && count($lines) < (int) $item['ticketCount']) {
|
||||||
return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400);
|
return $this->json([
|
||||||
|
'result' => -3,
|
||||||
|
'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'
|
||||||
|
], 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($params['items'] as $item) {
|
foreach ($params['items'] as $item) {
|
||||||
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
|
@ -565,7 +558,6 @@ class StoreroomController extends AbstractController
|
||||||
throw $this->createNotFoundException('کالا یافت نشد!');
|
throw $this->createNotFoundException('کالا یافت نشد!');
|
||||||
if (!$row->getCommodity())
|
if (!$row->getCommodity())
|
||||||
throw $this->createNotFoundException('کالا یافت نشد!');
|
throw $this->createNotFoundException('کالا یافت نشد!');
|
||||||
//check row count not upper ticket count
|
|
||||||
if ($row->getCommdityCount() < $item['ticketCount'])
|
if ($row->getCommdityCount() < $item['ticketCount'])
|
||||||
throw $this->createNotFoundException('تعداد کالای اضافه شده بیشتر از تعداد کالا در فاکتور است.');
|
throw $this->createNotFoundException('تعداد کالای اضافه شده بیشتر از تعداد کالا در فاکتور است.');
|
||||||
$ticketItem = new StoreroomItem();
|
$ticketItem = new StoreroomItem();
|
||||||
|
@ -578,21 +570,17 @@ class StoreroomController extends AbstractController
|
||||||
$ticketItem->setCommodity($row->getCommodity());
|
$ticketItem->setCommodity($row->getCommodity());
|
||||||
$ticketItem->setType($item['type']);
|
$ticketItem->setType($item['type']);
|
||||||
$entityManager->persist($ticketItem);
|
$entityManager->persist($ticketItem);
|
||||||
|
|
||||||
// Bind warranty serials per item if provided
|
|
||||||
if ($requireWarrantySerial) {
|
|
||||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||||
if ((int)$item['ticketCount'] > 0) {
|
if ($requireWarrantySerial) {
|
||||||
// Ensure we have an id to bind to
|
if ((int) $item['ticketCount'] > 0) {
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
$lines = array_slice($lines, 0, (int)$item['ticketCount']);
|
$lines = array_slice($lines, 0, (int) $item['ticketCount']);
|
||||||
foreach ($lines as $ln) {
|
foreach ($lines as $ln) {
|
||||||
$warrantyCode = $ln['warranty'] ?? null;
|
$warrantyCode = $ln['warranty'] ?? null;
|
||||||
$deviceSerial = $ln['serial'] ?? null;
|
$deviceSerial = $ln['serial'] ?? null;
|
||||||
if (!$warrantyCode) {
|
if (!$warrantyCode) {
|
||||||
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
|
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
|
||||||
}
|
}
|
||||||
/** @var PlugWarrantySerial|null $serial */
|
|
||||||
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||||
'business' => $acc['bid'],
|
'business' => $acc['bid'],
|
||||||
'serialNumber' => $warrantyCode,
|
'serialNumber' => $warrantyCode,
|
||||||
|
@ -613,27 +601,51 @@ class StoreroomController extends AbstractController
|
||||||
$entityManager->persist($serial);
|
$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();
|
$entityManager->flush();
|
||||||
|
|
||||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
$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) {
|
if ($businessRequire) {
|
||||||
$ticket->setIsPreview(true);
|
$ticket->setIsPreview(true);
|
||||||
$ticket->setIsApproved(false);
|
$ticket->setIsApproved(false);
|
||||||
$ticket->setApprovedBy(null); // هنوز تأیید نشده
|
$ticket->setApprovedBy(null);
|
||||||
} else {
|
} else {
|
||||||
$ticket->setIsPreview(false);
|
$ticket->setIsPreview(false);
|
||||||
$ticket->setIsApproved(true);
|
$ticket->setIsApproved(true);
|
||||||
$ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی
|
$ticket->setApprovedBy($this->getUser());
|
||||||
}
|
}
|
||||||
//save logs
|
|
||||||
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
||||||
if ($pluginService->isActive('accpro', $acc['bid'])) {
|
if ($pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
//notification to person
|
|
||||||
if ($params['ticket']['sms'] == true) {
|
if ($params['ticket']['sms'] == true) {
|
||||||
$ticket->setCanShare(true);
|
$ticket->setCanShare(true);
|
||||||
$entityManager->persist($ticket);
|
$entityManager->persist($ticket);
|
||||||
|
@ -676,7 +688,6 @@ class StoreroomController extends AbstractController
|
||||||
3
|
3
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($smsres == 2) {
|
if ($smsres == 2) {
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'result' => 2
|
'result' => 2
|
||||||
|
@ -684,7 +695,6 @@ class StoreroomController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'result' => 0
|
'result' => 0
|
||||||
]);
|
]);
|
||||||
|
@ -698,7 +708,7 @@ class StoreroomController extends AbstractController
|
||||||
throw $this->createAccessDeniedException();
|
throw $this->createAccessDeniedException();
|
||||||
$params = json_decode($request->getContent() ?: '{}', true);
|
$params = json_decode($request->getContent() ?: '{}', true);
|
||||||
$status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval
|
$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);
|
return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400);
|
||||||
}
|
}
|
||||||
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
|
||||||
|
@ -729,7 +739,7 @@ class StoreroomController extends AbstractController
|
||||||
$criteria['status'] = $status;
|
$criteria['status'] = $status;
|
||||||
}
|
}
|
||||||
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']);
|
$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 [
|
return [
|
||||||
'code' => $t->getCode(),
|
'code' => $t->getCode(),
|
||||||
'date' => $t->getDate(),
|
'date' => $t->getDate(),
|
||||||
|
@ -764,7 +774,8 @@ class StoreroomController extends AbstractController
|
||||||
'getDoc',
|
'getDoc',
|
||||||
'getTypeString',
|
'getTypeString',
|
||||||
'isPreview',
|
'isPreview',
|
||||||
'isApproved'
|
'isApproved',
|
||||||
|
'isCompleted'
|
||||||
], 2);
|
], 2);
|
||||||
|
|
||||||
foreach ($result as $key => &$ticket) {
|
foreach ($result as $key => &$ticket) {
|
||||||
|
@ -779,6 +790,16 @@ class StoreroomController extends AbstractController
|
||||||
} else {
|
} else {
|
||||||
$ticket['approvedBy'] = null;
|
$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);
|
return $this->json($result);
|
||||||
|
@ -799,7 +820,7 @@ class StoreroomController extends AbstractController
|
||||||
//get items
|
//get items
|
||||||
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
||||||
$res = [];
|
$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['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0);
|
||||||
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
||||||
//get rows
|
//get rows
|
||||||
|
@ -918,7 +939,7 @@ class StoreroomController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
$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 ($businessRequire && $doc->isApproved() !== true && $doc->isPreview() == true) {
|
||||||
if ($doc->isPreview()) {
|
if ($doc->isPreview()) {
|
||||||
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
|
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
|
||||||
|
@ -973,4 +994,103 @@ class StoreroomController extends AbstractController
|
||||||
);
|
);
|
||||||
return $this->json(['id' => $pdfPid]);
|
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_CONSUMED);
|
||||||
|
$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' => 'پروسه با موفقیت تکمیل شد'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -409,5 +409,15 @@ class ImportWorkflow
|
||||||
}
|
}
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getComputedTotalAmount(): ?string
|
||||||
|
{
|
||||||
|
$items = $this->getItems();
|
||||||
|
$total = 0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$total += $item->getTotalPrice();
|
||||||
|
}
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ class Permission
|
||||||
private ?bool $ai = null;
|
private ?bool $ai = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $warehouseManager = null;
|
private ?bool $storehelper = null;
|
||||||
|
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $importWorkflow = null;
|
private ?bool $importWorkflow = null;
|
||||||
|
@ -659,14 +659,14 @@ class Permission
|
||||||
return $this;
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -82,6 +82,12 @@ class PlugWarrantySerial
|
||||||
#[ORM\ManyToOne]
|
#[ORM\ManyToOne]
|
||||||
private ?Person $buyer = null;
|
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)]
|
#[ORM\Column(type: 'string', length: 32, nullable: true)]
|
||||||
private ?string $activationTicketCode = null;
|
private ?string $activationTicketCode = null;
|
||||||
|
|
||||||
|
@ -152,6 +158,12 @@ class PlugWarrantySerial
|
||||||
public function getBuyer(): ?Person { return $this->buyer; }
|
public function getBuyer(): ?Person { return $this->buyer; }
|
||||||
public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; }
|
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 getActivationTicketCode(): ?string { return $this->activationTicketCode; }
|
||||||
public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; }
|
public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; }
|
||||||
public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; }
|
public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; }
|
||||||
|
|
|
@ -96,6 +96,17 @@ class StoreroomTicket
|
||||||
#[ORM\JoinColumn(nullable: true)]
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
private ?User $approvedBy = null;
|
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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->storeroomItems = new ArrayCollection();
|
$this->storeroomItems = new ArrayCollection();
|
||||||
|
@ -409,4 +420,38 @@ class StoreroomTicket
|
||||||
$this->approvedBy = $approvedBy;
|
$this->approvedBy = $approvedBy;
|
||||||
return $this;
|
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
|
'user'=>$this->user
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if($warehousePermission && $warehousePermission->isWarehouseManager()){
|
if($warehousePermission && $warehousePermission->isStorehelper()){
|
||||||
$warehouseRoles = ['commodity', 'store', 'plugWarrantyManager'];
|
$warehouseRoles = ['commodity', 'store', 'plugWarrantyManager'];
|
||||||
if(in_array($roll, $warehouseRoles)){
|
if(in_array($roll, $warehouseRoles)){
|
||||||
return $accessArray;
|
return $accessArray;
|
||||||
|
|
|
@ -192,15 +192,15 @@ const rules = {
|
||||||
required: (value) => !!value || 'این فیلد الزامی است',
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
minLength: (value) => !value || value.length >= 2 || 'حداقل 2 کاراکتر الزامی است',
|
minLength: (value) => !value || value.length >= 2 || 'حداقل 2 کاراکتر الزامی است',
|
||||||
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
||||||
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
//email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
||||||
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
//phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||||
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
positiveMoney: (value) => {
|
positiveMoney: (value) => {
|
||||||
const numeric = parseMoneyInput(value)
|
const numeric = parseMoney(value)
|
||||||
return numeric > 0 || 'مقدار باید مثبت باشد'
|
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||||
},
|
},
|
||||||
maxAmount: (value) => !value || parseFloat(value) <= 999999999 || 'مبلغ نباید بیشتر از 999,999,999 باشد',
|
maxAmount: (value) => !value || parseMoney(value) <= 999999999 || 'مبلغ نباید بیشتر از 999,999,999 باشد',
|
||||||
maxExchangeRate: (value) => !value || parseFloat(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد'
|
maxExchangeRate: (value) => !value || parseMoney(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد'
|
||||||
}
|
}
|
||||||
|
|
||||||
const parseMoney = (val) => {
|
const parseMoney = (val) => {
|
||||||
|
|
|
@ -12,35 +12,32 @@
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
||||||
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
||||||
<template #append>
|
<template #prepend>
|
||||||
<v-btn icon small @click="openScanner" :disabled="isEdit" color="primary" variant="text">
|
<v-tooltip bottom size="small">
|
||||||
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
<template v-slot:activator="{ props }">
|
||||||
</v-btn>
|
<v-icon v-bind="props" color="primary" @click.stop="showQrScanner = true">mdi-barcode-scan</v-icon>
|
||||||
|
</template>
|
||||||
|
<span>اسکن بارکد</span>
|
||||||
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<Hcommoditysearch
|
<Hcommoditysearch :model-value="formData.commodity_id ?? undefined"
|
||||||
: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 }"
|
@update:modelValue="(val: number | Record<string, any>) => { formData.commodity_id = typeof val === 'number' ? val : (val as any)?.id ?? null }"
|
||||||
:return-object="false"
|
:return-object="false" label="محصول *" :rules="[rules.commodity]" required class="serial-commodity" />
|
||||||
label="محصول *"
|
|
||||||
:rules="[rules.commodity]"
|
|
||||||
required
|
|
||||||
class="serial-commodity"
|
|
||||||
/>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]" :ignore-year-range="true" dense
|
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]"
|
||||||
outlined hide-details="auto" />
|
:ignore-year-range="true" dense outlined hide-details="auto" />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
||||||
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense outlined
|
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" :ignore-year-range="true" dense
|
||||||
hide-details="auto" />
|
outlined hide-details="auto" />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
|
@ -70,35 +67,7 @@
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<v-dialog v-model="showQrScanner" :max-width="isMobile ? '95vw' : 560" persistent>
|
<BarcodeScanner v-model="showQrScanner" @detected="handleBarcodeScan" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
|
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
|
||||||
{{ notificationText }}
|
{{ notificationText }}
|
||||||
|
@ -110,9 +79,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, nextTick } from 'vue'
|
||||||
import { Html5Qrcode, Html5QrcodeSupportedFormats, Html5QrcodeScannerState } from 'html5-qrcode'
|
|
||||||
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
||||||
|
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
@ -195,93 +164,6 @@ const showNotify = (t: string, c: 'success' | 'error' | 'warning' | 'info' = 'su
|
||||||
}
|
}
|
||||||
|
|
||||||
const showQrScanner = ref(false)
|
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 save = async () => {
|
||||||
const res = await form.value?.validate()
|
const res = await form.value?.validate()
|
||||||
|
@ -297,7 +179,6 @@ const save = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
closeScanner()
|
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -346,13 +227,24 @@ const handleCommoditySelect = (c: any) => {
|
||||||
|
|
||||||
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
||||||
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
||||||
onBeforeUnmount(() => { closeScanner() })
|
|
||||||
|
const handleBarcodeScan = (val: string) => {
|
||||||
|
formData.value.serialNumber = val
|
||||||
|
showQrScanner.value = false
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* normalize Hcommoditysearch height with other inputs */
|
/* normalize Hcommoditysearch height with other inputs */
|
||||||
.serial-commodity :deep(.v-field) { min-height: 56px; }
|
.serial-commodity :deep(.v-field) {
|
||||||
.serial-commodity :deep(.v-field__input) { padding-top: 14px; padding-bottom: 14px; }
|
min-height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.serial-commodity :deep(.v-field__input) {
|
||||||
|
padding-top: 14px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
#qr-shaded-region {
|
#qr-shaded-region {
|
||||||
display: none !important;
|
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 {
|
.v-input--density-compact .v-field--variant-outlined {
|
||||||
height: 3rem;
|
height: 3rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdi-barcode-scan::before {
|
||||||
|
font-size: 25px !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
255
webUI/src/components/widgets/BarcodeScanner.vue
Normal file
255
webUI/src/components/widgets/BarcodeScanner.vue
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
<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 v-if="errorMessage" class="error-box">
|
||||||
|
<v-icon color="red" class="mr-2">mdi-alert-circle</v-icon>
|
||||||
|
{{ errorMessage }}
|
||||||
|
<div v-if="canRetry" class="mt-2">
|
||||||
|
<v-btn size="small" color="primary" @click="requestPermission">تلاش دوباره</v-btn>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else 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-if="!errorMessage">
|
||||||
|
<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 errorMessage = ref('')
|
||||||
|
const canRetry = ref(false)
|
||||||
|
|
||||||
|
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 = async (promise: Promise<void>) => {
|
||||||
|
try {
|
||||||
|
await promise
|
||||||
|
errorMessage.value = ''
|
||||||
|
canRetry.value = false
|
||||||
|
} catch (err: any) {
|
||||||
|
if (err.name === 'NotAllowedError' || err.message?.includes('permission')) {
|
||||||
|
errorMessage.value = 'برای استفاده از اسکنر لطفاً اجازه دسترسی به دوربین را بدهید.'
|
||||||
|
canRetry.value = true
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err.name === 'NotFoundError' || err.message?.includes('camera') || err.message?.includes('device')) {
|
||||||
|
errorMessage.value = 'هیچ دوربینی در دستگاه شما یافت نشد.'
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errorMessage.value = 'خطا در راهاندازی دوربین: ' + (err.message || 'خطای نامشخص')
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 requestPermission = async () => {
|
||||||
|
try {
|
||||||
|
await navigator.mediaDevices.getUserMedia({ video: true })
|
||||||
|
errorMessage.value = ''
|
||||||
|
canRetry.value = false
|
||||||
|
} catch {
|
||||||
|
errorMessage.value = 'دسترسی به دوربین رد شد.'
|
||||||
|
canRetry.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const close = () => {
|
||||||
|
internalShow.value = false
|
||||||
|
emit('close')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const devices = await navigator.mediaDevices.enumerateDevices()
|
||||||
|
availableCameras.value = devices.filter(device => device.kind === 'videoinput')
|
||||||
|
if (availableCameras.value.length === 0) {
|
||||||
|
errorMessage.value = 'هیچ دوربینی در دستگاه شما یافت نشد.'
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
errorMessage.value = 'خطا در بررسی دوربینهای موجود.'
|
||||||
|
canRetry.value = false
|
||||||
|
emit('error', new Error(errorMessage.value))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-box {
|
||||||
|
padding: 16px;
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -153,6 +153,7 @@ const fa_lang = {
|
||||||
storeroom: "انبار",
|
storeroom: "انبار",
|
||||||
storeroom_title: "انبارداری",
|
storeroom_title: "انبارداری",
|
||||||
storeroom_ticket: "حواله انبار",
|
storeroom_ticket: "حواله انبار",
|
||||||
|
storeroom_ticket_helper: "کمک انباردار",
|
||||||
storerooms: "انبارها",
|
storerooms: "انبارها",
|
||||||
commodity_exist_count: "موجودی کالا",
|
commodity_exist_count: "موجودی کالا",
|
||||||
inventory: "موجودی کالا",
|
inventory: "موجودی کالا",
|
||||||
|
|
|
@ -974,12 +974,24 @@ const router = createRouter({
|
||||||
component: () =>
|
component: () =>
|
||||||
import('../views/acc/storeroom/io/ticketList.vue'),
|
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',
|
path: 'storeroom/ticket/view/:id',
|
||||||
name: 'storeroom_ticket_view',
|
name: 'storeroom_ticket_view',
|
||||||
component: () =>
|
component: () =>
|
||||||
import('../views/acc/storeroom/io/view.vue'),
|
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',
|
path: 'storeroom/new/ticket/buy/:doc/:storeID',
|
||||||
name: 'storeroom_new_ticket_buy',
|
name: 'storeroom_new_ticket_buy',
|
||||||
|
|
|
@ -216,7 +216,8 @@ export default {
|
||||||
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||||
{ 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/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/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/plugins/import-workflow', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.plugImportWorkflow },
|
||||||
|
{ 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) {
|
restorePermissions(shortcuts) {
|
||||||
|
@ -555,7 +556,7 @@ export default {
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
<v-list-subheader color="primary">{{ $t('drawer.acc_store_tools') }}</v-list-subheader>
|
<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 }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-list-item class="text-dark" v-bind="props" prepend-icon="mdi-store"
|
<v-list-item class="text-dark" v-bind="props" prepend-icon="mdi-store"
|
||||||
:title="$t('drawer.storeroom_title')"></v-list-item>
|
:title="$t('drawer.storeroom_title')"></v-list-item>
|
||||||
|
@ -587,6 +588,13 @@ export default {
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</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 v-if="permissions.store" to="/acc/storeroom/commodity/check/exist">
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
{{ $t('drawer.commodity_exist_count') }}
|
{{ $t('drawer.commodity_exist_count') }}
|
||||||
|
@ -807,13 +815,13 @@ export default {
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
<v-list-subheader color="primary">{{ $t('drawer.services') }}</v-list-subheader>
|
<v-list-subheader color="primary">{{ $t('drawer.services') }}</v-list-subheader>
|
||||||
<v-list-group v-show="isPluginActive('import-workflow') && permissions.importWorkflow">
|
<v-list-group v-show="isPluginActive('import-workflow') && permissions.plugImportWorkflow">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-list-item class="text-dark" v-bind="props" title="مدیریت واردات کالا">
|
<v-list-item class="text-dark" v-bind="props" title="مدیریت واردات کالا">
|
||||||
<template v-slot:prepend><v-icon icon="mdi-import" color="primary"></v-icon></template>
|
<template v-slot:prepend><v-icon icon="mdi-import" color="primary"></v-icon></template>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item v-if="permissions.importWorkflow" to="/acc/plugins/import-workflow/list">
|
<v-list-item v-if="permissions.plugImportWorkflow" to="/acc/plugins/import-workflow/list">
|
||||||
<v-list-item-title>
|
<v-list-item-title>
|
||||||
لیست پروندههای واردات
|
لیست پروندههای واردات
|
||||||
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/import-workflow/list') }}</span>
|
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/import-workflow/list') }}</span>
|
||||||
|
|
|
@ -11,8 +11,8 @@
|
||||||
</p>
|
</p>
|
||||||
<div class="plugin-version">
|
<div class="plugin-version">
|
||||||
<span class="version-badge">نسخه 1.0.0</span>
|
<span class="version-badge">نسخه 1.0.0</span>
|
||||||
<span v-if="isPluginActive('importWorkflow')" class="status-badge active">فعال</span>
|
<span v-if="isPluginActive('import-workflow')" class="status-badge active">فعال</span>
|
||||||
<RouterLink to="/acc/plugin-center/view-end/import-workflow" v-if="!isPluginActive('importWorkflow')">
|
<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">
|
<span class="status-badge active text-white d-flex align-items-center">
|
||||||
<i class="fa fa-shopping-cart me-1"></i>
|
<i class="fa fa-shopping-cart me-1"></i>
|
||||||
خرید
|
خرید
|
||||||
|
@ -123,7 +123,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="intro-footer">
|
<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">
|
<router-link to="/acc/plugins/import-workflow/list" class="btn btn-success">
|
||||||
<i class="fas fa-list"></i>
|
<i class="fas fa-list"></i>
|
||||||
ورود به مدیریت واردات
|
ورود به مدیریت واردات
|
||||||
|
|
|
@ -11,10 +11,14 @@
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-btn color="info" variant="outlined" prepend-icon="mdi-share-variant" @click="openActivationLinkDialog"
|
<v-btn color="info" variant="outlined" prepend-icon="mdi-share-variant" @click="openActivationLinkDialog"
|
||||||
class="ml-2">
|
class="ml-2">
|
||||||
|
<div class="button-title">
|
||||||
اشتراک گذاری لینک فعالسازی
|
اشتراک گذاری لینک فعالسازی
|
||||||
|
</div>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
<v-btn color="primary" variant="outlined" prepend-icon="mdi-cog" @click="goToWarrantySettings">
|
<v-btn color="primary" variant="outlined" prepend-icon="mdi-cog" @click="goToWarrantySettings">
|
||||||
|
<div class="button-title">
|
||||||
تنظیمات گارانتی
|
تنظیمات گارانتی
|
||||||
|
</div>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
<div class="warranty-plugin">
|
<div class="warranty-plugin">
|
||||||
|
@ -154,55 +158,76 @@
|
||||||
@close="closeBulkImportDialog" />
|
@close="closeBulkImportDialog" />
|
||||||
|
|
||||||
<!-- Activation Link Dialog -->
|
<!-- 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>
|
||||||
<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>
|
<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-title>
|
||||||
<v-card-text>
|
<v-card-text class="pa-4 pa-sm-6">
|
||||||
<v-alert type="info" variant="tonal" class="mb-4">
|
<v-alert type="info" variant="tonal" class="mb-4">
|
||||||
<strong>نکته:</strong> این لینک برای فعالسازی گارانتی توسط مشتریان استفاده میشود.
|
<strong>نکته:</strong> این لینک برای فعالسازی گارانتی توسط مشتریان استفاده میشود.
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<label class="text-body-2 font-weight-medium mb-2 d-block">لینک فعالسازی گارانتی:</label>
|
<label class="text-body-2 font-weight-medium mb-2 d-block">لینک فعالسازی گارانتی:</label>
|
||||||
<div class="d-flex align-center flex-row-reverse gap-2">
|
<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"
|
<v-text-field
|
||||||
class="flex-grow-1 text-left" hide-details></v-text-field>
|
:model-value="activationLink"
|
||||||
<v-btn color="primary" variant="tonal" @click="copyActivationLink" class="ml-2" :loading="copying">
|
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>
|
<v-icon>mdi-content-copy</v-icon>
|
||||||
|
<span v-if="$vuetify.display.xs" class="mr-2">کپی لینک</span>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h4 class="text-h6 mb-3">راهنمای استفاده:</h4>
|
<h4 class="text-h6 mb-3">راهنمای استفاده:</h4>
|
||||||
<v-list density="compact">
|
<v-list density="compact" class="bg-transparent">
|
||||||
<v-list-item>
|
<v-list-item class="px-0">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon color="primary" size="small">mdi-numeric-1-circle</v-icon>
|
<v-icon color="primary" size="small">mdi-numeric-1-circle</v-icon>
|
||||||
</template>
|
</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>
|
<v-list-item class="px-0">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon color="primary" size="small">mdi-numeric-2-circle</v-icon>
|
<v-icon color="primary" size="small">mdi-numeric-2-circle</v-icon>
|
||||||
</template>
|
</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>
|
<v-list-item class="px-0">
|
||||||
<template #prepend>
|
<template #prepend>
|
||||||
<v-icon color="primary" size="small">mdi-numeric-3-circle</v-icon>
|
<v-icon color="primary" size="small">mdi-numeric-3-circle</v-icon>
|
||||||
</template>
|
</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>
|
</v-list>
|
||||||
</div>
|
</div>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
<v-card-actions>
|
<v-card-actions class="pa-4 pa-sm-6">
|
||||||
<v-spacer></v-spacer>
|
<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-actions>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
@ -842,3 +867,23 @@ onMounted(async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</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-card-text>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="content.requireWarrantyOnDelivery"
|
v-model="content.requireWarrantyOnDelivery"
|
||||||
label="الزام ثبت گارانتی هنگام صدور حواله خروج"
|
label="الزام ثبت گارانتی هنگام تکمیل پروسه حواله خروج"
|
||||||
color="primary"
|
color="primary"
|
||||||
hide-details
|
hide-details
|
||||||
class="mb-2"
|
class="mb-2"
|
||||||
|
|
|
@ -372,6 +372,18 @@
|
||||||
:disabled="loadingSwitches.store"
|
:disabled="loadingSwitches.store"
|
||||||
></v-switch>
|
></v-switch>
|
||||||
</v-list-item>
|
</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-list-item>
|
||||||
<v-switch
|
<v-switch
|
||||||
v-model="info.wallet"
|
v-model="info.wallet"
|
||||||
|
@ -401,53 +413,6 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-row v-if="isPluginActive('accpro')" class="mt-4">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card-title class="text-h6 font-weight-bold mb-4">بسته حسابداری پیشرفته</v-card-title>
|
<v-card-title class="text-h6 font-weight-bold mb-4">بسته حسابداری پیشرفته</v-card-title>
|
||||||
|
@ -710,6 +675,58 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</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-card>
|
||||||
</v-container>
|
</v-container>
|
||||||
<v-snackbar
|
<v-snackbar
|
||||||
|
@ -792,7 +809,9 @@ export default {
|
||||||
plugTaxSettings: false,
|
plugTaxSettings: false,
|
||||||
inquiry: false,
|
inquiry: false,
|
||||||
ai: false,
|
ai: false,
|
||||||
warehouseManager: false
|
storehelper: false,
|
||||||
|
plugWarranty: false,
|
||||||
|
plugImportWorkflow: false
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post('/api/business/get/user/permissions',
|
axios.post('/api/business/get/user/permissions',
|
||||||
|
|
838
webUI/src/views/acc/storeroom/io/complete.vue
Normal file
838
webUI/src/views/acc/storeroom/io/complete.vue
Normal file
|
@ -0,0 +1,838 @@
|
||||||
|
<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-if="isPluginActive('warranty')">
|
||||||
|
<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 v-if="isPluginActive('warranty')" 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 plugins = ref<any>({})
|
||||||
|
|
||||||
|
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, pluginsResponse] = 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: [] } })),
|
||||||
|
axios.post('/api/plugin/get/actives')
|
||||||
|
])
|
||||||
|
|
||||||
|
plugins.value = pluginsResponse.data
|
||||||
|
|
||||||
|
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 (isPluginActive('warranty') && 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()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isPluginActive = (plugName: string) => {
|
||||||
|
return plugins.value[plugName] !== undefined
|
||||||
|
}
|
||||||
|
</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" />
|
<v-text-field v-model="items[index].referral" variant="outlined" density="compact" />
|
||||||
</template>
|
</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">
|
<td :colspan="headers.length" class="py-4 px-3">
|
||||||
<div class="lines-wrap">
|
<div class="lines-wrap">
|
||||||
<div class="line-row header">
|
<div class="line-row header">
|
||||||
|
@ -105,40 +105,31 @@
|
||||||
<div class="col-serial">
|
<div class="col-serial">
|
||||||
<v-text-field v-model.trim="items[index].lines[lidx].serialNumber" variant="outlined"
|
<v-text-field v-model.trim="items[index].lines[lidx].serialNumber" variant="outlined"
|
||||||
density="compact" hide-details="auto" placeholder="شماره سریال را وارد یا اسکن کنید"
|
density="compact" hide-details="auto" placeholder="شماره سریال را وارد یا اسکن کنید"
|
||||||
:rules="serialRules"
|
:rules="serialRules" @blur="onSerialBlur(index, lidx)" />
|
||||||
@blur="onSerialBlur(index, lidx)" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-actions">
|
<div class="col-actions">
|
||||||
<v-btn icon size="small" color="primary" variant="text"
|
<!-- <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-icon>mdi-barcode-scan</v-icon>
|
||||||
</v-btn>
|
</v-btn> -->
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-warranty">
|
<div class="col-warranty">
|
||||||
<v-autocomplete
|
<v-autocomplete v-model="items[index].lines[lidx].warrantySerial"
|
||||||
v-model="items[index].lines[lidx].warrantySerial"
|
:items="filteredWarranties(index, lidx)" item-title="label" item-value="serialNumber"
|
||||||
:items="filteredWarranties(index, lidx)"
|
variant="outlined" density="compact" hide-details="auto" clearable
|
||||||
item-title="label"
|
|
||||||
item-value="serialNumber"
|
|
||||||
variant="outlined"
|
|
||||||
density="compact"
|
|
||||||
hide-details="auto"
|
|
||||||
clearable
|
|
||||||
:loading="items[index].loadingWarranties"
|
:loading="items[index].loadingWarranties"
|
||||||
:no-data-text="items[index].loadingWarranties ? 'در حال بارگذاری...' : (items[index].warrantiesLoaded ? 'موردی یافت نشد' : 'برای مشاهده لیست، کلیک کنید')"
|
:no-data-text="items[index].loadingWarranties ? 'در حال بارگذاری...' : (items[index].warrantiesLoaded ? 'موردی یافت نشد' : 'برای مشاهده لیست، کلیک کنید')"
|
||||||
:filter="(i: any, q: any) => String(i.label || '').toLowerCase().includes(String(q || '').toLowerCase())"
|
:filter="(i: any, q: any) => String(i.label || '').toLowerCase().includes(String(q || '').toLowerCase())"
|
||||||
placeholder="انتخاب/وارد کردن کد گارانتی"
|
placeholder="انتخاب/وارد کردن کد گارانتی" :rules="warrantyRules"
|
||||||
:rules="warrantyRules"
|
|
||||||
@update:menu="val => { if (val) ensureWarrantiesLoaded(index) }"
|
@update:menu="val => { if (val) ensureWarrantiesLoaded(index) }"
|
||||||
@update:search="ensureWarrantiesLoaded(index)"
|
@update:search="ensureWarrantiesLoaded(index)" />
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-actions">
|
<div class="col-actions">
|
||||||
<v-btn icon size="small" color="secondary" variant="text"
|
<v-btn icon size="small" color="secondary" variant="text"
|
||||||
@click="openScanner({ mode: 'warranty', 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-icon>mdi-barcode-scan</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</div>
|
</div>
|
||||||
|
@ -155,39 +146,7 @@
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-container>
|
</v-container>
|
||||||
|
|
||||||
<v-dialog v-model="scanner.open" :max-width="isMobile ? '95vw' : 560" persistent>
|
<BarcodeScanner v-model="scanner.open" @detected="handleBarcodeScan" />
|
||||||
<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>
|
|
||||||
|
|
||||||
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000" location="bottom">
|
<v-snackbar v-model="snackbar.show" :color="snackbar.color" :timeout="3000" location="bottom">
|
||||||
{{ snackbar.message }}
|
{{ snackbar.message }}
|
||||||
|
@ -202,7 +161,7 @@ import { ref, reactive, onMounted, nextTick, computed, watch } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import Hdatepicker from '@/components/forms/Hdatepicker.vue'
|
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 TransferType { id: number; name: string }
|
||||||
interface Person { des: string; mobile: 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 serialRules = computed(() => warrantySettings.value.requireWarrantyOnDelivery ? [rules.required] : [])
|
||||||
const warrantyRules = 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({
|
const scanner = reactive({
|
||||||
open: false,
|
open: false,
|
||||||
ready: false,
|
ready: false,
|
||||||
|
@ -297,122 +252,34 @@ const scanner = reactive({
|
||||||
})
|
})
|
||||||
const isMobile = computed(() => window.innerWidth <= 768)
|
const isMobile = computed(() => window.innerWidth <= 768)
|
||||||
|
|
||||||
const openScanner = async (opt: { mode: 'serial' | 'warranty'; itemIndex: number; lineIndex: number }) => {
|
const handleBarcodeScan = async (text: string) => {
|
||||||
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 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') {
|
if (scanner.mode === 'serial') {
|
||||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].serialNumber = text
|
items.value[scanner.itemIndex].lines[scanner.lineIndex].serialNumber = text
|
||||||
showSnack('سریال ثبت شد', 'success')
|
if (!warrantySettings.value.matchWarrantyToSerial) return
|
||||||
await closeScanner()
|
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 {
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const ok = await validateWarranty(text, items.value[scanner.itemIndex].commodity)
|
const ok = await validateWarranty(text, items.value[scanner.itemIndex].commodity)
|
||||||
if (!ok.valid) { showSnack(ok.message || 'گارانتی معتبر نیست', 'error'); return }
|
if (!ok.valid) { showSnack(ok.message || 'گارانتی معتبر نیست', 'error'); return }
|
||||||
items.value[scanner.itemIndex].lines[scanner.lineIndex].warrantySerial = text
|
items.value[scanner.itemIndex].lines[scanner.lineIndex].warrantySerial = text
|
||||||
showSnack('گارانتی ثبت شد', 'success')
|
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -478,18 +345,8 @@ const submit = async () => {
|
||||||
|
|
||||||
items.value.forEach((row) => {
|
items.value.forEach((row) => {
|
||||||
totalCount += Number(row.ticketCount || 0)
|
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 (totalCount === 0) errors.push('هیچ کالایی برای خروج انتخاب نشده است')
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
|
@ -507,8 +364,7 @@ const submit = async () => {
|
||||||
serialLines: r.lines.slice(0, r.ticketCount).map(l => ({ serial: l.serialNumber, warranty: l.warrantySerial }))
|
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: false }
|
||||||
const payloadTicket = { ...ticket.value, requireWarrantySerial }
|
|
||||||
|
|
||||||
const response = await axios.post('/api/storeroom/ticket/insert', {
|
const response = await axios.post('/api/storeroom/ticket/insert', {
|
||||||
doc: doc.value,
|
doc: doc.value,
|
||||||
|
@ -591,8 +447,10 @@ const loadData = async () => {
|
||||||
ticket.value.transferType = transferTypesResponse.data[0]
|
ticket.value.transferType = transferTypesResponse.data[0]
|
||||||
|
|
||||||
plugins.value = pluginsResponse.data
|
plugins.value = pluginsResponse.data
|
||||||
|
if (isPluginActive('warranty')) {
|
||||||
warrantySettings.value.requireWarrantyOnDelivery = !!(warrantyResp?.data?.requireWarrantyOnDelivery)
|
warrantySettings.value.requireWarrantyOnDelivery = !!(warrantyResp?.data?.requireWarrantyOnDelivery)
|
||||||
warrantySettings.value.matchWarrantyToSerial = !!(warrantyResp?.data?.matchWarrantyToSerial)
|
warrantySettings.value.matchWarrantyToSerial = !!(warrantyResp?.data?.matchWarrantyToSerial)
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
showSnack('خطا در بارگذاری دادهها', 'error')
|
showSnack('خطا در بارگذاری دادهها', 'error')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
|
@ -34,15 +34,15 @@
|
||||||
<v-icon start>mdi-file-export</v-icon>
|
<v-icon start>mdi-file-export</v-icon>
|
||||||
حوالههای خروج
|
حوالههای خروج
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab value="input">
|
<v-tab v-if="permissions.store" value="input">
|
||||||
<v-icon start>mdi-file-import</v-icon>
|
<v-icon start>mdi-file-import</v-icon>
|
||||||
حوالههای ورود
|
حوالههای ورود
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab value="transfer">
|
<v-tab v-if="permissions.store" value="transfer">
|
||||||
<v-icon start>mdi-swap-horizontal</v-icon>
|
<v-icon start>mdi-swap-horizontal</v-icon>
|
||||||
حوالههای انتقال
|
حوالههای انتقال
|
||||||
</v-tab>
|
</v-tab>
|
||||||
<v-tab value="waste">
|
<v-tab v-if="permissions.store" value="waste">
|
||||||
<v-icon start>mdi-delete-empty</v-icon>
|
<v-icon start>mdi-delete-empty</v-icon>
|
||||||
ضایعات
|
ضایعات
|
||||||
</v-tab>
|
</v-tab>
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
density="compact" hide-details class="mb-1"></v-text-field>
|
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-tabs v-if="business.requireTwoStepApproval" v-model="outputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||||
<v-tab value="approved">تایید شده</v-tab>
|
<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-tabs>
|
||||||
|
|
||||||
<v-data-table :headers="visibleHeaders" :items="displayOutputItems" :search="outputSearchValue" :loading="loading"
|
<v-data-table :headers="visibleHeaders" :items="displayOutputItems" :search="outputSearchValue" :loading="loading"
|
||||||
|
@ -75,6 +75,12 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>مشاهده</v-list-item-title>
|
<v-list-item-title>مشاهده</v-list-item-title>
|
||||||
</v-list-item>
|
</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)">
|
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||||
|
@ -87,12 +93,18 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>لغو تایید حواله</v-list-item-title>
|
<v-list-item-title>لغو تایید حواله</v-list-item-title>
|
||||||
</v-list-item>
|
</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>
|
<template v-slot:prepend>
|
||||||
<v-icon color="error">mdi-delete</v-icon>
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>حذف</v-list-item-title>
|
<v-list-item-title>حذف</v-list-item-title>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</td>
|
</td>
|
||||||
|
@ -356,7 +368,11 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
import Swal from 'sweetalert2';
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
interface Ticket {
|
interface Ticket {
|
||||||
code: string;
|
code: string;
|
||||||
|
@ -377,6 +393,7 @@ interface Ticket {
|
||||||
email: string;
|
email: string;
|
||||||
id: number;
|
id: number;
|
||||||
};
|
};
|
||||||
|
completed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Header {
|
interface Header {
|
||||||
|
@ -388,6 +405,12 @@ interface Header {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Permissions {
|
||||||
|
store?: boolean;
|
||||||
|
storehelper?: boolean;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const inputItems = ref<Ticket[]>([]);
|
const inputItems = ref<Ticket[]>([]);
|
||||||
const outputItems = ref<Ticket[]>([]);
|
const outputItems = ref<Ticket[]>([]);
|
||||||
|
@ -398,6 +421,7 @@ const outputSearchValue = ref('');
|
||||||
const transferSearchValue = ref('');
|
const transferSearchValue = ref('');
|
||||||
const wasteSearchValue = ref('');
|
const wasteSearchValue = ref('');
|
||||||
const activeTab = ref('output');
|
const activeTab = ref('output');
|
||||||
|
const permissions = ref<Permissions>({});
|
||||||
const inputSubTab = ref<'approved' | 'pending'>('approved');
|
const inputSubTab = ref<'approved' | 'pending'>('approved');
|
||||||
const outputSubTab = ref<'approved' | 'pending'>('approved');
|
const outputSubTab = ref<'approved' | 'pending'>('approved');
|
||||||
const transferSubTab = 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 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 loadColumnSettings = () => {
|
||||||
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
|
const savedSettings = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
if (savedSettings) {
|
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 () => {
|
const loadData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
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(() => {
|
onMounted(() => {
|
||||||
loadColumnSettings();
|
loadColumnSettings();
|
||||||
loadBusinessInfo();
|
loadBusinessInfo();
|
||||||
loadCurrentUser();
|
loadCurrentUser();
|
||||||
|
loadPermissions();
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
</script>
|
</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 () => {
|
const loadData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
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/storeroom/tickets/info/${router.currentRoute.value.params.id}`),
|
||||||
axios.post(`/api/business/get/info/${localStorage.getItem('activeBid')}`),
|
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
|
item.value.ticket = ticketResponse.data.ticket
|
||||||
|
@ -122,6 +123,8 @@ const loadData = async () => {
|
||||||
// warranty data
|
// warranty data
|
||||||
warrantySerials.value = (warrantyResponse.data && warrantyResponse.data.items) ? warrantyResponse.data.items : []
|
warrantySerials.value = (warrantyResponse.data && warrantyResponse.data.items) ? warrantyResponse.data.items : []
|
||||||
ticketActivationCode.value = warrantyResponse.data ? (warrantyResponse.data.ticketActivationCode || null) : null
|
ticketActivationCode.value = warrantyResponse.data ? (warrantyResponse.data.ticketActivationCode || null) : null
|
||||||
|
|
||||||
|
plugins.value = pluginsResponse.data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error)
|
console.error('Error loading data:', error)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -136,7 +139,7 @@ const printInvoice = async () => {
|
||||||
type: item.value.ticket.type
|
type: item.value.ticket.type
|
||||||
})
|
})
|
||||||
window.open(`${import.meta.env.VITE_API_URL}/front/print/${response.data.id}`, '_blank', 'noreferrer')
|
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) {
|
if (error?.response?.data?.message) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
text: error?.response?.data?.message,
|
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(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
loadAttachments()
|
loadAttachments()
|
||||||
|
@ -502,7 +509,7 @@ onMounted(() => {
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
<!-- Warranty Section -->
|
<!-- 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-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>
|
||||||
گارانتیهای حواله و کد فعالسازی
|
گارانتیهای حواله و کد فعالسازی
|
||||||
|
|
|
@ -16,23 +16,16 @@
|
||||||
<!-- Modern Progress Steps -->
|
<!-- Modern Progress Steps -->
|
||||||
<div class="modern-stepper mb-10">
|
<div class="modern-stepper mb-10">
|
||||||
<div class="stepper-track">
|
<div class="stepper-track">
|
||||||
<div
|
<div class="stepper-progress"
|
||||||
class="stepper-progress"
|
:style="{ width: `${((currentStep - 1) / (steps.length - 1)) * 100}%` }"></div>
|
||||||
:style="{ width: `${((currentStep - 1) / (steps.length - 1)) * 100}%` }"
|
|
||||||
></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="stepper-steps">
|
<div class="stepper-steps">
|
||||||
<div
|
<div v-for="(step, index) in steps" :key="index" class="stepper-step" :class="{
|
||||||
v-for="(step, index) in steps"
|
|
||||||
:key="index"
|
|
||||||
class="stepper-step"
|
|
||||||
:class="{
|
|
||||||
'active': currentStep === index + 1,
|
'active': currentStep === index + 1,
|
||||||
'completed': currentStep > index + 1,
|
'completed': currentStep > index + 1,
|
||||||
'pending': currentStep < index + 1
|
'pending': currentStep < index + 1
|
||||||
}"
|
}">
|
||||||
>
|
|
||||||
<div class="stepper-circle">
|
<div class="stepper-circle">
|
||||||
<div class="stepper-circle-inner">
|
<div class="stepper-circle-inner">
|
||||||
<v-icon v-if="currentStep > index + 1" size="18">mdi-check</v-icon>
|
<v-icon v-if="currentStep > index + 1" size="18">mdi-check</v-icon>
|
||||||
|
@ -63,54 +56,25 @@
|
||||||
<v-form ref="codeForm" v-model="codeFormValid" @submit.prevent="checkWarrantyCode">
|
<v-form ref="codeForm" v-model="codeFormValid" @submit.prevent="checkWarrantyCode">
|
||||||
<div class="input-section mb-6">
|
<div class="input-section mb-6">
|
||||||
|
|
||||||
<v-text-field
|
<v-text-field v-model="warrantyCode" label="کد گارانتی" placeholder="مثال: WR-123456789" outlined
|
||||||
v-model="warrantyCode"
|
:rules="codeRules" :loading="loading" prepend-inner-icon="mdi-shield-outline"
|
||||||
label="کد گارانتی"
|
class="custom-input" hide-details="auto" @keyup.enter="checkWarrantyCode">
|
||||||
placeholder="مثال: WR-123456789"
|
|
||||||
outlined
|
|
||||||
:rules="codeRules"
|
|
||||||
:loading="loading"
|
|
||||||
prepend-inner-icon="mdi-shield-outline"
|
|
||||||
class="custom-input"
|
|
||||||
hide-details="auto"
|
|
||||||
@keyup.enter="checkWarrantyCode"
|
|
||||||
>
|
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<v-btn
|
<v-btn icon small @click="showQrScanner = true" :disabled="loading" class="qr-scan-btn">
|
||||||
icon
|
|
||||||
small
|
|
||||||
@click="scanQrCode"
|
|
||||||
:disabled="loading"
|
|
||||||
class="qr-scan-btn"
|
|
||||||
>
|
|
||||||
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
</v-text-field>
|
</v-text-field>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<v-alert
|
<v-alert v-if="errorMessage" type="error" dismissible class="mb-4 custom-alert" border="start"
|
||||||
v-if="errorMessage"
|
colored-border>
|
||||||
type="error"
|
|
||||||
dismissible
|
|
||||||
class="mb-4 custom-alert"
|
|
||||||
border="left"
|
|
||||||
colored-border
|
|
||||||
>
|
|
||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
|
|
||||||
<div class="action-buttons mb-6">
|
<div class="action-buttons mb-6">
|
||||||
<v-btn
|
<v-btn color="primary" large block :loading="loading" :disabled="!codeFormValid"
|
||||||
color="primary"
|
@click="checkWarrantyCode" class="primary-btn mb-4" elevation="2">
|
||||||
large
|
|
||||||
block
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="!codeFormValid"
|
|
||||||
@click="checkWarrantyCode"
|
|
||||||
class="primary-btn mb-4"
|
|
||||||
elevation="2"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-magnify</v-icon>
|
<v-icon left>mdi-magnify</v-icon>
|
||||||
بررسی کد گارانتی
|
بررسی کد گارانتی
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -203,18 +167,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="info-content">
|
<div class="info-content">
|
||||||
<div class="info-label">کد فعالسازی حواله</div>
|
<div class="info-label">کد فعالسازی حواله</div>
|
||||||
<v-text-field
|
<v-text-field v-model.trim="activationSecret" placeholder="کد 8 کاراکتری فعالسازی"
|
||||||
v-model.trim="activationSecret"
|
:rules="activationSecretRules" maxlength="8" variant="outlined" density="comfortable"
|
||||||
placeholder="کد 8 کاراکتری فعالسازی"
|
hide-details="auto" autocomplete="one-time-code" spellcheck="false" autocapitalize="off" />
|
||||||
:rules="activationSecretRules"
|
|
||||||
maxlength="8"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
autocomplete="one-time-code"
|
|
||||||
spellcheck="false"
|
|
||||||
autocapitalize="off"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -233,14 +188,17 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Warning -->
|
<!-- Status Warning -->
|
||||||
<div v-if="productInfo.activation !== 'deactive' && productInfo.activation !== 'active'" class="status-warning mb-6">
|
<div v-if="productInfo.activation !== 'deactive' && productInfo.activation !== 'active'"
|
||||||
|
class="status-warning mb-6">
|
||||||
<div class="warning-content">
|
<div class="warning-content">
|
||||||
<div class="warning-icon" :class="{ 'error-icon': productInfo.activation === 'expired' }">
|
<div class="warning-icon" :class="{ 'error-icon': productInfo.activation === 'expired' }">
|
||||||
<v-icon size="28" color="error">mdi-alert-circle-outline</v-icon>
|
<v-icon size="28" color="error">mdi-alert-circle-outline</v-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="warning-text">
|
<div class="warning-text">
|
||||||
<div class="warning-title" :class="{ 'error-title': productInfo.activation === 'expired' }">وضعیت گارانتی</div>
|
<div class="warning-title" :class="{ 'error-title': productInfo.activation === 'expired' }">
|
||||||
<div class="warning-subtitle" :class="{ 'error-subtitle': productInfo.activation === 'expired' }">
|
وضعیت گارانتی</div>
|
||||||
|
<div class="warning-subtitle"
|
||||||
|
:class="{ 'error-subtitle': productInfo.activation === 'expired' }">
|
||||||
این گارانتی {{ warrantyStatusText }} است و قابل فعالسازی نمیباشد
|
این گارانتی {{ warrantyStatusText }} است و قابل فعالسازی نمیباشد
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -250,42 +208,25 @@
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<v-row no-gutters class="gap-3">
|
<v-row no-gutters class="gap-3">
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-btn
|
<v-btn outlined large block @click="currentStep = 1" class="back-btn" elevation="0">
|
||||||
outlined
|
|
||||||
large
|
|
||||||
block
|
|
||||||
@click="currentStep = 1"
|
|
||||||
class="back-btn"
|
|
||||||
elevation="0"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-arrow-right</v-icon>
|
<v-icon left>mdi-arrow-right</v-icon>
|
||||||
بازگشت
|
بازگشت
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-btn
|
<v-btn color="success" large block :loading="loading"
|
||||||
color="success"
|
|
||||||
large
|
|
||||||
block
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="productInfo.activation !== 'deactive' || productInfo.status !== 'consumed' || !canActivate"
|
:disabled="productInfo.activation !== 'deactive' || productInfo.status !== 'consumed' || !canActivate"
|
||||||
@click="activateWarranty"
|
@click="activateWarranty"
|
||||||
:class="{ 'success-btn': productInfo.activation === 'deactive' || productInfo.activation === 'active', 'error-btn': productInfo.activation === 'expired' }"
|
:class="{ 'success-btn': productInfo.activation === 'deactive' || productInfo.activation === 'active', 'error-btn': productInfo.activation === 'expired' }"
|
||||||
elevation="2"
|
elevation="2">
|
||||||
>
|
|
||||||
<v-icon left>mdi-shield-check</v-icon>
|
<v-icon left>mdi-shield-check</v-icon>
|
||||||
{{ productInfo.activation === 'deactive' ? 'فعالسازی گارانتی' : productInfo.activation === 'active' ? 'این گارانتی قبلا فعال شده است' : productInfo.activation === 'expired' ? 'این گارانتی منقضی شده است' : '' }}
|
{{ productInfo.activation === 'deactive' ? 'فعالسازی گارانتی' : productInfo.activation ===
|
||||||
|
'active' ? 'این گارانتی قبلا فعال شده است' : productInfo.activation === 'expired' ? 'این گارانتی منقضی شده است' : '' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
<v-alert
|
<v-alert v-if="activationErrors.length" type="error" dismissible class="mt-4 custom-alert"
|
||||||
v-if="activationErrors.length"
|
border="start" colored-border>
|
||||||
type="error"
|
|
||||||
dismissible
|
|
||||||
class="mt-4 custom-alert"
|
|
||||||
border="left"
|
|
||||||
colored-border
|
|
||||||
>
|
|
||||||
{{ activationErrors }}
|
{{ activationErrors }}
|
||||||
</v-alert>
|
</v-alert>
|
||||||
</div>
|
</div>
|
||||||
|
@ -332,7 +273,9 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="success-info-content">
|
<div class="success-info-content">
|
||||||
<div class="success-info-label">تاریخ انقضا</div>
|
<div class="success-info-label">تاریخ انقضا</div>
|
||||||
<div class="success-info-value">{{ productInfo.warrantyEndDate ? formatDate(productInfo.warrantyEndDate) : 'بدون تاریخ انقضا' }}</div>
|
<div class="success-info-value">{{ productInfo.warrantyEndDate ?
|
||||||
|
formatDate(productInfo.warrantyEndDate) :
|
||||||
|
'بدون تاریخ انقضا' }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -354,14 +297,7 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-col> -->
|
</v-col> -->
|
||||||
<v-col>
|
<v-col>
|
||||||
<v-btn
|
<v-btn color="primary" large block @click="resetForm" class="primary-btn" elevation="2">
|
||||||
color="primary"
|
|
||||||
large
|
|
||||||
block
|
|
||||||
@click="resetForm"
|
|
||||||
class="primary-btn"
|
|
||||||
elevation="2"
|
|
||||||
>
|
|
||||||
<v-icon left>mdi-plus</v-icon>
|
<v-icon left>mdi-plus</v-icon>
|
||||||
فعالسازی جدید
|
فعالسازی جدید
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
@ -376,105 +312,7 @@
|
||||||
</v-container>
|
</v-container>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- QR Scanner Dialog -->
|
<BarcodeScanner v-model="showQrScanner" @detected="handleBarcodeScan" />
|
||||||
<v-dialog v-model="showQrScanner" max-width="500" persistent>
|
|
||||||
<v-card class="qr-dialog-card">
|
|
||||||
<v-card-title class="qr-dialog-title">
|
|
||||||
<v-icon left color="primary">mdi-qrcode-scan</v-icon>
|
|
||||||
اسکن کد QR/بارکد
|
|
||||||
</v-card-title>
|
|
||||||
<v-card-text>
|
|
||||||
<div class="text-center">
|
|
||||||
<!-- Loading State -->
|
|
||||||
<div v-show="!cameraReady && !scanError" class="qr-scanner-placeholder">
|
|
||||||
<v-icon size="100" color="primary" class="qr-icon">mdi-qrcode-scan</v-icon>
|
|
||||||
<p class="mt-4 qr-loading-text">{{ cameraStatus }}</p>
|
|
||||||
<div class="qr-loading-spinner mt-4">
|
|
||||||
<v-progress-circular indeterminate color="primary" size="32"></v-progress-circular>
|
|
||||||
</div>
|
|
||||||
<p class="text-caption qr-instruction mt-4">
|
|
||||||
اگر پیام دسترسی به دوربین نمایش داده شد، لطفاً "اجازه دادن" را انتخاب کنید
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error State -->
|
|
||||||
<div v-show="scanError && !cameraReady" class="qr-scanner-error">
|
|
||||||
<v-icon size="100" color="error" class="qr-icon">mdi-camera-off</v-icon>
|
|
||||||
<p class="mt-4 qr-error-text">{{ scanError }}</p>
|
|
||||||
<v-btn
|
|
||||||
color="primary"
|
|
||||||
class="mt-4"
|
|
||||||
@click="scanQrCode"
|
|
||||||
:loading="loading"
|
|
||||||
>
|
|
||||||
تلاش مجدد
|
|
||||||
</v-btn>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Camera Video - Always in DOM -->
|
|
||||||
<div class="qr-camera-container" v-show="cameraReady && !scanError">
|
|
||||||
<video
|
|
||||||
ref="qrVideo"
|
|
||||||
class="qr-video"
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
<!-- <div class="qr-overlay">
|
|
||||||
<div class="qr-scan-area">
|
|
||||||
<div class="qr-corner qr-corner-tl"></div>
|
|
||||||
<div class="qr-corner qr-corner-tr"></div>
|
|
||||||
<div class="qr-corner qr-corner-bl"></div>
|
|
||||||
<div class="qr-corner qr-corner-br"></div>
|
|
||||||
<div class="qr-scan-line"></div>
|
|
||||||
</div>
|
|
||||||
</div> -->
|
|
||||||
<p class="qr-scan-instruction">کد QR یا بارکد را در کادر قرار دهید</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hidden video element to ensure it's always available -->
|
|
||||||
<video
|
|
||||||
v-if="!qrVideo"
|
|
||||||
ref="qrVideo"
|
|
||||||
style="display: none;"
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
></video>
|
|
||||||
|
|
||||||
<!-- Error Message -->
|
|
||||||
<v-alert
|
|
||||||
v-if="scanError"
|
|
||||||
type="error"
|
|
||||||
dense
|
|
||||||
class="mt-4"
|
|
||||||
>
|
|
||||||
{{ scanError }}
|
|
||||||
</v-alert>
|
|
||||||
</div>
|
|
||||||
</v-card-text>
|
|
||||||
<v-card-actions>
|
|
||||||
<v-btn
|
|
||||||
v-if="cameraReady"
|
|
||||||
text
|
|
||||||
color="primary"
|
|
||||||
@click="toggleFlashlight"
|
|
||||||
class="qr-flash-btn"
|
|
||||||
>
|
|
||||||
<v-icon left>{{ flashlightOn ? 'mdi-flashlight-off' : 'mdi-flashlight' }}</v-icon>
|
|
||||||
{{ flashlightOn ? 'خاموش کردن فلش' : 'روشن کردن فلش' }}
|
|
||||||
</v-btn>
|
|
||||||
<v-spacer></v-spacer>
|
|
||||||
<v-btn
|
|
||||||
text
|
|
||||||
@click="stopScanner"
|
|
||||||
class="qr-cancel-btn"
|
|
||||||
>
|
|
||||||
انصراف
|
|
||||||
</v-btn>
|
|
||||||
</v-card-actions>
|
|
||||||
</v-card>
|
|
||||||
</v-dialog>
|
|
||||||
|
|
||||||
<!-- Modern Footer -->
|
<!-- Modern Footer -->
|
||||||
<footer class="modern-footer">
|
<footer class="modern-footer">
|
||||||
|
@ -523,23 +361,13 @@
|
||||||
<div class="developer-info">
|
<div class="developer-info">
|
||||||
<v-icon size="16" color="white" class="mr-2">mdi-code-tags</v-icon>
|
<v-icon size="16" color="white" class="mr-2">mdi-code-tags</v-icon>
|
||||||
<span class="developer-text">توسعهدهنده:</span>
|
<span class="developer-text">توسعهدهنده:</span>
|
||||||
<a
|
<a href="https://pirouz.xyz" target="_blank" class="developer-link" rel="noopener noreferrer">
|
||||||
href="https://pirouz.xyz"
|
|
||||||
target="_blank"
|
|
||||||
class="developer-link"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
محمد رضائی
|
محمد رضائی
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="developer-website" style="display: flex; align-items: center; gap: 5px;">
|
<div class="developer-website" style="display: flex; align-items: center; gap: 5px;">
|
||||||
<v-icon size="14" color="rgba(255,255,255,0.6)" class="mr-1">mdi-web</v-icon>
|
<v-icon size="14" color="rgba(255,255,255,0.6)" class="mr-1">mdi-web</v-icon>
|
||||||
<a
|
<a href="https://pirouz.xyz" target="_blank" class="website-link" rel="noopener noreferrer">
|
||||||
href="https://pirouz.xyz"
|
|
||||||
target="_blank"
|
|
||||||
class="website-link"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
pirouz.xyz
|
pirouz.xyz
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -553,43 +381,27 @@
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import QrScanner from 'qr-scanner'
|
import BarcodeScanner from '@/components/widgets/BarcodeScanner.vue'
|
||||||
|
|
||||||
export default {
|
const route = useRoute()
|
||||||
name: 'WarrantyActivation',
|
|
||||||
setup() {
|
|
||||||
// Route
|
|
||||||
const route = useRoute()
|
|
||||||
|
|
||||||
// Reactive data
|
const currentStep = ref(1)
|
||||||
const currentStep = ref(1)
|
const warrantyCode = ref('')
|
||||||
const warrantyCode = ref('')
|
const loading = ref(false)
|
||||||
const loading = ref(false)
|
const codeFormValid = ref(false)
|
||||||
const codeFormValid = ref(false)
|
const errorMessage = ref('')
|
||||||
const errorMessage = ref('')
|
const activationErrors = ref<string[]>([])
|
||||||
const activationErrors = ref([])
|
const businessId = ref(route.params.businessId as string)
|
||||||
const businessId = ref(route.params.businessId)
|
const showHelpDialog = ref(false)
|
||||||
const showHelpDialog = ref(false)
|
const showQrScanner = ref(false)
|
||||||
const showQrScanner = ref(false)
|
|
||||||
|
|
||||||
// QR Scanner data
|
const productInfo = ref({
|
||||||
const qrVideo = ref(null)
|
|
||||||
const cameraReady = ref(false)
|
|
||||||
const cameraStatus = ref('در حال دسترسی به دوربین...')
|
|
||||||
const scanError = ref('')
|
|
||||||
const flashlightOn = ref(false)
|
|
||||||
|
|
||||||
// QR Scanner instance variables
|
|
||||||
let qrScanner = null
|
|
||||||
let currentStream = null
|
|
||||||
|
|
||||||
// Product information (will be fetched from backend)
|
|
||||||
const productInfo = ref({
|
|
||||||
serialNumber: '',
|
serialNumber: '',
|
||||||
|
commoditySerial: '',
|
||||||
productName: '',
|
productName: '',
|
||||||
productCode: '',
|
productCode: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -603,22 +415,26 @@ export default {
|
||||||
submitter: '',
|
submitter: '',
|
||||||
businessName: '',
|
businessName: '',
|
||||||
activationTicketCode: '',
|
activationTicketCode: '',
|
||||||
requireActivationSecret: false
|
requireActivationSecret: false,
|
||||||
})
|
activation: ''
|
||||||
|
})
|
||||||
|
|
||||||
const activationSecret = ref('')
|
const activationSecret = ref('')
|
||||||
const activationSecretRules = [
|
const activationSecretRules = [
|
||||||
v => !productInfo.value.requireActivationSecret || (!!v && v.length === 8) || 'کد فعالسازی 8 کاراکتری الزامی است'
|
(v: string) =>
|
||||||
]
|
!productInfo.value.requireActivationSecret ||
|
||||||
const canActivate = computed(() => {
|
(!!v && v.length === 8) ||
|
||||||
|
'کد فعالسازی 8 کاراکتری الزامی است'
|
||||||
|
]
|
||||||
|
|
||||||
|
const canActivate = computed(() => {
|
||||||
if (productInfo.value.requireActivationSecret) {
|
if (productInfo.value.requireActivationSecret) {
|
||||||
return activationSecret.value && activationSecret.value.length === 8
|
return activationSecret.value && activationSecret.value.length === 8
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
// Steps data
|
const steps = ref([
|
||||||
const steps = ref([
|
|
||||||
{
|
{
|
||||||
title: 'وارد کردن کد',
|
title: 'وارد کردن کد',
|
||||||
subtitle: 'کد گارانتی را وارد کنید'
|
subtitle: 'کد گارانتی را وارد کنید'
|
||||||
|
@ -631,25 +447,23 @@ export default {
|
||||||
title: 'فعالسازی',
|
title: 'فعالسازی',
|
||||||
subtitle: 'گارانتی فعال شد'
|
subtitle: 'گارانتی فعال شد'
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const handleBarcodeScan = (val: any) => {
|
||||||
|
warrantyCode.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeRules = [
|
||||||
|
(v: string) => !!v || 'کد گارانتی الزامی است',
|
||||||
|
// (v: string) => (v && v.length >= 8) || 'کد گارانتی باید حداقل 8 کاراکتر باشد',
|
||||||
|
// (v: string) => /^[A-Za-z0-9-]+$/.test(v) || 'کد گارانتی فقط شامل حروف، اعداد و خط تیره باشد'
|
||||||
|
]
|
||||||
|
|
||||||
// Form validation rules
|
const timeRemaining = computed(() => {
|
||||||
const codeRules = [
|
|
||||||
v => !!v || 'کد گارانتی الزامی است',
|
|
||||||
// v => (v && v.length >= 8) || 'کد گارانتی باید حداقل 8 کاراکتر باشد',
|
|
||||||
// v => /^[A-Za-z0-9-]+$/.test(v) || 'کد گارانتی فقط شامل حروف، اعداد و خط تیره باشد'
|
|
||||||
]
|
|
||||||
|
|
||||||
// Computed properties
|
|
||||||
const timeRemaining = computed(() => {
|
|
||||||
// Use daysRemaining from backend response
|
|
||||||
return productInfo.value.daysRemaining || 0
|
return productInfo.value.daysRemaining || 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert warranty status to Persian
|
const warrantyStatusText = computed(() => {
|
||||||
const warrantyStatusText = computed(() => {
|
|
||||||
const status = productInfo.value.activation
|
const status = productInfo.value.activation
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active':
|
case 'active':
|
||||||
|
@ -663,10 +477,9 @@ export default {
|
||||||
default:
|
default:
|
||||||
return status || 'نامشخص'
|
return status || 'نامشخص'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Convert date to Jalali
|
const formatDate = (dateString: string) => {
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return 'نامشخص'
|
if (!dateString) return 'نامشخص'
|
||||||
try {
|
try {
|
||||||
const date = new Date(dateString)
|
const date = new Date(dateString)
|
||||||
|
@ -674,29 +487,29 @@ export default {
|
||||||
} catch {
|
} catch {
|
||||||
return dateString
|
return dateString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activationDate = computed(() => {
|
const activationDate = computed(() => {
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
return today.toLocaleDateString('fa-IR')
|
return today.toLocaleDateString('fa-IR')
|
||||||
})
|
})
|
||||||
|
|
||||||
const expirationDate = computed(() => {
|
const expirationDate = computed(() => {
|
||||||
// Mock calculation - will be calculated from backend
|
|
||||||
const today = new Date()
|
const today = new Date()
|
||||||
const expiry = new Date(today.getTime() + (18 * 30 * 24 * 60 * 60 * 1000)) // 18 months
|
const expiry = new Date(today.getTime() + 18 * 30 * 24 * 60 * 60 * 1000) // 18 months
|
||||||
return expiry.toLocaleDateString('fa-IR')
|
return expiry.toLocaleDateString('fa-IR')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Methods
|
const checkWarrantyCode = async () => {
|
||||||
const checkWarrantyCode = async () => {
|
|
||||||
if (!codeFormValid.value) return
|
if (!codeFormValid.value) return
|
||||||
|
|
||||||
loading.value = true
|
loading.value = true
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/public/${businessId.value}/warranty/check/${warrantyCode.value}`)
|
const response = await axios.get(
|
||||||
|
`/api/public/${businessId.value}/warranty/check/${warrantyCode.value}`
|
||||||
|
)
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
productInfo.value = response.data.data
|
productInfo.value = response.data.data
|
||||||
|
@ -704,9 +517,8 @@ export default {
|
||||||
} else {
|
} else {
|
||||||
throw new Error(response.data.message || 'خطا در بررسی کد گارانتی')
|
throw new Error(response.data.message || 'خطا در بررسی کد گارانتی')
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
} catch (error) {
|
if (error.response?.data?.message) {
|
||||||
if (error.response && error.response.data && error.response.data.message) {
|
|
||||||
errorMessage.value = error.response.data.message
|
errorMessage.value = error.response.data.message
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'خطا در بررسی کد گارانتی'
|
errorMessage.value = 'خطا در بررسی کد گارانتی'
|
||||||
|
@ -714,117 +526,44 @@ export default {
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const activateWarranty = async () => {
|
const activateWarranty = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await axios.post(`/api/public/${businessId.value}/warranty/activate/${warrantyCode.value}`, {
|
const response = await axios.post(
|
||||||
activationSecret: activationSecret.value
|
`/api/public/${businessId.value}/warranty/activate/${warrantyCode.value}`,
|
||||||
})
|
{ activationSecret: activationSecret.value }
|
||||||
|
)
|
||||||
|
|
||||||
if (response.data.success) {
|
if (response.data.success) {
|
||||||
currentStep.value = 3
|
currentStep.value = 3
|
||||||
} else {
|
} else {
|
||||||
const msgs = Array.response.data?.message ? response.data.message : ''
|
const msgs = Array.isArray(response.data?.message)
|
||||||
activationErrors.value = msgs ? msgs : response.data.message || 'خطا در فعالسازی گارانتی'
|
? response.data.message
|
||||||
|
: []
|
||||||
|
activationErrors.value =
|
||||||
|
msgs.length > 0 ? msgs : response.data.message || 'خطا در فعالسازی گارانتی'
|
||||||
throw new Error(response.data.message || 'خطا در فعالسازی گارانتی')
|
throw new Error(response.data.message || 'خطا در فعالسازی گارانتی')
|
||||||
}
|
}
|
||||||
|
} catch (error: any) {
|
||||||
} catch (error) {
|
|
||||||
const data = error?.response?.data
|
const data = error?.response?.data
|
||||||
const msgs = Array.data?.message ? data.message : ''
|
const msgs = Array.isArray(data?.message) ? data.message : []
|
||||||
activationErrors.value = msgs ? msgs : data?.message || 'خطا در فعالسازی گارانتی'
|
activationErrors.value =
|
||||||
|
msgs.length > 0 ? msgs : data?.message || 'خطا در فعالسازی گارانتی'
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const scanQrCode = async () => {
|
|
||||||
showQrScanner.value = true
|
|
||||||
cameraReady.value = false
|
|
||||||
cameraStatus.value = 'در حال دسترسی به دوربین...'
|
|
||||||
scanError.value = ''
|
|
||||||
|
|
||||||
|
const downloadCertificate = async () => {
|
||||||
try {
|
try {
|
||||||
// First check if we have camera permissions
|
const response = await axios.get(
|
||||||
try {
|
`/api/public/${businessId.value}/warranty/certificate/${warrantyCode.value}`
|
||||||
await navigator.mediaDevices.getUserMedia({ video: true })
|
|
||||||
} catch (permissionError) {
|
|
||||||
throw new Error('لطفاً دسترسی به دوربین را مجاز کنید')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if QrScanner is supported
|
|
||||||
const hasCamera = await QrScanner.hasCamera()
|
|
||||||
if (!hasCamera) {
|
|
||||||
throw new Error('دوربین در دسترس نیست یا پشتیبانی نمیشود')
|
|
||||||
}
|
|
||||||
|
|
||||||
cameraStatus.value = 'در حال آمادهسازی دوربین...'
|
|
||||||
|
|
||||||
// Wait for DOM to update and video element to be available with retry logic
|
|
||||||
let retries = 0
|
|
||||||
const maxRetries = 10
|
|
||||||
|
|
||||||
while (!qrVideo.value && retries < maxRetries) {
|
|
||||||
await nextTick()
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
retries++
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!qrVideo.value) {
|
|
||||||
throw new Error('عنصر ویدئو در دسترس نیست - لطفاً صفحه را رفرش کنید')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize QR Scanner with better error handling
|
|
||||||
qrScanner = new QrScanner(
|
|
||||||
qrVideo.value,
|
|
||||||
result => {
|
|
||||||
warrantyCode.value = result.data
|
|
||||||
stopScanner()
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onDecodeError: (error) => {
|
|
||||||
// Silent error - normal when no QR code is visible
|
|
||||||
},
|
|
||||||
highlightScanRegion: true,
|
|
||||||
highlightCodeOutline: true,
|
|
||||||
maxScansPerSecond: 5,
|
|
||||||
preferredCamera: 'environment', // Use back camera if available
|
|
||||||
returnDetailedScanResult: true
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start the scanner
|
|
||||||
await qrScanner.start()
|
|
||||||
cameraReady.value = true
|
|
||||||
cameraStatus.value = 'دوربین آماده است - کد QR یا بارکد را در کادر قرار دهید'
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
let errorMsg = 'خطا در راهاندازی دوربین'
|
|
||||||
|
|
||||||
if (error.name === 'NotAllowedError') {
|
|
||||||
errorMsg = 'لطفاً دسترسی به دوربین را مجاز کنید'
|
|
||||||
} else if (error.name === 'NotFoundError') {
|
|
||||||
errorMsg = 'دوربین پیدا نشد'
|
|
||||||
} else if (error.name === 'NotSupportedError') {
|
|
||||||
errorMsg = 'دوربین پشتیبانی نمیشود'
|
|
||||||
} else if (error.message) {
|
|
||||||
errorMsg = error.message
|
|
||||||
}
|
|
||||||
|
|
||||||
scanError.value = errorMsg
|
|
||||||
cameraReady.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadCertificate = async () => {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(`/api/public/${businessId.value}/warranty/certificate/${warrantyCode.value}`)
|
|
||||||
|
|
||||||
if (response.data.success && response.data.downloadUrl) {
|
if (response.data.success && response.data.downloadUrl) {
|
||||||
// Open download link in new tab
|
|
||||||
window.open(response.data.downloadUrl, '_blank')
|
window.open(response.data.downloadUrl, '_blank')
|
||||||
} else {
|
} else {
|
||||||
alert('خطا در دانلود گواهی گارانتی')
|
alert('خطا در دانلود گواهی گارانتی')
|
||||||
|
@ -832,61 +571,15 @@ export default {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('خطا در دانلود گواهی گارانتی')
|
alert('خطا در دانلود گواهی گارانتی')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopScanner = () => {
|
const resetForm = () => {
|
||||||
try {
|
|
||||||
if (qrScanner) {
|
|
||||||
qrScanner.stop()
|
|
||||||
qrScanner.destroy()
|
|
||||||
qrScanner = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop all video tracks
|
|
||||||
const video = qrVideo.value
|
|
||||||
if (video && video.srcObject) {
|
|
||||||
const stream = video.srcObject
|
|
||||||
stream.getTracks().forEach(track => track.stop())
|
|
||||||
video.srcObject = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStream) {
|
|
||||||
currentStream.getTracks().forEach(track => track.stop())
|
|
||||||
currentStream = null
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
} finally {
|
|
||||||
showQrScanner.value = false
|
|
||||||
cameraReady.value = false
|
|
||||||
flashlightOn.value = false
|
|
||||||
scanError.value = ''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleFlashlight = async () => {
|
|
||||||
if (qrScanner && cameraReady.value) {
|
|
||||||
try {
|
|
||||||
if (flashlightOn.value) {
|
|
||||||
await qrScanner.turnFlashlightOff()
|
|
||||||
flashlightOn.value = false
|
|
||||||
} else {
|
|
||||||
await qrScanner.turnFlashlightOn()
|
|
||||||
flashlightOn.value = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
scanError.value = 'خطا در کنترل فلش دوربین یا دستگاه از فلش پشتیبانی نمیکند'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetForm = () => {
|
|
||||||
stopScanner() // Stop scanner if running
|
|
||||||
currentStep.value = 1
|
currentStep.value = 1
|
||||||
warrantyCode.value = ''
|
warrantyCode.value = ''
|
||||||
errorMessage.value = ''
|
errorMessage.value = ''
|
||||||
productInfo.value = {
|
productInfo.value = {
|
||||||
serialNumber: '',
|
serialNumber: '',
|
||||||
|
commoditySerial: '',
|
||||||
productName: '',
|
productName: '',
|
||||||
productCode: '',
|
productCode: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
@ -898,57 +591,10 @@ export default {
|
||||||
activationTimeLimit: 0,
|
activationTimeLimit: 0,
|
||||||
daysRemaining: 0,
|
daysRemaining: 0,
|
||||||
submitter: '',
|
submitter: '',
|
||||||
businessName: ''
|
businessName: '',
|
||||||
}
|
activationTicketCode: '',
|
||||||
}
|
requireActivationSecret: false,
|
||||||
|
activation: ''
|
||||||
// Cleanup on component unmount
|
|
||||||
onUnmounted(() => {
|
|
||||||
stopScanner()
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
// Reactive data
|
|
||||||
currentStep,
|
|
||||||
warrantyCode,
|
|
||||||
loading,
|
|
||||||
codeFormValid,
|
|
||||||
errorMessage,
|
|
||||||
showHelpDialog,
|
|
||||||
showQrScanner,
|
|
||||||
productInfo,
|
|
||||||
steps,
|
|
||||||
|
|
||||||
// QR Scanner data
|
|
||||||
qrVideo,
|
|
||||||
cameraReady,
|
|
||||||
cameraStatus,
|
|
||||||
scanError,
|
|
||||||
flashlightOn,
|
|
||||||
|
|
||||||
// Form rules
|
|
||||||
codeRules,
|
|
||||||
|
|
||||||
// Computed
|
|
||||||
timeRemaining,
|
|
||||||
warrantyStatusText,
|
|
||||||
formatDate,
|
|
||||||
activationDate,
|
|
||||||
expirationDate,
|
|
||||||
|
|
||||||
// Methods
|
|
||||||
checkWarrantyCode,
|
|
||||||
activateWarranty,
|
|
||||||
scanQrCode,
|
|
||||||
stopScanner,
|
|
||||||
toggleFlashlight,
|
|
||||||
downloadCertificate,
|
|
||||||
resetForm,
|
|
||||||
activationErrors,
|
|
||||||
activationSecret,
|
|
||||||
activationSecretRules,
|
|
||||||
canActivate,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1017,8 +663,15 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float {
|
@keyframes float {
|
||||||
0%, 100% { transform: translateY(0px); }
|
|
||||||
50% { transform: translateY(-20px); }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateY(0px);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(-20px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-content {
|
.hero-content {
|
||||||
|
@ -1047,9 +700,20 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4); }
|
0% {
|
||||||
70% { transform: scale(1.05); box-shadow: 0 0 0 10px rgba(255, 255, 255, 0); }
|
transform: scale(1);
|
||||||
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); }
|
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hero-title {
|
.hero-title {
|
||||||
|
@ -1199,9 +863,11 @@ export default {
|
||||||
0% {
|
0% {
|
||||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
70% {
|
70% {
|
||||||
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
box-shadow: 0 0 0 10px rgba(59, 130, 246, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
box-shadow: 0 0 0 0 rgba(59, 130, 246, 0);
|
||||||
}
|
}
|
||||||
|
@ -1248,6 +914,7 @@ export default {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
|
@ -1532,11 +1199,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ===== ANIMATIONS ===== */
|
/* ===== ANIMATIONS ===== */
|
||||||
.v-enter-active, .v-leave-active {
|
.v-enter-active,
|
||||||
|
.v-leave-active {
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-enter-from, .v-leave-to {
|
.v-enter-from,
|
||||||
|
.v-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
}
|
}
|
||||||
|
@ -1599,8 +1268,17 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes qr-pulse {
|
@keyframes qr-pulse {
|
||||||
0%, 100% { transform: scale(1); opacity: 0.8; }
|
|
||||||
50% { transform: scale(1.05); opacity: 1; }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.qr-loading-text {
|
.qr-loading-text {
|
||||||
|
@ -1731,10 +1409,12 @@ export default {
|
||||||
top: 0;
|
top: 0;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
top: 50%;
|
top: 50%;
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
top: 100%;
|
top: 100%;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@ -2100,13 +1780,27 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes success-pulse {
|
@keyframes success-pulse {
|
||||||
0%, 100% { transform: scale(1); }
|
|
||||||
50% { transform: scale(1.05); }
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes success-ring {
|
@keyframes success-ring {
|
||||||
0% { transform: scale(1); opacity: 1; }
|
0% {
|
||||||
100% { transform: scale(1.2); opacity: 0; }
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: scale(1.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-title {
|
.success-title {
|
||||||
|
|
Loading…
Reference in a new issue