update Warranty & ImportWorkflow plugins

This commit is contained in:
Gloomy 2025-08-20 20:07:02 +00:00
parent 2dde89e03c
commit 91d2558893
21 changed files with 783 additions and 919 deletions

View 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 Version20250820090839 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 business DROP invoice_approver, DROP warehouse_approver, DROP financial_approver
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow DROP total_amount, DROP total_amount_irr
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business ADD invoice_approver VARCHAR(255) DEFAULT NULL, ADD warehouse_approver VARCHAR(255) DEFAULT NULL, ADD financial_approver VARCHAR(255) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow ADD total_amount VARCHAR(255) DEFAULT NULL, ADD total_amount_irr VARCHAR(255) DEFAULT NULL
SQL);
}
}

View 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 Version20250820104158 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 business DROP invoice_approver, DROP warehouse_approver, DROP financial_approver
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT 0, CHANGE is_approved is_approved TINYINT(1) DEFAULT 1
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow DROP total_amount, DROP total_amount_irr
SQL);
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql(<<<'SQL'
ALTER TABLE business ADD invoice_approver VARCHAR(255) DEFAULT NULL, ADD warehouse_approver VARCHAR(255) DEFAULT NULL, ADD financial_approver VARCHAR(255) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE hesabdari_doc CHANGE is_preview is_preview TINYINT(1) DEFAULT NULL, CHANGE is_approved is_approved TINYINT(1) DEFAULT NULL
SQL);
$this->addSql(<<<'SQL'
ALTER TABLE import_workflow ADD total_amount VARCHAR(255) DEFAULT NULL, ADD total_amount_irr VARCHAR(255) DEFAULT NULL
SQL);
}
}

View file

@ -0,0 +1,63 @@
<?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 Version20250820174027 extends AbstractMigration
{
public function getDescription(): string
{
return 'Change monetary fields from VARCHAR to DECIMAL to support decimal currency amounts';
}
public function up(Schema $schema): void
{
// ImportWorkflow table - exchange rate
$this->addSql('ALTER TABLE import_workflow MODIFY exchange_rate DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowItem table - monetary fields
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price_irr DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price_irr DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowPayment table - monetary fields
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount_irr DECIMAL(15,2) DEFAULT NULL');
// ImportWorkflowCustoms table - monetary fields
$this->addSql('ALTER TABLE import_workflow_customs MODIFY customs_duty DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY value_added_tax DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY other_charges DECIMAL(15,2) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY total_customs_charges DECIMAL(15,2) DEFAULT NULL');
}
public function down(Schema $schema): void
{
// ImportWorkflow table - exchange rate
$this->addSql('ALTER TABLE import_workflow MODIFY exchange_rate VARCHAR(255) DEFAULT NULL');
// ImportWorkflowItem table - monetary fields
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY unit_price_irr VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_item MODIFY total_price_irr VARCHAR(255) DEFAULT NULL');
// ImportWorkflowPayment table - monetary fields
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_payment MODIFY amount_irr VARCHAR(255) DEFAULT NULL');
// ImportWorkflowCustoms table - monetary fields
$this->addSql('ALTER TABLE import_workflow_customs MODIFY customs_duty VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY value_added_tax VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY other_charges VARCHAR(255) DEFAULT NULL');
$this->addSql('ALTER TABLE import_workflow_customs MODIFY total_customs_charges VARCHAR(255) DEFAULT NULL');
}
}

View file

@ -360,6 +360,9 @@ class AdminController extends AbstractController
'passChequeInput' => $registryMGR->get('sms', 'plugAccproPassChequeInput'), 'passChequeInput' => $registryMGR->get('sms', 'plugAccproPassChequeInput'),
'rejectChequeInput' => $registryMGR->get('sms', 'plugAccproRejectChequeInput') 'rejectChequeInput' => $registryMGR->get('sms', 'plugAccproRejectChequeInput')
]; ];
$resp['plugWarranty'] = [
'sendSerial' => $registryMGR->get('sms', 'plugWarrantySendSerial'),
];
return $this->json($resp); return $this->json($resp);
} }
@ -431,7 +434,10 @@ class AdminController extends AbstractController
if (array_key_exists('rejectChequeInput', $params['plugAccpro'])) if (array_key_exists('rejectChequeInput', $params['plugAccpro']))
$registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput'] ?? ''); $registryMGR->update('sms', 'plugAccproRejectChequeInput', $params['plugAccpro']['rejectChequeInput'] ?? '');
} }
if (array_key_exists('plugWarranty', $params)) {
if (array_key_exists('sendSerial', $params['plugWarranty']))
$registryMGR->update('sms', 'plugWarrantySendSerial', $params['plugWarranty']['sendSerial'] ?? '');
}
return $this->json(JsonResp::success()); return $this->json(JsonResp::success());
} }

View file

@ -653,22 +653,23 @@ class ApprovalController extends AbstractController
} }
$approversMap = [ $approversMap = [
'sell' => 'getApproverSellInvoice', 'getApproverSellInvoice' => ['sell'],
'buy' => 'getApproverBuyInvoice', 'getApproverBuyInvoice' => ['buy'],
'storeroom' => 'getApproverWarehouseTransfer', 'getApproverWarehouseTransfer' => ['storeroom'],
'rfsell' => 'getApproverReturnSell', 'getApproverReturnSell' => ['rfsell'],
'rfbuy' => 'getApproverReturnBuy', 'getApproverReturnBuy' => ['rfbuy'],
'sell_receive' => 'getApproverReceiveFromPersons', 'getApproverReceiveFromPersons' => ['person_receive'],
'buy_send' => 'getApproverPayToPersons', 'getApproverPayToPersons' => ['person_send'],
'hesabdari' => 'getApproverAccountingDocs', 'getApproverAccountingDocs' => ['calc'],
'transfer' => 'getApproverBankTransfers', 'getApproverBankTransfers' => ['transfer'],
]; ];
if (!isset($approversMap[$documentType])) { foreach ($approversMap as $method => $types) {
return false; if (in_array($documentType, $types, true)) {
return $business->$method() === $user->getEmail();
}
} }
$method = $approversMap[$documentType]; return false;
return $business->$method() === $user->getEmail();
} }
} }

View file

@ -64,7 +64,7 @@ class PlugImportWorkflowController extends AbstractController
'status' => $workflow->getStatus(), 'status' => $workflow->getStatus(),
'dateSubmit' => $workflow->getDateSubmit(), 'dateSubmit' => $workflow->getDateSubmit(),
'supplierName' => $workflow->getSupplierName(), 'supplierName' => $workflow->getSupplierName(),
'totalAmount' => $workflow->getTotalAmount(), 'totalAmount' => $workflow->getComputedTotalAmount(),
'currency' => $workflow->getCurrency(), 'currency' => $workflow->getCurrency(),
'submitter' => $workflow->getSubmitter()->getFullName() 'submitter' => $workflow->getSubmitter()->getFullName()
]; ];
@ -137,10 +137,8 @@ class PlugImportWorkflowController extends AbstractController
$workflow->setSupplierAddress($data['supplierAddress'] ?? ''); $workflow->setSupplierAddress($data['supplierAddress'] ?? '');
$workflow->setSupplierPhone($data['supplierPhone'] ?? ''); $workflow->setSupplierPhone($data['supplierPhone'] ?? '');
$workflow->setSupplierEmail($data['supplierEmail'] ?? ''); $workflow->setSupplierEmail($data['supplierEmail'] ?? '');
$workflow->setTotalAmount($data['totalAmount'] ?? '');
$workflow->setCurrency($data['currency'] ?? ''); $workflow->setCurrency($data['currency'] ?? '');
$workflow->setExchangeRate($data['exchangeRate'] ?? ''); $workflow->setExchangeRate(isset($data['exchangeRate']) && $data['exchangeRate'] !== '' ? $data['exchangeRate'] : null);
$workflow->setTotalAmountIRR($data['totalAmountIRR'] ?? '');
$workflow->setStatus('draft'); $workflow->setStatus('draft');
$entityManager->persist($workflow); $entityManager->persist($workflow);
@ -242,10 +240,8 @@ class PlugImportWorkflowController extends AbstractController
'supplierAddress' => $workflow->getSupplierAddress(), 'supplierAddress' => $workflow->getSupplierAddress(),
'supplierPhone' => $workflow->getSupplierPhone(), 'supplierPhone' => $workflow->getSupplierPhone(),
'supplierEmail' => $workflow->getSupplierEmail(), 'supplierEmail' => $workflow->getSupplierEmail(),
'totalAmount' => $workflow->getTotalAmount(),
'currency' => $workflow->getCurrency(), 'currency' => $workflow->getCurrency(),
'exchangeRate' => $workflow->getExchangeRate(), 'exchangeRate' => $workflow->getExchangeRate(),
'totalAmountIRR' => $workflow->getTotalAmountIRR(),
'submitter' => $workflow->getSubmitter()->getFullName(), 'submitter' => $workflow->getSubmitter()->getFullName(),
'items' => [], 'items' => [],
'payments' => [], 'payments' => [],
@ -632,10 +628,10 @@ class PlugImportWorkflowController extends AbstractController
$c->setDeclarationNumber($data['declarationNumber'] ?? ''); $c->setDeclarationNumber($data['declarationNumber'] ?? '');
$c->setCustomsCode($data['customsCode'] ?? null); $c->setCustomsCode($data['customsCode'] ?? null);
$c->setClearanceDate(isset($data['clearanceDate']) ? ($this->jalaliToGregorian($data['clearanceDate']) ?? null) : null); $c->setClearanceDate(isset($data['clearanceDate']) ? ($this->jalaliToGregorian($data['clearanceDate']) ?? null) : null);
$c->setCustomsDuty(isset($data['customsDuty']) ? (string)$data['customsDuty'] : null); $c->setCustomsDuty(isset($data['customsDuty']) && $data['customsDuty'] !== '' ? (string)$data['customsDuty'] : null);
$c->setValueAddedTax(isset($data['valueAddedTax']) ? (string)$data['valueAddedTax'] : null); $c->setValueAddedTax(isset($data['valueAddedTax']) && $data['valueAddedTax'] !== '' ? (string)$data['valueAddedTax'] : null);
$c->setOtherCharges(isset($data['otherCharges']) ? (string)$data['otherCharges'] : null); $c->setOtherCharges(isset($data['otherCharges']) && $data['otherCharges'] !== '' ? (string)$data['otherCharges'] : null);
$c->setTotalCustomsCharges(isset($data['totalCustomsCharges']) ? (string)$data['totalCustomsCharges'] : null); $c->setTotalCustomsCharges(isset($data['totalCustomsCharges']) && $data['totalCustomsCharges'] !== '' ? (string)$data['totalCustomsCharges'] : null);
$c->setCustomsBroker($data['customsBroker'] ?? null); $c->setCustomsBroker($data['customsBroker'] ?? null);
$c->setCustomsBrokerPhone($data['customsBrokerPhone'] ?? null); $c->setCustomsBrokerPhone($data['customsBrokerPhone'] ?? null);
$c->setCustomsBrokerEmail($data['customsBrokerEmail'] ?? null); $c->setCustomsBrokerEmail($data['customsBrokerEmail'] ?? null);
@ -665,10 +661,10 @@ class PlugImportWorkflowController extends AbstractController
if (isset($data['declarationNumber'])) $c->setDeclarationNumber($data['declarationNumber']); if (isset($data['declarationNumber'])) $c->setDeclarationNumber($data['declarationNumber']);
if (isset($data['customsCode'])) $c->setCustomsCode($data['customsCode']); if (isset($data['customsCode'])) $c->setCustomsCode($data['customsCode']);
if (isset($data['clearanceDate'])) $c->setClearanceDate($this->jalaliToGregorian($data['clearanceDate']) ?? null); if (isset($data['clearanceDate'])) $c->setClearanceDate($this->jalaliToGregorian($data['clearanceDate']) ?? null);
if (isset($data['customsDuty'])) $c->setCustomsDuty((string)$data['customsDuty']); if (isset($data['customsDuty'])) $c->setCustomsDuty($data['customsDuty'] !== '' ? (string)$data['customsDuty'] : null);
if (isset($data['valueAddedTax'])) $c->setValueAddedTax((string)$data['valueAddedTax']); if (isset($data['valueAddedTax'])) $c->setValueAddedTax($data['valueAddedTax'] !== '' ? (string)$data['valueAddedTax'] : null);
if (isset($data['otherCharges'])) $c->setOtherCharges((string)$data['otherCharges']); if (isset($data['otherCharges'])) $c->setOtherCharges($data['otherCharges'] !== '' ? (string)$data['otherCharges'] : null);
if (isset($data['totalCustomsCharges'])) $c->setTotalCustomsCharges((string)$data['totalCustomsCharges']); if (isset($data['totalCustomsCharges'])) $c->setTotalCustomsCharges($data['totalCustomsCharges'] !== '' ? (string)$data['totalCustomsCharges'] : null);
if (isset($data['customsBroker'])) $c->setCustomsBroker($data['customsBroker']); if (isset($data['customsBroker'])) $c->setCustomsBroker($data['customsBroker']);
if (isset($data['customsBrokerPhone'])) $c->setCustomsBrokerPhone($data['customsBrokerPhone']); if (isset($data['customsBrokerPhone'])) $c->setCustomsBrokerPhone($data['customsBrokerPhone']);
if (isset($data['customsBrokerEmail'])) $c->setCustomsBrokerEmail($data['customsBrokerEmail']); if (isset($data['customsBrokerEmail'])) $c->setCustomsBrokerEmail($data['customsBrokerEmail']);
@ -714,9 +710,9 @@ class PlugImportWorkflowController extends AbstractController
$p = new ImportWorkflowPayment(); $p = new ImportWorkflowPayment();
$p->setImportWorkflow($workflow); $p->setImportWorkflow($workflow);
$p->setType($data['type'] ?? 'other'); $p->setType($data['type'] ?? 'other');
$p->setAmount((string)($data['amount'] ?? '0')); $p->setAmount(($data['amount'] ?? '0'));
$p->setCurrency($data['currency'] ?? 'IRR'); $p->setCurrency($data['currency'] ?? 'IRR');
$p->setAmountIRR(isset($data['amountIRR']) ? (string)$data['amountIRR'] : null); $p->setAmountIRR(isset($data['amountIRR']) && $data['amountIRR'] !== '' ? (string)$data['amountIRR'] : null);
$p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d'));
$p->setReferenceNumber($data['referenceNumber'] ?? null); $p->setReferenceNumber($data['referenceNumber'] ?? null);
$p->setBankName($data['bankName'] ?? null); $p->setBankName($data['bankName'] ?? null);
@ -749,7 +745,7 @@ class PlugImportWorkflowController extends AbstractController
if (isset($data['type'])) $p->setType($data['type']); if (isset($data['type'])) $p->setType($data['type']);
if (isset($data['amount'])) $p->setAmount((string)$data['amount']); if (isset($data['amount'])) $p->setAmount((string)$data['amount']);
if (isset($data['currency'])) $p->setCurrency($data['currency']); if (isset($data['currency'])) $p->setCurrency($data['currency']);
if (isset($data['amountIRR'])) $p->setAmountIRR((string)$data['amountIRR']); if (isset($data['amountIRR'])) $p->setAmountIRR($data['amountIRR'] !== '' ? (string)$data['amountIRR'] : null);
if (isset($data['paymentDate'])) $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); if (isset($data['paymentDate'])) $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d'));
if (isset($data['referenceNumber'])) $p->setReferenceNumber($data['referenceNumber']); if (isset($data['referenceNumber'])) $p->setReferenceNumber($data['referenceNumber']);
if (isset($data['bankName'])) $p->setBankName($data['bankName']); if (isset($data['bankName'])) $p->setBankName($data['bankName']);
@ -818,10 +814,28 @@ class PlugImportWorkflowController extends AbstractController
$item->setModel($data['model'] ?? null); $item->setModel($data['model'] ?? null);
$item->setOriginCountry($data['originCountry'] ?? null); $item->setOriginCountry($data['originCountry'] ?? null);
$item->setQuantity($data['quantity'] ?? '0'); $item->setQuantity($data['quantity'] ?? '0');
$item->setUnitPrice($data['unitPrice'] ?? null); $item->setUnitPrice(isset($data['unitPrice']) && $data['unitPrice'] !== '' ? $data['unitPrice'] : null);
$item->setUnitPriceIRR($data['unitPriceIRR'] ?? null);
$item->setTotalPrice($data['totalPrice'] ?? null); // محاسبه قیمت واحد ریالی بر اساس نرخ ارز پرونده
$item->setTotalPriceIRR($data['totalPriceIRR'] ?? null); $unitPrice = (float) ($data['unitPrice'] ?? 0);
$exchangeRate = (float) ($workflow->getExchangeRate() ?? 0);
$currency = $workflow->getCurrency();
if ($currency === 'IRR') {
$unitPriceIRR = $unitPrice;
} else {
$unitPriceIRR = $unitPrice * $exchangeRate;
}
$item->setUnitPriceIRR((string) round($unitPriceIRR));
// محاسبه قیمت کل
$quantity = (float) ($data['quantity'] ?? 0);
$totalPrice = $quantity * $unitPrice;
$item->setTotalPrice((string) round($totalPrice));
// محاسبه قیمت کل ریالی
$totalPriceIRR = $quantity * $unitPriceIRR;
$item->setTotalPriceIRR((string) round($totalPriceIRR));
$item->setWeight($data['weight'] ?? null); $item->setWeight($data['weight'] ?? null);
$item->setVolume($data['volume'] ?? null); $item->setVolume($data['volume'] ?? null);
$item->setDescription($data['description'] ?? null); $item->setDescription($data['description'] ?? null);
@ -869,10 +883,30 @@ class PlugImportWorkflowController extends AbstractController
if (isset($data['model'])) $item->setModel($data['model']); if (isset($data['model'])) $item->setModel($data['model']);
if (isset($data['originCountry'])) $item->setOriginCountry($data['originCountry']); if (isset($data['originCountry'])) $item->setOriginCountry($data['originCountry']);
if (isset($data['quantity'])) $item->setQuantity($data['quantity']); if (isset($data['quantity'])) $item->setQuantity($data['quantity']);
if (isset($data['unitPrice'])) $item->setUnitPrice($data['unitPrice']); if (isset($data['unitPrice'])) {
if (isset($data['unitPriceIRR'])) $item->setUnitPriceIRR($data['unitPriceIRR']); $item->setUnitPrice($data['unitPrice'] !== '' ? $data['unitPrice'] : null);
if (isset($data['totalPrice'])) $item->setTotalPrice($data['totalPrice']);
if (isset($data['totalPriceIRR'])) $item->setTotalPriceIRR($data['totalPriceIRR']); // محاسبه مجدد قیمت واحد ریالی بر اساس نرخ ارز پرونده
$unitPrice = (float) $data['unitPrice'];
$exchangeRate = (float) ($workflow->getExchangeRate() ?? 0);
$currency = $workflow->getCurrency();
if ($currency === 'IRR') {
$unitPriceIRR = $unitPrice;
} else {
$unitPriceIRR = $unitPrice * $exchangeRate;
}
$item->setUnitPriceIRR((string) round($unitPriceIRR));
// محاسبه مجدد قیمت کل
$quantity = (float) $item->getQuantity();
$totalPrice = $quantity * $unitPrice;
$item->setTotalPrice((string) round($totalPrice));
// محاسبه مجدد قیمت کل ریالی
$totalPriceIRR = $quantity * $unitPriceIRR;
$item->setTotalPriceIRR((string) round($totalPriceIRR));
}
if (isset($data['weight'])) $item->setWeight($data['weight']); if (isset($data['weight'])) $item->setWeight($data['weight']);
if (isset($data['volume'])) $item->setVolume($data['volume']); if (isset($data['volume'])) $item->setVolume($data['volume']);
if (isset($data['description'])) $item->setDescription($data['description']); if (isset($data['description'])) $item->setDescription($data['description']);
@ -942,10 +976,9 @@ class PlugImportWorkflowController extends AbstractController
if (isset($data['supplierAddress'])) $workflow->setSupplierAddress($data['supplierAddress']); if (isset($data['supplierAddress'])) $workflow->setSupplierAddress($data['supplierAddress']);
if (isset($data['supplierPhone'])) $workflow->setSupplierPhone($data['supplierPhone']); if (isset($data['supplierPhone'])) $workflow->setSupplierPhone($data['supplierPhone']);
if (isset($data['supplierEmail'])) $workflow->setSupplierEmail($data['supplierEmail']); if (isset($data['supplierEmail'])) $workflow->setSupplierEmail($data['supplierEmail']);
if (isset($data['totalAmount'])) $workflow->setTotalAmount($data['totalAmount']);
if (isset($data['currency'])) $workflow->setCurrency($data['currency']); if (isset($data['currency'])) $workflow->setCurrency($data['currency']);
if (isset($data['exchangeRate'])) $workflow->setExchangeRate($data['exchangeRate']); if (isset($data['exchangeRate'])) $workflow->setExchangeRate($data['exchangeRate'] !== '' ? $data['exchangeRate'] : null);
if (isset($data['totalAmountIRR'])) $workflow->setTotalAmountIRR($data['totalAmountIRR']);
if (isset($data['status'])) $workflow->setStatus($data['status']); if (isset($data['status'])) $workflow->setStatus($data['status']);
$workflow->setDateMod(date('Y-m-d H:i:s')); $workflow->setDateMod(date('Y-m-d H:i:s'));

View file

@ -60,18 +60,12 @@ class ImportWorkflow
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $supplierEmail = null; private ?string $supplierEmail = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $totalAmount = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $currency = null; private ?string $currency = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $exchangeRate = null; private ?string $exchangeRate = null;
#[ORM\Column(length: 255, nullable: true)]
private ?string $totalAmountIRR = null;
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowItem::class, orphanRemoval: true)] #[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowItem::class, orphanRemoval: true)]
private Collection $items; private Collection $items;
@ -250,17 +244,6 @@ class ImportWorkflow
return $this; return $this;
} }
public function getTotalAmount(): ?string
{
return $this->totalAmount;
}
public function setTotalAmount(?string $totalAmount): static
{
$this->totalAmount = $totalAmount;
return $this;
}
public function getCurrency(): ?string public function getCurrency(): ?string
{ {
return $this->currency; return $this->currency;
@ -283,17 +266,6 @@ class ImportWorkflow
return $this; return $this;
} }
public function getTotalAmountIRR(): ?string
{
return $this->totalAmountIRR;
}
public function setTotalAmountIRR(?string $totalAmountIRR): static
{
$this->totalAmountIRR = $totalAmountIRR;
return $this;
}
public function getItems(): Collection public function getItems(): Collection
{ {
return $this->items; return $this->items;

View file

@ -29,16 +29,16 @@ class ImportWorkflowCustoms
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $clearanceDate = null; private ?string $clearanceDate = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $customsDuty = null; private ?string $customsDuty = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $valueAddedTax = null; private ?string $valueAddedTax = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $otherCharges = null; private ?string $otherCharges = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalCustomsCharges = null; private ?string $totalCustomsCharges = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]

View file

@ -43,16 +43,16 @@ class ImportWorkflowItem
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $quantity = null; private ?string $quantity = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPrice = null; private ?string $unitPrice = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $unitPriceIRR = null; private ?string $unitPriceIRR = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPrice = null; private ?string $totalPrice = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $totalPriceIRR = null; private ?string $totalPriceIRR = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]

View file

@ -23,13 +23,13 @@ class ImportWorkflowPayment
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]
private ?string $type = null; private ?string $type = null;
#[ORM\Column(length: 255)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2)]
private ?string $amount = null; private ?string $amount = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(length: 255, nullable: true)]
private ?string $currency = null; private ?string $currency = null;
#[ORM\Column(length: 255, nullable: true)] #[ORM\Column(type: Types::DECIMAL, precision: 15, scale: 2, nullable: true)]
private ?string $amountIRR = null; private ?string $amountIRR = null;
#[ORM\Column(length: 255)] #[ORM\Column(length: 255)]

View file

@ -15,7 +15,7 @@ class FileStorage
{ {
$safeOriginal = preg_replace('/[^A-Za-z0-9_.-]/', '_', $file->getClientOriginalName()); $safeOriginal = preg_replace('/[^A-Za-z0-9_.-]/', '_', $file->getClientOriginalName());
$relativeDir = 'storage/' . trim($businessId) . '/' . trim($context); $relativeDir = 'storage/' . trim($businessId) . '/' . trim($context);
$absDir = rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativeDir; $absDir = rtrim($this->kernel->getProjectDir(), '/').'/../hesabixArchive/' . $relativeDir;
if (!is_dir($absDir)) { if (!is_dir($absDir)) {
@mkdir($absDir, 0775, true); @mkdir($absDir, 0775, true);
} }
@ -35,7 +35,7 @@ class FileStorage
public function absolutePath(string $relativePath): string public function absolutePath(string $relativePath): string
{ {
$relativePath = ltrim($relativePath, '/'); $relativePath = ltrim($relativePath, '/');
return rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativePath; return rtrim($this->kernel->getProjectDir(), '/').'/../hesabixArchive/' . $relativePath;
} }
} }

View file

@ -75,18 +75,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field
class="ltr-input"
:model-value="formatMoney(formData.totalAmount)"
label="مبلغ کل"
type="text"
inputmode="numeric"
:rules="[rules.positiveMoney, rules.maxAmount]"
@update:modelValue="onMoneyInput('totalAmount', $event)"
></v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-select <v-select
v-model="formData.currency" v-model="formData.currency"
:items="currencyOptions" :items="currencyOptions"
@ -95,26 +84,29 @@
required required
></v-select> ></v-select>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field <v-text-field
class="ltr-input" class="ltr-input"
:model-value="formatMoney(formData.exchangeRate)" :model-value="formatMoneyTyping(formData.exchangeRate)"
label="نرخ تبدیل" label="نرخ تبدیل (ریال)"
type="text" type="text"
inputmode="numeric" inputmode="decimal"
:rules="[rules.positiveMoney, rules.maxExchangeRate]" :rules="[rules.positiveMoney, rules.maxExchangeRate]"
@update:modelValue="onMoneyInput('exchangeRate', $event)" @update:modelValue="onMoneyInput('exchangeRate', $event)"
@blur="formData.exchangeRate = parseMoney(formData.exchangeRate)"
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-alert
:model-value="formatMoney(formData.totalAmountIRR)" type="info"
label="مبلغ کل (ریال)" variant="tonal"
readonly class="mb-4"
></v-text-field> >
<strong>نکته:</strong> مبلغ کل پرونده به صورت خودکار از جمع اقلام محاسبه میشود و نیازی به وارد کردن دستی نیست.
</v-alert>
</v-col> </v-col>
</v-row> </v-row>
@ -174,10 +166,8 @@ const formData = ref({
supplierPhone: '', supplierPhone: '',
supplierEmail: '', supplierEmail: '',
supplierAddress: '', supplierAddress: '',
totalAmount: '',
currency: 'USD', currency: 'USD',
exchangeRate: '', exchangeRate: '',
totalAmountIRR: '',
description: '' description: ''
}) })
@ -213,35 +203,34 @@ const rules = {
maxExchangeRate: (value) => !value || parseFloat(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد' maxExchangeRate: (value) => !value || parseFloat(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد'
} }
const parseMoneyInput = (val) => { const parseMoney = (val) => {
if (val === null || val === undefined) return 0 if (val === null || val === undefined || val === '') return 0
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '') const clean = String(val).replace(/,/g, '')
const num = Number(cleaned) const num = parseFloat(clean)
return Number.isFinite(num) ? num : 0 return isNaN(num) ? 0 : parseFloat(num.toFixed(2))
} }
const onMoneyInput = (field, value) => { const formatMoneyTyping = (val) => {
const numeric = parseMoneyInput(value) if (val === null || val === undefined || val === '') return ''
formData.value[field] = numeric const str = String(val).replace(/,/g, '')
if (str === '') return ''
const parts = str.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
const onMoneyInput = (field, val) => {
formData.value[field] = parseMoney(val)
} }
const formatMoney = (value) => { const formatMoney = (value) => {
const numericValue = Number(value) || 0 const numericValue = Number(value) || 0
return numericValue return numericValue
.toFixed(0) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
watch([ // مبلغ کل به صورت محاسباتی از اقلام محاسبه میشود
() => formData.value.totalAmount,
() => formData.value.exchangeRate,
() => formData.value.currency
], ([newTotalAmount, newExchangeRate, currency]) => {
const total = parseMoneyInput(newTotalAmount)
const rate = currency === 'IRR' ? 1 : parseMoneyInput(newExchangeRate)
const result = Math.round(total * rate)
formData.value.totalAmountIRR = isNaN(result) ? 0 : result
}, { immediate: true })
// Methods // Methods
const create = async () => { const create = async () => {
@ -287,10 +276,8 @@ const resetForm = () => {
supplierPhone: '', supplierPhone: '',
supplierEmail: '', supplierEmail: '',
supplierAddress: '', supplierAddress: '',
totalAmount: '',
currency: 'USD', currency: 'USD',
exchangeRate: '', exchangeRate: '',
totalAmountIRR: '',
description: '' description: ''
} }
if (form.value) { if (form.value) {

View file

@ -28,6 +28,12 @@
</div> </div>
</template> </template>
<template v-slot:item.clearanceDate="{ item }">
<div>
{{ formatDate(item.clearanceDate) }}
</div>
</template>
<template v-slot:item.actions="{ item }"> <template v-slot:item.actions="{ item }">
<v-btn <v-btn
icon="mdi-pencil" icon="mdi-pencil"
@ -93,34 +99,37 @@
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-text-field <v-text-field
class="ltr-input" class="ltr-input"
:model-value="formatMoney(formData.customsDuty)" :model-value="formatMoneyTyping(formData.customsDuty)"
label="حقوق گمرکی" label="حقوق گمرکی"
type="text" type="text"
inputmode="numeric" inputmode="decimal"
:rules="[rules.positiveMoney]" :rules="[rules.positiveMoney]"
@update:modelValue="onMoneyInput('customsDuty', $event)" @update:modelValue="onMoneyInput('customsDuty', $event)"
@blur="formData.customsDuty = parseMoney(formData.customsDuty)"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-text-field <v-text-field
class="ltr-input" class="ltr-input"
:model-value="formatMoney(formData.valueAddedTax)" :model-value="formatMoneyTyping(formData.valueAddedTax)"
label="مالیات بر ارزش افزوده" label="مالیات بر ارزش افزوده"
type="text" type="text"
inputmode="numeric" inputmode="decimal"
:rules="[rules.positiveMoney]" :rules="[rules.positiveMoney]"
@update:modelValue="onMoneyInput('valueAddedTax', $event)" @update:modelValue="onMoneyInput('valueAddedTax', $event)"
@blur="formData.valueAddedTax = parseMoney(formData.valueAddedTax)"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<v-text-field <v-text-field
class="ltr-input" class="ltr-input"
:model-value="formatMoney(formData.otherCharges)" :model-value="formatMoneyTyping(formData.otherCharges)"
label="سایر عوارض" label="سایر عوارض"
type="text" type="text"
inputmode="numeric" inputmode="decimal"
:rules="[rules.positiveMoney]" :rules="[rules.positiveMoney]"
@update:modelValue="onMoneyInput('otherCharges', $event)" @update:modelValue="onMoneyInput('otherCharges', $event)"
@blur="formData.otherCharges = parseMoney(formData.otherCharges)"
></v-text-field> ></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@ -261,22 +270,30 @@ const rules = {
} }
// Helpers for money formatting/parse and LTR input // Helpers for money formatting/parse and LTR input
const parseMoneyInput = (val) => { const parseMoney = (val) => {
if (val === null || val === undefined) return 0 if (val === null || val === undefined || val === '') return 0
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '') const clean = String(val).replace(/,/g, '')
const num = Number(cleaned) const num = parseFloat(clean)
return Number.isFinite(num) ? num : 0 return isNaN(num) ? 0 : parseFloat(num.toFixed(2))
} }
const onMoneyInput = (field, value) => { const formatMoneyTyping = (val) => {
const numeric = parseMoneyInput(value) if (val === null || val === undefined || val === '') return ''
formData.value[field] = numeric const str = String(val).replace(/,/g, '')
if (str === '') return ''
const parts = str.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
const onMoneyInput = (field, val) => {
formData.value[field] = parseMoney(val)
} }
const formatMoney = (value) => { const formatMoney = (value) => {
const numericValue = Number(value) || 0 const numericValue = Number(value) || 0
return numericValue return numericValue
.toFixed(0) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
@ -391,6 +408,11 @@ const formatNumber = (number) => {
if (!number) return '0' if (!number) return '0'
return new Intl.NumberFormat('fa-IR').format(number) return new Intl.NumberFormat('fa-IR').format(number)
} }
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('fa-IR')
}
</script> </script>
<style scoped> <style scoped>

View file

@ -3,24 +3,13 @@
<v-card-text> <v-card-text>
<div class="d-flex justify-space-between align-center mb-4"> <div class="d-flex justify-space-between align-center mb-4">
<h3>آیتمهای وارداتی</h3> <h3>آیتمهای وارداتی</h3>
<v-btn <v-btn color="primary" prepend-icon="mdi-plus" @click="showAddDialog = true">
color="primary"
prepend-icon="mdi-plus"
@click="showAddDialog = true"
>
افزودن آیتم افزودن آیتم
</v-btn> </v-btn>
</div> </div>
<v-data-table <v-data-table :headers="headers" :items="items" :loading="loading" density="comfortable" class="elevation-1"
:headers="headers" :header-props="{ class: 'custom-header' }" no-data-text="آیتمی ثبت نشده است">
:items="items"
:loading="loading"
density="comfortable"
class="elevation-1"
:header-props="{ class: 'custom-header' }"
no-data-text="آیتمی ثبت نشده است"
>
<template v-slot:item.unitPrice="{ item }"> <template v-slot:item.unitPrice="{ item }">
<div> <div>
{{ formatNumber(item.unitPrice) }} {{ formatNumber(item.unitPrice) }}
@ -29,26 +18,22 @@
</template> </template>
<template v-slot:item.totalPrice="{ item }"> <template v-slot:item.totalPrice="{ item }">
<div> <div class="d-flex justify-center align-center gap-2">
{{ formatNumber(item.totalPrice) }} <div>
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small> {{ formatNumber(item.unitPrice * item.quantity) }}
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
</div>
<span class="mx-1">|</span>
<div>
{{ formatNumber(Number(item.unitPrice * item.quantity) * Number(props.exchangeRate)) }}
<small class="text-medium-emphasis">ریال</small>
</div>
</div> </div>
</template> </template>
<template v-slot:item.actions="{ item }"> <template v-slot:item.actions="{ item }">
<v-btn <v-btn icon="mdi-pencil" size="small" variant="text" @click="editItem(item)"></v-btn>
icon="mdi-pencil" <v-btn icon="mdi-delete" size="small" variant="text" color="error" @click="deleteItem(item)"></v-btn>
size="small"
variant="text"
@click="editItem(item)"
></v-btn>
<v-btn
icon="mdi-delete"
size="small"
variant="text"
color="error"
@click="deleteItem(item)"
></v-btn>
</template> </template>
</v-data-table> </v-data-table>
</v-card-text> </v-card-text>
@ -68,20 +53,12 @@
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<template v-if="!editingItem"> <template v-if="!editingItem">
<Hcommoditysearch <Hcommoditysearch v-model="selectedCommodity" :return-object="true" label="انتخاب کالا" />
v-model="selectedCommodity"
:return-object="true"
label="انتخاب کالا"
/>
</template> </template>
<template v-else> <template v-else>
<v-text-field <v-text-field
:model-value="selectedCommodity ? (selectedCommodity.name + (selectedCommodity.code ? ` (${selectedCommodity.code})` : '')) : ''" :model-value="selectedCommodity ? (selectedCommodity.name + (selectedCommodity.code ? ` (${selectedCommodity.code})` : '')) : ''"
label="کالا" label="کالا" variant="outlined" density="compact" disabled />
variant="outlined"
density="compact"
disabled
/>
</template> </template>
</v-col> </v-col>
</v-row> </v-row>
@ -90,52 +67,32 @@
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.brand" label="برند"></v-text-field>
v-model="formData.brand"
label="برند"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.model" label="مدل"></v-text-field>
v-model="formData.model"
label="مدل"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.originCountry" label="کشور مبدا"></v-text-field>
v-model="formData.originCountry"
label="کشور مبدا"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.quantity" label="تعداد" type="number"
v-model="formData.quantity" :rules="[rules.required, rules.positive]" required min="1"></v-text-field>
label="تعداد"
type="number"
:rules="[rules.required, rules.positive]"
required
min="1"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field <v-text-field class="ltr-input" :model-value="formatMoneyTyping(formData.unitPrice)"
class="ltr-input" label="قیمت واحد (ارزی)" type="text" inputmode="decimal"
:model-value="formatMoney(formData.unitPrice)" :rules="[rules.required, rules.positiveMoney]" required
label="قیمت واحد (ارزی)"
type="text"
inputmode="numeric"
:rules="[rules.required, rules.positiveMoney]"
required
@update:modelValue="onMoneyInput('unitPrice', $event)" @update:modelValue="onMoneyInput('unitPrice', $event)"
></v-text-field> @blur="formData.unitPrice = parseMoney(formData.unitPrice)"></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <!-- <v-col cols="12" md="4">
<v-text-field <v-text-field
class="ltr-input" class="ltr-input"
:model-value="formatMoney(formData.unitPriceIRR)" :model-value="formatMoney(formData.unitPriceIRR)"
@ -146,56 +103,32 @@
required required
@update:modelValue="onMoneyInput('unitPriceIRR', $event)" @update:modelValue="onMoneyInput('unitPriceIRR', $event)"
></v-text-field> ></v-text-field>
</v-col> </v-col> -->
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field <v-text-field :model-value="formatMoney(computedTotalPriceIRR)" label="قیمت کل" readonly></v-text-field>
:model-value="(formData.quantity && formData.unitPrice) ? formatMoney(totalPrice) : ''"
label="قیمت کل"
readonly
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.weight" label="وزن (کیلوگرم)" type="number" step="0.01"
v-model="formData.weight" :rules="[rules.positive]" min="0"></v-text-field>
label="وزن (کیلوگرم)"
type="number"
step="0.01"
:rules="[rules.positive]"
min="0"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.volume" label="حجم (متر مکعب)" type="number" step="0.01"
v-model="formData.volume" :rules="[rules.positive]" min="0"></v-text-field>
label="حجم (متر مکعب)"
type="number"
step="0.01"
:rules="[rules.positive]"
min="0"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea v-model="formData.specifications" label="ویژگی‌ها" rows="2"></v-textarea>
v-model="formData.specifications"
label="ویژگی‌ها"
rows="2"
></v-textarea>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea v-model="formData.description" label="توضیحات" rows="2"></v-textarea>
v-model="formData.description"
label="توضیحات"
rows="2"
></v-textarea>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
@ -205,12 +138,7 @@
<v-card-actions class="pa-4"> <v-card-actions class="pa-4">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="closeDialog">لغو</v-btn> <v-btn @click="closeDialog">لغو</v-btn>
<v-btn <v-btn type="submit" color="primary" :loading="loading" :disabled="!valid">
type="submit"
color="primary"
:loading="loading"
:disabled="!valid"
>
{{ editingItem ? 'ویرایش' : 'افزودن' }} {{ editingItem ? 'ویرایش' : 'افزودن' }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -254,6 +182,10 @@ const props = defineProps({
currency: { currency: {
type: String, type: String,
default: 'USD' default: 'USD'
},
exchangeRate: {
type: [String, Number],
default: 0
} }
}) })
@ -311,50 +243,54 @@ const headers = [
// Validation rules // Validation rules
const rules = { const rules = {
required: (value) => !!value || 'این فیلد الزامی است', required: (value) =>
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد', value !== null && value !== '' && value !== undefined || 'این فیلد الزامی است',
positive: (value) => {
const num = parseMoney(value)
return num > 0 || 'مقدار باید مثبت باشد'
},
positiveMoney: (value) => { positiveMoney: (value) => {
const numeric = parseMoneyInput(value) const num = parseMoney(value)
return numeric > 0 || 'مقدار باید مثبت باشد' return num > 0 || 'مقدار باید مثبت باشد'
} }
} }
// Helpers for money formatting/parse and LTR input
const parseMoneyInput = (val) => {
if (val === null || val === undefined) return 0
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
const num = Number(cleaned)
return Number.isFinite(num) ? num : 0
}
const onMoneyInput = (field, value) => {
const numeric = parseMoneyInput(value)
formData.value[field] = numeric
}
const formatMoney = (value) => { const formatMoney = (value) => {
const numericValue = Number(value) || 0 const numericValue = Number(value) || 0
return numericValue return numericValue
.toFixed(0) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
// Computed const parseMoney = (val) => {
const totalPrice = computed(() => { if (val === null || val === undefined || val === '') return 0
if (formData.value.quantity && formData.value.unitPrice) { const clean = String(val).replace(/,/g, '')
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPrice) const num = parseFloat(clean)
formData.value.totalPrice = total.toString() return isNaN(num) ? 0 : parseFloat(num.toFixed(2))
return total }
}
return 0
})
// Watch for unit price IRR and quantity changes const formatMoneyTyping = (val) => {
watch([() => formData.value.quantity, () => formData.value.unitPriceIRR], () => { if (val === null || val === undefined || val === '') return ''
if (formData.value.quantity && formData.value.unitPriceIRR) { const str = String(val).replace(/,/g, '')
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPriceIRR) if (str === '') return ''
formData.value.totalPriceIRR = total.toString() const parts = str.split('.')
} parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
const onMoneyInput = (field, val) => {
formData.value[field] = parseMoney(val)
}
const computedTotalPriceIRR = computed(() => {
if (!formData.value.quantity || !formData.value.unitPrice || !props.exchangeRate) return 0
const quantity = parseFloat(formData.value.quantity)
const unitPrice = parseFloat(formData.value.unitPrice)
const exchangeRate = parseFloat(props.exchangeRate)
const currency = props.currency
if (currency === 'IRR') return quantity * unitPrice
return (quantity * unitPrice * exchangeRate).toFixed(2)
}) })
// Methods // Methods
@ -482,12 +418,19 @@ const closeDialog = () => {
} }
// Utilities // Utilities
const formatNumber = (number) => { const formatNumber = (number, fractionDigits = 2) => {
if (!number) return '0' if (number === null || number === undefined || number === '') return '0'
return new Intl.NumberFormat('fa-IR').format(number) const formatted = new Intl.NumberFormat('fa-IR', {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
}).format(number)
return formatted.replace('٫', '.')
} }
const getCurrency = () => props.currency || 'USD' const getCurrency = (item) => {
return props.currency || 'USD'
}
</script> </script>
<style scoped> <style scoped>
@ -518,6 +461,3 @@ const getCurrency = () => props.currency || 'USD'
background-color: #f5f5f5 !important; background-color: #f5f5f5 !important;
} }
</style> </style>

View file

@ -3,30 +3,15 @@
<v-card-text> <v-card-text>
<div class="d-flex justify-space-between align-center mb-4"> <div class="d-flex justify-space-between align-center mb-4">
<h3>پرداختها</h3> <h3>پرداختها</h3>
<v-btn <v-btn color="primary" prepend-icon="mdi-plus" @click="showAddDialog = true">
color="primary"
prepend-icon="mdi-plus"
@click="showAddDialog = true"
>
افزودن پرداخت افزودن پرداخت
</v-btn> </v-btn>
</div> </div>
<v-data-table <v-data-table :headers="headers" :items="payments" :loading="loading" density="comfortable" class="elevation-1"
:headers="headers" :header-props="{ class: 'custom-header' }" no-data-text="پرداختی ثبت نشده است">
:items="payments"
:loading="loading"
density="comfortable"
class="elevation-1"
:header-props="{ class: 'custom-header' }"
no-data-text="پرداختی ثبت نشده است"
>
<template v-slot:item.type="{ item }"> <template v-slot:item.type="{ item }">
<v-chip <v-chip :color="getTypeColor(item.type)" size="small" variant="flat">
:color="getTypeColor(item.type)"
size="small"
variant="flat"
>
{{ getTypeText(item.type) }} {{ getTypeText(item.type) }}
</v-chip> </v-chip>
</template> </template>
@ -40,16 +25,13 @@
<template v-slot:item.amountIRR="{ item }"> <template v-slot:item.amountIRR="{ item }">
<div> <div>
{{ formatNumber(item.amountIRR) }} {{ formatNumber(Number(item.amount) * Number(props.exchangeRate)) }}
<small class="text-medium-emphasis">ریال</small> <small class="text-medium-emphasis">ریال</small>
</div> </div>
</template> </template>
<template v-slot:item.status="{ item }"> <template v-slot:item.status="{ item }">
<v-chip <v-chip :color="getStatusColor(item.status)" size="small">
:color="getStatusColor(item.status)"
size="small"
>
{{ getStatusText(item.status) }} {{ getStatusText(item.status) }}
</v-chip> </v-chip>
</template> </template>
@ -59,19 +41,8 @@
</template> </template>
<template v-slot:item.actions="{ item }"> <template v-slot:item.actions="{ item }">
<v-btn <v-btn icon="mdi-pencil" size="small" variant="text" @click="editPayment(item)"></v-btn>
icon="mdi-pencil" <v-btn icon="mdi-delete" size="small" variant="text" color="error" @click="deletePayment(item)"></v-btn>
size="small"
variant="text"
@click="editPayment(item)"
></v-btn>
<v-btn
icon="mdi-delete"
size="small"
variant="text"
color="error"
@click="deletePayment(item)"
></v-btn>
</template> </template>
</v-data-table> </v-data-table>
</v-card-text> </v-card-text>
@ -89,113 +60,65 @@
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select <v-select v-model="formData.type" :items="paymentTypes" label="نوع پرداخت" :rules="[rules.required]"
v-model="formData.type" required></v-select>
:items="paymentTypes"
label="نوع پرداخت"
:rules="[rules.required]"
required
></v-select>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-select <v-select v-model="formData.status" :items="statusOptions" label="وضعیت" :rules="[rules.required]"
v-model="formData.status" required></v-select>
:items="statusOptions"
label="وضعیت"
:rules="[rules.required]"
required
></v-select>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field <v-text-field class="ltr-input" :model-value="formatMoneyTyping(formData.amount)" label="مبلغ (ارزی)"
class="ltr-input" type="text" inputmode="decimal" :rules="[rules.required, rules.positiveMoney]" required
:model-value="formatMoney(formData.amount)"
label="مبلغ"
type="text"
inputmode="numeric"
:rules="[rules.required, rules.positiveMoney]"
required
@update:modelValue="onMoneyInput('amount', $event)" @update:modelValue="onMoneyInput('amount', $event)"
></v-text-field> @blur="formData.amount = parseMoney(formData.amount)"></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-select <v-text-field :model-value="formatMoney(computedAmountIRR)" label="مبلغ (ریال) - محاسباتی" readonly
v-model="formData.currency" variant="outlined" color="primary"></v-text-field>
:items="currencyOptions"
label="واحد پول"
:rules="[rules.required]"
required
></v-select>
</v-col> </v-col>
<v-col cols="12" md="4"> </v-row>
<v-text-field <v-row>
class="ltr-input" <v-col cols="12">
:model-value="formatMoney(formData.amountIRR)" <v-alert type="info" variant="tonal" class="mb-4">
label="مبلغ (ریال)" <strong>نکته:</strong> مبلغ ریالی به صورت خودکار بر اساس نرخ ارز پرونده محاسبه میشود.
type="text" </v-alert>
inputmode="numeric"
:rules="[rules.positiveMoney]"
@update:modelValue="onMoneyInput('amountIRR', $event)"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<h-date-picker <h-date-picker v-model="formData.paymentDate" label="تاریخ پرداخت" :rules="[rules.required]" />
v-model="formData.paymentDate"
label="تاریخ پرداخت"
:rules="[rules.required]"
/>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.referenceNumber" label="شماره مرجع"></v-text-field>
v-model="formData.referenceNumber"
label="شماره مرجع"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.bankName" label="نام بانک"></v-text-field>
v-model="formData.bankName"
label="نام بانک"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.accountNumber" label="شماره حساب"></v-text-field>
v-model="formData.accountNumber"
label="شماره حساب"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.recipientName" label="نام دریافت کننده"></v-text-field>
v-model="formData.recipientName"
label="نام دریافت کننده"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="formData.receiptNumber" label="شماره رسید"></v-text-field>
v-model="formData.receiptNumber"
label="شماره رسید"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea v-model="formData.description" label="توضیحات" rows="2"></v-textarea>
v-model="formData.description"
label="توضیحات"
rows="2"
></v-textarea>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
@ -205,12 +128,7 @@
<v-card-actions class="pa-4"> <v-card-actions class="pa-4">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn @click="closeDialog">لغو</v-btn> <v-btn @click="closeDialog">لغو</v-btn>
<v-btn <v-btn type="submit" color="primary" :loading="loading" :disabled="!valid">
type="submit"
color="primary"
:loading="loading"
:disabled="!valid"
>
{{ editingPayment ? 'ویرایش' : 'افزودن' }} {{ editingPayment ? 'ویرایش' : 'افزودن' }}
</v-btn> </v-btn>
</v-card-actions> </v-card-actions>
@ -236,7 +154,7 @@
</template> </template>
<script setup> <script setup>
import { ref } from 'vue' import { computed, ref } from 'vue'
import axios from 'axios' import axios from 'axios'
import Swal from 'sweetalert2' import Swal from 'sweetalert2'
import HDatepicker from '@/components/forms/Hdatepicker.vue' import HDatepicker from '@/components/forms/Hdatepicker.vue'
@ -250,6 +168,14 @@ const props = defineProps({
payments: { payments: {
type: Array, type: Array,
default: () => [] default: () => []
},
currency: {
type: String,
default: 'USD'
},
exchangeRate: {
type: [String, Number],
default: 0
} }
}) })
@ -321,34 +247,56 @@ const currencyOptions = [
// Validation rules // Validation rules
const rules = { const rules = {
required: (value) => !!value || 'این فیلد الزامی است', required: (value) =>
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد', value !== null && value !== '' && value !== undefined || 'این فیلد الزامی است',
positive: (value) => {
const num = parseMoney(value)
return num > 0 || 'مقدار باید مثبت باشد'
},
positiveMoney: (value) => { positiveMoney: (value) => {
const numeric = parseMoneyInput(value) const num = parseMoney(value)
return numeric > 0 || 'مقدار باید مثبت باشد' return num > 0 || 'مقدار باید مثبت باشد'
} }
} }
// Helpers for money formatting/parse and LTR input const parseMoney = (val) => {
const parseMoneyInput = (val) => { if (val === null || val === undefined || val === '') return 0
if (val === null || val === undefined) return 0 const clean = String(val).replace(/,/g, '')
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '') const num = parseFloat(clean)
const num = Number(cleaned) return isNaN(num) ? 0 : parseFloat(num.toFixed(2))
return Number.isFinite(num) ? num : 0
} }
const onMoneyInput = (field, value) => { const formatMoneyTyping = (val) => {
const numeric = parseMoneyInput(value) if (val === null || val === undefined || val === '') return ''
formData.value[field] = numeric const str = String(val).replace(/,/g, '')
if (str === '') return ''
const parts = str.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
const onMoneyInput = (field, val) => {
formData.value[field] = parseMoney(val)
} }
const formatMoney = (value) => { const formatMoney = (value) => {
const numericValue = Number(value) || 0 const numericValue = Number(value) || 0
return numericValue return numericValue
.toFixed(0) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
// محاسبه مبلغ ریالی بر اساس نرخ ارز پرونده
const computedAmountIRR = computed(() => {
if (!formData.value.amount || !props.exchangeRate) return 0
const amount = parseFloat(formData.value.amount)
const exchangeRate = parseFloat(props.exchangeRate)
const currency = props.currency
if (currency === 'IRR') return amount
return Math.round(amount * exchangeRate)
})
// Methods // Methods
const editPayment = (payment) => { const editPayment = (payment) => {
editingPayment.value = payment editingPayment.value = payment
@ -498,9 +446,14 @@ const getStatusText = (status) => {
return texts[status] || status return texts[status] || status
} }
const formatNumber = (number) => { const formatNumber = (number, fractionDigits = 2) => {
if (!number) return '0' if (number === null || number === undefined || number === '') return '0'
return new Intl.NumberFormat('fa-IR').format(number) const formatted = new Intl.NumberFormat('fa-IR', {
minimumFractionDigits: fractionDigits,
maximumFractionDigits: fractionDigits
}).format(number)
return formatted.replace('٫', '.')
} }
const formatDate = (date) => { const formatDate = (date) => {
@ -542,6 +495,3 @@ const closeDialog = () => {
background-color: #f5f5f5 !important; background-color: #f5f5f5 !important;
} }
</style> </style>

View file

@ -508,6 +508,7 @@ const fa_lang = {
year_label: "سال مالی جاری", year_label: "سال مالی جاری",
global_settings: "تنظیمات سراسری", global_settings: "تنظیمات سراسری",
warranty_settings: "تنظیمات گارانتی", warranty_settings: "تنظیمات گارانتی",
warranty_page_title: " سریال های گارانتی",
gate_pay: "درگاه پرداخت", gate_pay: "درگاه پرداخت",
a4l: "کاغذ A4 افقی", a4l: "کاغذ A4 افقی",
a4p: "کاغذ A4 عمودی", a4p: "کاغذ A4 عمودی",
@ -826,6 +827,7 @@ const fa_lang = {
sms_settings_plug_accpro_pass_cheque_input: "واگذاری چک", sms_settings_plug_accpro_pass_cheque_input: "واگذاری چک",
sms_settings_reject_cheque_input: "برگشت چک", sms_settings_reject_cheque_input: "برگشت چک",
sms_settings_plug_accpro_reject_cheque_input: "برگشت چک", sms_settings_plug_accpro_reject_cheque_input: "برگشت چک",
sms_settings_warranty_send_serial: "ارسال سریال گارانتی",
inquiry_zohal_api_key: "کلید API زحل", inquiry_zohal_api_key: "کلید API زحل",
inquiry_zohal_api_key_des: "کلید API زحل برای دریافت اطلاعات از سامانه زحل", inquiry_zohal_api_key_des: "کلید API زحل برای دریافت اطلاعات از سامانه زحل",
inquiry_zohal_api_key_placeholder: "کلید API زحل", inquiry_zohal_api_key_placeholder: "کلید API زحل",

View file

@ -127,17 +127,6 @@
class="mb-3" class="mb-3"
@update:model-value="loadWorkflows" @update:model-value="loadWorkflows"
/> />
<v-select
v-model="filters.status"
label="وضعیت"
:items="statusOptions"
clearable
density="compact"
variant="outlined"
hide-details
class="mb-3"
@update:model-value="loadWorkflows"
/>
</div> </div>
<!-- دسکتاپ --> <!-- دسکتاپ -->
@ -155,18 +144,6 @@
@update:model-value="loadWorkflows" @update:model-value="loadWorkflows"
class="ml-2" class="ml-2"
/> />
<v-select
v-model="filters.status"
label="وضعیت"
:items="statusOptions"
clearable
density="compact"
variant="outlined"
hide-details
style="max-width: 200px;"
@update:model-value="loadWorkflows"
class="ml-2"
/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn <v-btn
color="primary" color="primary"
@ -179,15 +156,6 @@
</div> </div>
</template> </template>
<template v-slot:item.status="{ item }">
<v-chip
:color="getStatusColor(item.status)"
size="small"
>
{{ getStatusText(item.status) }}
</v-chip>
</template>
<template v-slot:item.totalAmount="{ item }"> <template v-slot:item.totalAmount="{ item }">
<div> <div>
{{ formatNumber(item.totalAmount) }} {{ formatNumber(item.totalAmount) }}
@ -322,7 +290,6 @@ const showNotification = (text, color = 'success') => {
// Filters // Filters
const filters = ref({ const filters = ref({
status: '',
search: '' search: ''
}) })
@ -340,22 +307,10 @@ const headers = [
{ title: 'عنوان', key: 'title', sortable: true }, { title: 'عنوان', key: 'title', sortable: true },
{ title: 'تامین کننده', key: 'supplierName', sortable: true }, { title: 'تامین کننده', key: 'supplierName', sortable: true },
{ title: 'مبلغ کل', key: 'totalAmount', sortable: true }, { title: 'مبلغ کل', key: 'totalAmount', sortable: true },
{ title: 'وضعیت', key: 'status', sortable: true },
{ title: 'تاریخ ثبت', key: 'dateSubmit', sortable: true }, { title: 'تاریخ ثبت', key: 'dateSubmit', sortable: true },
{ title: 'ثبت کننده', key: 'submitter', sortable: true } { title: 'ثبت کننده', key: 'submitter', sortable: true }
] ]
// Status options
const statusOptions = [
{ title: 'پیش‌نویس', value: 'draft' },
{ title: 'در حال پردازش', value: 'processing' },
{ title: 'ارسال شده', value: 'shipped' },
{ title: 'رسیده', value: 'arrived' },
{ title: 'ترخیص شده', value: 'cleared' },
{ title: 'تکمیل شده', value: 'completed' },
{ title: 'لغو شده', value: 'cancelled' }
]
// Methods // Methods
const loadWorkflows = async () => { const loadWorkflows = async () => {
loading.value = true loading.value = true
@ -365,10 +320,6 @@ const loadWorkflows = async () => {
limit: pagination.value.limit limit: pagination.value.limit
} }
if (filters.value.status) {
params.status = filters.value.status
}
if (filters.value.search) { if (filters.value.search) {
params.search = filters.value.search params.search = filters.value.search
} }
@ -398,30 +349,18 @@ const loadStats = async () => {
} else { } else {
// Calculate stats from current data if API not available // Calculate stats from current data if API not available
const totalWorkflows = workflows.value.length const totalWorkflows = workflows.value.length
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
stats.value = { stats.value = {
totalWorkflows, totalWorkflows,
draftWorkflows,
processingWorkflows,
completedWorkflows
} }
} }
} catch (error) { } catch (error) {
console.error('خطا در بارگذاری آمار:', error) console.error('خطا در بارگذاری آمار:', error)
// Calculate stats from current data // Calculate stats from current data
const totalWorkflows = workflows.value.length const totalWorkflows = workflows.value.length
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
stats.value = { stats.value = {
totalWorkflows, totalWorkflows,
draftWorkflows,
processingWorkflows,
completedWorkflows
} }
} finally { } finally {
statsLoading.value = false statsLoading.value = false
@ -434,11 +373,6 @@ const updatePagination = (options) => {
loadWorkflows() loadWorkflows()
} }
const filterByStatus = (status) => {
filters.value.status = filters.value.status === status ? '' : status
loadWorkflows()
}
const viewWorkflow = (workflow) => { const viewWorkflow = (workflow) => {
router.push(`/acc/plugins/import-workflow/${workflow.id}`) router.push(`/acc/plugins/import-workflow/${workflow.id}`)
} }
@ -479,50 +413,6 @@ const onWorkflowCreated = () => {
// loadStats() // loadStats()
} }
// Utilities
const getStatusColor = (status) => {
const colors = {
draft: 'grey',
processing: 'blue',
shipped: 'orange',
arrived: 'purple',
cleared: 'teal',
completed: 'green',
cancelled: 'red'
}
return colors[status] || 'grey'
}
const getStatusText = (status) => {
const texts = {
draft: 'پیش‌نویس',
processing: 'در حال پردازش',
shipped: 'ارسال شده',
arrived: 'رسیده',
cleared: 'ترخیص شده',
completed: 'تکمیل شده',
cancelled: 'لغو شده'
}
return texts[status] || status
}
const getCardClasses = (status) => {
const baseClasses = 'stats-card'
const statusClasses = {
'draft': 'draft-card',
'processing': 'processing-card',
'completed': 'completed-card'
}
const classes = [baseClasses, statusClasses[status]]
if (filters.value.status === status) {
classes.push('active-filter')
}
return classes.join(' ')
}
const formatNumber = (number) => { const formatNumber = (number) => {
if (!number) return '0' if (!number) return '0'
return new Intl.NumberFormat('fa-IR').format(number) return new Intl.NumberFormat('fa-IR').format(number)

View file

@ -7,28 +7,12 @@
<v-card class="mb-4"> <v-card class="mb-4">
<v-card-title class="d-flex align-center justify-space-between"> <v-card-title class="d-flex align-center justify-space-between">
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-btn <v-btn icon="mdi-arrow-right" variant="text" @click="$router.back()" class="ml-2"></v-btn>
icon="mdi-arrow-right"
variant="text"
@click="$router.back()"
class="ml-2"
></v-btn>
<v-icon class="ml-2" color="primary">mdi-import</v-icon> <v-icon class="ml-2" color="primary">mdi-import</v-icon>
<span>{{ workflow?.title || 'جزئیات پرونده واردات' }}</span> <span>{{ workflow?.title || 'جزئیات پرونده واردات' }}</span>
</div> </div>
<div class="d-flex align-center"> <div class="d-flex align-center">
<v-chip <v-btn color="primary" variant="outlined" prepend-icon="mdi-pencil" @click="editMode = !editMode">
:color="getStatusColor(workflow?.status)"
class="ml-2"
>
{{ getStatusText(workflow?.status) }}
</v-chip>
<v-btn
color="primary"
variant="outlined"
prepend-icon="mdi-pencil"
@click="editMode = !editMode"
>
{{ editMode ? 'لغو ویرایش' : 'ویرایش' }} {{ editMode ? 'لغو ویرایش' : 'ویرایش' }}
</v-btn> </v-btn>
<!-- <v-btn <!-- <v-btn
@ -61,119 +45,67 @@
<v-form v-if="editMode" ref="form" v-model="valid" validate-on="input"> <v-form v-if="editMode" ref="form" v-model="valid" validate-on="input">
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="editData.title" label="عنوان پرونده"
v-model="editData.title" :rules="[rules.required, rules.minLength]" counter="100"></v-text-field>
label="عنوان پرونده"
:rules="[rules.required, rules.minLength]"
counter="100"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="editData.supplierName" label="نام تامین کننده"
v-model="editData.supplierName" :rules="[rules.required, rules.minLength]" counter="100"></v-text-field>
label="نام تامین کننده"
:rules="[rules.required, rules.minLength]"
counter="100"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="editData.supplierCountry" label="کشور تامین کننده"
v-model="editData.supplierCountry" :rules="[rules.maxLength]" counter="50"></v-text-field>
label="کشور تامین کننده"
:rules="[rules.maxLength]"
counter="50"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12" md="6">
<v-text-field <v-text-field v-model="editData.supplierPhone" label="تلفن تامین کننده" :rules="[rules.phone]"
v-model="editData.supplierPhone" counter="20"></v-text-field>
label="تلفن تامین کننده"
:rules="[rules.phone]"
counter="20"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-text-field v-model="editData.supplierEmail" label="ایمیل تامین کننده" :rules="[rules.email]"
v-model="editData.supplierEmail" counter="100"></v-text-field>
label="ایمیل تامین کننده"
:rules="[rules.email]"
counter="100"
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-textarea v-model="editData.supplierAddress" label="آدرس تامین کننده" rows="2"
v-model="editData.supplierAddress" :rules="[rules.maxLength]" counter="500"></v-textarea>
label="آدرس تامین کننده"
rows="2"
:rules="[rules.maxLength]"
counter="500"
></v-textarea>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-text-field <v-select v-model="editData.currency" :items="currencyOptions" label="واحد پول"
class="ltr-input" :rules="[rules.required]"></v-select>
:model-value="formatMoney(editData.totalAmount)"
label="مبلغ کل (ارزی)"
type="text"
inputmode="numeric"
:rules="[rules.positiveMoney]"
@update:modelValue="onMoneyInput('totalAmount', $event)"
></v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="6">
<v-select <v-text-field class="ltr-input" :model-value="formatMoneyTyping(editData.exchangeRate)"
v-model="editData.currency" label="نرخ تبدیل (ریال)" type="text" inputmode="decimal" :rules="[rules.exchangeRateRule]"
:items="currencyOptions"
label="واحد پول"
:rules="[rules.required]"
></v-select>
</v-col>
<v-col cols="12" md="4">
<v-text-field
class="ltr-input"
:model-value="formatMoney(editData.exchangeRate)"
label="نرخ تبدیل (ریال)"
type="text"
inputmode="numeric"
:rules="[rules.exchangeRateRule]"
@update:modelValue="onMoneyInput('exchangeRate', $event)" @update:modelValue="onMoneyInput('exchangeRate', $event)"
></v-text-field> @blur="editData.exchangeRate = parseMoney(editData.exchangeRate)"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" md="6">
<v-text-field :model-value="formatMoney(computedTotalAmount)" label="مبلغ کل (ارزی) - محاسباتی"
readonly variant="outlined" color="primary"></v-text-field>
</v-col>
<v-col cols="12" md="6">
<v-text-field :model-value="formatMoney(computedTotalAmountIRR)" label="مبلغ کل (ریال) - محاسباتی"
readonly variant="outlined" color="primary"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-text-field <v-textarea v-model="editData.description" label="توضیحات" rows="3"></v-textarea>
:model-value="formatMoney(editData.totalAmountIRR)"
label="مبلغ کل (ریال)"
readonly
></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-textarea <v-btn color="primary" @click="saveChanges" :loading="saveLoading"
v-model="editData.description" :disabled="!isFormValidForSave">
label="توضیحات"
rows="3"
></v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-btn
color="primary"
@click="saveChanges"
:loading="saveLoading"
:disabled="!isFormValidForSave"
>
ذخیره تغییرات ذخیره تغییرات
</v-btn> </v-btn>
</v-col> </v-col>
@ -220,20 +152,41 @@
<v-row> <v-row>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<div class="mb-3"> <div class="mb-3">
<strong>مبلغ کل:</strong> <strong>مبلغ کل (ارزی):</strong>
<div>{{ formatMoney(workflow.totalAmount) }} {{ workflow.currency }}</div> <div>{{formatMoney(workflow.items?.reduce((total, item) => Number(total) +
Number(item.unitPrice * item.quantity || 0), 0)) }} {{ workflow.currency }}</div>
</div> </div>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<div class="mb-3"> <div class="mb-3">
<strong>نرخ تبدیل:</strong> <strong>نرخ تبدیل (ریال):</strong>
<div>{{ formatMoney(workflow.exchangeRate) }}</div> <div>{{ formatMoney(workflow.exchangeRate) }}</div>
</div> </div>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12" md="4">
<div class="mb-3"> <div class="mb-3">
<strong>مبلغ کل (ریال):</strong> <strong>مبلغ کل (ریال):</strong>
<div>{{ formatMoney(workflow.totalAmountIRR) }}</div> <div>{{formatMoney(workflow.items?.reduce((total, item) => Number(total) +
Number(item.unitPrice * item.quantity * workflow.exchangeRate || 0), 0)) }}</div>
</div>
</v-col>
<v-col cols="12" md="4">
<div class="mb-3">
<strong>هزینه های ترخیص (ریال):</strong>
<div>{{formatMoney(workflow.customs?.reduce((total, custom) => Number(total) +
Number(custom.totalCustomsCharges || 0), 0)) }}</div>
</div>
</v-col>
<v-col cols="12" md="4">
<div class="mb-3">
<strong>مجموعه پرداخت ها :</strong>
<div class="d-flex align-center gap-2">
<div>{{formatMoney(workflow.payments?.reduce((total, payment) => Number(total) +
Number(payment.amount || 0), 0)) }} {{ workflow.currency }}</div>
<div>|</div>
<div>{{formatMoney(workflow.payments?.reduce((total, payment) => Number(total) +
Number(payment.amount * workflow.exchangeRate || 0), 0)) }} ریال</div>
</div>
</div> </div>
</v-col> </v-col>
</v-row> </v-row>
@ -276,90 +229,64 @@
<v-col cols="12"> <v-col cols="12">
<v-card> <v-card>
<v-tabs v-model="activeTab" bg-color="primary"> <v-tabs v-model="activeTab" bg-color="primary">
<v-tab value="items">آیتمها <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.items?.length || 0 }}</v-chip></v-tab> <v-tab value="items">آیتمها <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
<v-tab value="payments">پرداختها <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.payments?.length || 0 }}</v-chip></v-tab> style="color: white !important;">{{ workflow.items?.length || 0 }}</v-chip></v-tab>
<v-tab value="documents">اسناد <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.documents?.length || 0 }}</v-chip></v-tab> <v-tab value="payments">پرداختها <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
<v-tab value="stages">مراحل <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.stages?.length || 0 }}</v-chip></v-tab> style="color: white !important;">{{ workflow.payments?.length || 0 }}</v-chip></v-tab>
<v-tab value="shipping">حمل و نقل <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.shipping?.length || 0 }}</v-chip></v-tab> <v-tab value="documents">اسناد <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
<v-tab value="customs">ترخیص <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.customs?.length || 0 }}</v-chip></v-tab> style="color: white !important;">{{ workflow.documents?.length || 0 }}</v-chip></v-tab>
<v-tab value="stages">مراحل <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
style="color: white !important;">{{ workflow.stages?.length || 0 }}</v-chip></v-tab>
<v-tab value="shipping">حمل و نقل <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
style="color: white !important;">{{ workflow.shipping?.length || 0 }}</v-chip></v-tab>
<v-tab value="customs">ترخیص <v-chip size="small" color="secondary" variant="tonal" class="ms-2"
style="color: white !important;">{{ workflow.customs?.length || 0 }}</v-chip></v-tab>
<!-- <v-tab value="tickets">حوالههای مرتبط</v-tab> --> <!-- <v-tab value="tickets">حوالههای مرتبط</v-tab> -->
</v-tabs> </v-tabs>
<v-tabs-window v-model="activeTab"> <v-tabs-window v-model="activeTab">
<v-tabs-window-item value="items"> <v-tabs-window-item value="items">
<ImportWorkflowItems <ImportWorkflowItems :workflow-id="workflowId" :items="workflow.items" :currency="workflow.currency"
:workflow-id="workflowId" :exchange-rate="workflow.exchangeRate" @updated="loadWorkflow" />
:items="workflow.items"
:currency="workflow.currency"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="payments"> <v-tabs-window-item value="payments">
<ImportWorkflowPayments <ImportWorkflowPayments :workflow-id="workflowId" :payments="workflow.payments"
:workflow-id="workflowId" :currency="workflow.currency" :exchange-rate="workflow.exchangeRate" @updated="loadWorkflow" />
:payments="workflow.payments"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="documents"> <v-tabs-window-item value="documents">
<ImportWorkflowDocuments <ImportWorkflowDocuments :workflow-id="workflowId" :documents="workflow.documents"
:workflow-id="workflowId" @updated="loadWorkflow" />
:documents="workflow.documents"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="stages"> <v-tabs-window-item value="stages">
<ImportWorkflowStages <ImportWorkflowStages :workflow-id="workflowId" :stages="workflow.stages" @updated="loadWorkflow" />
:workflow-id="workflowId"
:stages="workflow.stages"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="shipping"> <v-tabs-window-item value="shipping">
<ImportWorkflowShipping <ImportWorkflowShipping :workflow-id="workflowId" :shipping="workflow.shipping"
:workflow-id="workflowId" @updated="loadWorkflow" />
:shipping="workflow.shipping"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="customs"> <v-tabs-window-item value="customs">
<ImportWorkflowCustoms <ImportWorkflowCustoms :workflow-id="workflowId" :customs="workflow.customs"
:workflow-id="workflowId" @updated="loadWorkflow" />
:customs="workflow.customs"
@updated="loadWorkflow"
/>
</v-tabs-window-item> </v-tabs-window-item>
<v-tabs-window-item value="tickets"> <v-tabs-window-item value="tickets">
<v-card flat> <v-card flat>
<v-card-text> <v-card-text>
<div class="d-flex align-center mb-4 gap-2"> <div class="d-flex align-center mb-4 gap-2">
<v-select <v-select v-model="ticketsStatusFilter" :items="ticketStatusOptions" label="فیلتر وضعیت"
v-model="ticketsStatusFilter" style="max-width: 260px" clearable density="compact" variant="outlined"
:items="ticketStatusOptions" @update:model-value="loadRelatedTickets" />
label="فیلتر وضعیت"
style="max-width: 260px"
clearable
density="compact"
variant="outlined"
@update:model-value="loadRelatedTickets"
/>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="primary" variant="text" icon="mdi-refresh" @click="loadRelatedTickets" :loading="loadingTickets" /> <v-btn color="primary" variant="text" icon="mdi-refresh" @click="loadRelatedTickets"
:loading="loadingTickets" />
</div> </div>
<v-data-table <v-data-table :headers="ticketsHeaders" :header-props="{ class: 'custom-header' }"
:headers="ticketsHeaders" :items="relatedTickets" :loading="loadingTickets" density="comfortable" class="elevation-1">
:header-props="{ class: 'custom-header' }"
:items="relatedTickets"
:loading="loadingTickets"
density="comfortable"
class="elevation-1"
>
<template #item.code="{ item }"> <template #item.code="{ item }">
<v-chip color="secondary" variant="tonal" size="small">{{ item.code }}</v-chip> <v-chip color="secondary" variant="tonal" size="small">{{ item.code }}</v-chip>
</template> </template>
@ -369,12 +296,8 @@
</v-chip> </v-chip>
</template> </template>
<template #item.actions="{ item }"> <template #item.actions="{ item }">
<v-btn <v-btn color="primary" variant="text" size="small"
color="primary" @click="$router.push({ name: 'storeroom_ticket_view', params: { id: item.code } })">
variant="text"
size="small"
@click="$router.push({ name: 'storeroom_ticket_view', params: { id: item.code } })"
>
مشاهده مشاهده
</v-btn> </v-btn>
<v-menu> <v-menu>
@ -384,7 +307,8 @@
</v-btn> </v-btn>
</template> </template>
<v-list> <v-list>
<v-list-item v-for="st in ticketStatusOptions" :key="st.value" @click="updateTicketStatus(item.code, st.value)" :disabled="!st.value"> <v-list-item v-for="st in ticketStatusOptions" :key="st.value"
@click="updateTicketStatus(item.code, st.value)" :disabled="!st.value">
<v-list-item-title>{{ st.title }}</v-list-item-title> <v-list-item-title>{{ st.title }}</v-list-item-title>
</v-list-item> </v-list-item>
</v-list> </v-list>
@ -407,47 +331,27 @@
<v-card-text> <v-card-text>
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-select <v-select v-model="selectedStoreroomId" :items="storerooms" item-title="name" item-value="id"
v-model="selectedStoreroomId" label="انبار" :loading="loadingStorerooms" :disabled="loadingStorerooms" variant="outlined"
:items="storerooms" density="compact" required />
item-title="name"
item-value="id"
label="انبار"
:loading="loadingStorerooms"
:disabled="loadingStorerooms"
variant="outlined"
density="compact"
required
/>
</v-col> </v-col>
<v-col cols="12"> <v-col cols="12">
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<v-text-field <v-text-field v-model="personSearch" label="جستجوی طرف‌حساب" variant="outlined" density="compact"
v-model="personSearch" hide-details />
label="جستجوی طرف‌حساب"
variant="outlined"
density="compact"
hide-details
/>
<v-btn color="primary" @click="searchPersons" :loading="loadingPersons">جستجو</v-btn> <v-btn color="primary" @click="searchPersons" :loading="loadingPersons">جستجو</v-btn>
</div> </div>
<v-select <v-select class="mt-3" v-model="selectedPersonId" :items="persons" item-title="nikename" item-value="id"
class="mt-3" label="انتخاب طرف‌حساب" variant="outlined" density="compact" />
v-model="selectedPersonId"
:items="persons"
item-title="nikename"
item-value="id"
label="انتخاب طرف‌حساب"
variant="outlined"
density="compact"
/>
</v-col> </v-col>
</v-row> </v-row>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="showCreateTicketDialog = false">انصراف</v-btn> <v-btn @click="showCreateTicketDialog = false">انصراف</v-btn>
<v-btn color="success" @click="createInboundTicket" :loading="creatingTicket" :disabled="!canCreateTicket">ایجاد حواله</v-btn> <v-btn color="success" @click="createInboundTicket" :loading="creatingTicket"
:disabled="!canCreateTicket">ایجاد
حواله</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-dialog> </v-dialog>
@ -624,14 +528,32 @@ const rules = {
} }
} }
// Watch for total amount and exchange rate changes const computedTotalAmount = computed(() => {
if (!workflow.value?.items) return 0
return workflow.value.items.reduce((total, item) => {
return total + parseMoney(item.unitPrice * item.quantity)
}, 0)
})
const computedTotalAmountIRR = computed(() => {
if (!workflow.value?.items) return 0
const exchangeRate =
parseFloat(editData.value?.exchangeRate) ||
parseFloat(workflow.value?.exchangeRate) ||
1
return workflow.value.items.reduce((total, item) => {
const itemTotalPrice = parseMoney(item.unitPrice * item.quantity)
return total + itemTotalPrice * exchangeRate
}, 0)
})
// Watch for exchange rate changes to trigger recomputation
watch( watch(
() => [editData.value.totalAmount, editData.value.exchangeRate, editData.value.currency], () => [editData.value.exchangeRate, editData.value.currency],
([totalAmount, exchangeRate, currency]) => { ([exchangeRate, currency]) => {
const total = parseFloat(totalAmount) || 0 // مبلغ کل ریالی به صورت خودکار محاسبه میشود
const computedRate = currency === 'IRR' ? 1 : (parseFloat(exchangeRate) || 0) // نیازی به بهروزرسانی دستی نیست
const result = Math.round(total * computedRate)
editData.value.totalAmountIRR = isNaN(result) ? 0 : result
}, },
{ immediate: true } { immediate: true }
) )
@ -646,11 +568,8 @@ const loadWorkflow = async () => {
workflow.value = response.data.Result workflow.value = response.data.Result
editData.value = { ...response.data.Result } editData.value = { ...response.data.Result }
// Trigger the watch manually after setting editData // مبلغ کل به صورت محاسباتی از اقلام محاسبه میشود
// مقدار ریالی بر اساس مبلغ و نرخ تبدیل/واحد پول محاسبه میشود // نیازی به محاسبه دستی نیست
const total = parseFloat(editData.value.totalAmount) || 0
const rate = editData.value.currency === 'IRR' ? 1 : (parseFloat(editData.value.exchangeRate) || 0)
editData.value.totalAmountIRR = Math.round(total * rate)
} else { } else {
throw new Error(response.data.ErrorMessage) throw new Error(response.data.ErrorMessage)
} }
@ -701,7 +620,7 @@ const parseMoneyInput = (val) => {
if (val === null || val === undefined) return 0 if (val === null || val === undefined) return 0
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '') const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
const num = Number(cleaned) const num = Number(cleaned)
return Number.isFinite(num) ? num : 0 return Number.isFinite(num) ? parseFloat(num.toFixed(2)) : 0
} }
// دکمه ذخیره تنها زمانی فعال شود که فیلدهای کلیدی معتبر باشند // دکمه ذخیره تنها زمانی فعال شود که فیلدهای کلیدی معتبر باشند
@ -709,18 +628,11 @@ const isFormValidForSave = computed(() => {
const titleOk = typeof editData.value.title === 'string' && editData.value.title.trim().length >= 3 const titleOk = typeof editData.value.title === 'string' && editData.value.title.trim().length >= 3
const supplierOk = typeof editData.value.supplierName === 'string' && editData.value.supplierName.trim().length >= 3 const supplierOk = typeof editData.value.supplierName === 'string' && editData.value.supplierName.trim().length >= 3
const currencyOk = !!editData.value.currency const currencyOk = !!editData.value.currency
const total = parseMoneyInput(editData.value.totalAmount)
const rate = parseMoneyInput(editData.value.exchangeRate) const rate = parseMoneyInput(editData.value.exchangeRate)
const totalOk = total >= 0
const rateOk = editData.value.currency === 'IRR' ? rate >= 0 : rate > 0 const rateOk = editData.value.currency === 'IRR' ? rate >= 0 : rate > 0
return titleOk && supplierOk && currencyOk && totalOk && rateOk && valid.value return titleOk && supplierOk && currencyOk && rateOk && valid.value
}) })
const onMoneyInput = (field, value) => {
const numeric = parseMoneyInput(value)
editData.value[field] = numeric
}
const getStatusColor = (status) => { const getStatusColor = (status) => {
const colors = { const colors = {
draft: 'grey', draft: 'grey',
@ -734,32 +646,39 @@ const getStatusColor = (status) => {
return colors[status] || 'grey' return colors[status] || 'grey'
} }
const getStatusText = (status) => {
const texts = {
draft: 'پیش‌نویس',
processing: 'در حال پردازش',
shipped: 'ارسال شده',
arrived: 'رسیده',
cleared: 'ترخیص شده',
completed: 'تکمیل شده',
cancelled: 'لغو شده'
}
return texts[status] || status
}
const formatNumber = (number) => { const formatNumber = (number) => {
if (!number) return '0' if (!number) return '0'
return new Intl.NumberFormat('fa-IR').format(number) return new Intl.NumberFormat('fa-IR').format(number)
} }
// نمایش مبالغ بدون اعشار و با جداکننده ویرگول بین هر سه رقم // نمایش مبالغ با اعشار و با جداکننده ویرگول بین هر سه رقم
const formatMoney = (value) => { const formatMoney = (value) => {
const numericValue = Number(value) || 0 const numericValue = Number(value) || 0
return numericValue return numericValue
.toFixed(0) .toFixed(2)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',') .replace(/\B(?=(\d{3})+(?!\d))/g, ',')
} }
const parseMoney = (val) => {
if (val === null || val === undefined || val === '') return 0
const clean = String(val).replace(/,/g, '')
const num = parseFloat(clean)
return isNaN(num) ? 0 : parseFloat(num.toFixed(2))
}
const formatMoneyTyping = (val) => {
if (val === null || val === undefined || val === '') return ''
const str = String(val).replace(/,/g, '')
if (str === '') return ''
const parts = str.split('.')
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',')
return parts.join('.')
}
const onMoneyInput = (field, val) => {
editData.value[field] = parseMoney(val)
}
const formatDate = (date) => { const formatDate = (date) => {
if (!date) return '-' if (!date) return '-'
return new Date(date).toLocaleDateString('fa-IR') return new Date(date).toLocaleDateString('fa-IR')
@ -815,7 +734,3 @@ onMounted(async () => {
text-align: left !important; text-align: left !important;
} }
</style> </style>

View file

@ -1,129 +1,24 @@
<template> <template>
<v-toolbar color="toolbar" :title="$t('dialog.warranty_page_title')">
<template v-slot:prepend>
<v-tooltip :text="$t('dialog.back')" location="bottom">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" @click="$router.back()" class="d-none d-sm-flex" variant="text"
icon="mdi-arrow-right" />
</template>
</v-tooltip>
</template>
<v-spacer></v-spacer>
<v-btn color="info" variant="outlined" prepend-icon="mdi-share-variant" @click="openActivationLinkDialog"
class="ml-2">
اشتراک گذاری لینک فعالسازی
</v-btn>
<v-btn color="primary" variant="outlined" prepend-icon="mdi-cog" @click="goToWarrantySettings">
تنظیمات گارانتی
</v-btn>
</v-toolbar>
<div class="warranty-plugin"> <div class="warranty-plugin">
<v-container fluid> <v-container fluid>
<!-- <v-row>
<v-col cols="6" sm="6" md="3">
<div class="stats-card total-card">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-barcode-scan</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-barcode-scan</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">{{ stats.totalSerials || 0 }}</div>
<div class="stats-label">کل سریالها</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('available')" @click="filterByStatus('available')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-check-circle</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-check-circle</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.available || 0 }}
</div>
<div class="stats-label">آزاد</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('allocated')" @click="filterByStatus('allocated')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-package-variant-closed</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-package-variant-closed</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.allocated || 0 }}
</div>
<div class="stats-label">تخصیص یافته</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('verified')" @click="filterByStatus('verified')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-clipboard-check</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-clipboard-check</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.verified || 0 }}
</div>
<div class="stats-label">تأیید شده</div>
</div>
</div>
</v-col>
</v-row>
<v-row class="mt-2">
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('bound')" @click="filterByStatus('bound')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-link-variant</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-link-variant</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.bound || 0 }}
</div>
<div class="stats-label">متصل</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('consumed')" @click="filterByStatus('consumed')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-check-decagram</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-check-decagram</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.consumed || 0 }}
</div>
<div class="stats-label">مصرف شده</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div :class="getCardClasses('void')" @click="filterByStatus('void')" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-cancel</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-cancel</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.byStatus.void || 0 }}
</div>
<div class="stats-label">باطل</div>
</div>
</div>
</v-col>
<v-col cols="6" sm="6" md="3">
<div class="stats-card expired-card" role="button" tabindex="0">
<div class="stats-icon">
<v-icon size="24" color="white" class="d-sm-none">mdi-clock-alert</v-icon>
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-clock-alert</v-icon>
</div>
<div class="stats-content">
<div class="stats-number">
<v-progress-circular v-if="statsLoading" indeterminate size="20" color="white" class="me-2" />
{{ stats.expiredFlagCount || 0 }}
</div>
<div class="stats-label">دارای پایان گارانتی گذشته</div>
</div>
</div>
</v-col>
</v-row> -->
<v-row> <v-row>
<v-col cols="12"> <v-col cols="12">
<v-card> <v-card>
@ -258,6 +153,60 @@
<BulkImportDialog v-model="showBulkImportDialog" :commodities="commodities" @import="bulkImport" <BulkImportDialog v-model="showBulkImportDialog" :commodities="commodities" @import="bulkImport"
@close="closeBulkImportDialog" /> @close="closeBulkImportDialog" />
<!-- Activation Link Dialog -->
<v-dialog v-model="showActivationLinkDialog" max-width="600">
<v-card>
<v-card-title class="d-flex align-center" style="padding: 20px !important;">
<v-icon class="ml-2" color="info">mdi-share-variant</v-icon>
اشتراک گذاری لینک فعالسازی گارانتی
</v-card-title>
<v-card-text>
<v-alert type="info" variant="tonal" class="mb-4">
<strong>نکته:</strong> این لینک برای فعالسازی گارانتی توسط مشتریان استفاده میشود.
</v-alert>
<div class="mb-4">
<label class="text-body-2 font-weight-medium mb-2 d-block">لینک فعالسازی گارانتی:</label>
<div class="d-flex align-center flex-row-reverse gap-2">
<v-text-field :model-value="activationLink" readonly variant="outlined" density="compact"
class="flex-grow-1 text-left" hide-details></v-text-field>
<v-btn color="primary" variant="tonal" @click="copyActivationLink" class="ml-2" :loading="copying">
<v-icon>mdi-content-copy</v-icon>
</v-btn>
</div>
</div>
<div class="mb-4">
<h4 class="text-h6 mb-3">راهنمای استفاده:</h4>
<v-list density="compact">
<v-list-item>
<template #prepend>
<v-icon color="primary" size="small">mdi-numeric-1-circle</v-icon>
</template>
<v-list-item-title>این لینک را برای مشتریان ارسال کنید</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon color="primary" size="small">mdi-numeric-2-circle</v-icon>
</template>
<v-list-item-title>مشتری با مراجعه به لینک میتواند گارانتی خود را فعال کند</v-list-item-title>
</v-list-item>
<v-list-item>
<template #prepend>
<v-icon color="primary" size="small">mdi-numeric-3-circle</v-icon>
</template>
<v-list-item-title>پس از فعالسازی، وضعیت گارانتی در سیستم بهروزرسانی میشود</v-list-item-title>
</v-list-item>
</v-list>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn @click="showActivationLinkDialog = false">بستن</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
<v-dialog v-model="showDeleteDialog" max-width="400"> <v-dialog v-model="showDeleteDialog" max-width="400">
<v-card> <v-card>
<v-card-title>تأیید حذف</v-card-title> <v-card-title>تأیید حذف</v-card-title>
@ -286,8 +235,6 @@
</v-card> </v-card>
</v-dialog> </v-dialog>
<!-- تنظیمات گارانتی به تب تنظیمات کسبوکار منتقل شد -->
<v-snackbar v-model="showSnackbar" :color="snackbarColor" :timeout="3000" location="bottom" class="rounded-lg" <v-snackbar v-model="showSnackbar" :color="snackbarColor" :timeout="3000" location="bottom" class="rounded-lg"
elevation="2"> elevation="2">
<div class="d-flex align-center"> <div class="d-flex align-center">
@ -350,7 +297,9 @@ const showAddDialog = ref(false)
const showViewDialog = ref(false) const showViewDialog = ref(false)
const showDeleteDialog = ref(false) const showDeleteDialog = ref(false)
const showBulkImportDialog = ref(false) const showBulkImportDialog = ref(false)
const showActivationLinkDialog = ref(false)
const selectedSerial = ref<any>(null) const selectedSerial = ref<any>(null)
const copying = ref(false)
const settings = ref({ const settings = ref({
requireWarrantyOnDelivery: false, requireWarrantyOnDelivery: false,
activationGraceDays: 0, activationGraceDays: 0,
@ -724,6 +673,35 @@ const formatDate = (dateVal: any) => {
} }
} }
const activationLink = ref('')
const businessId = ref(localStorage.getItem('activeBid') || '')
const generateActivationLink = () => {
const baseUrl = window.location.origin + '/u/public/' + businessId.value + '/warranty-activation'
activationLink.value = baseUrl
}
const copyActivationLink = async () => {
try {
copying.value = true
await navigator.clipboard.writeText(activationLink.value)
showNotification('لینک با موفقیت کپی شد', 'success')
} catch (err) {
showNotification('خطا در کپی کردن لینک', 'error')
} finally {
copying.value = false
}
}
const goToWarrantySettings = () => {
router.push('/acc/business/settings')
}
const openActivationLinkDialog = () => {
generateActivationLink()
showActivationLinkDialog.value = true
}
onMounted(async () => { onMounted(async () => {
await Promise.all([loadSerials(), loadCommodities()]) await Promise.all([loadSerials(), loadCommodities()])
// await loadStats() // await loadStats()

View file

@ -524,7 +524,7 @@ onMounted(() => {
:headers="[ :headers="[
{ title: 'سریال گارانتی', key: 'serialNumber' }, { title: 'سریال گارانتی', key: 'serialNumber' },
{ title: 'کالا', key: 'commodity' }, { title: 'کالا', key: 'commodity' },
{ title: 'وضعیت', key: 'status' }, // { title: 'وضعیت', key: 'status' },
{ title: 'فعال‌سازی', key: 'activation' }, { title: 'فعال‌سازی', key: 'activation' },
// { title: 'کد فعالسازی', key: 'activationTicketCode' }, // { title: 'کد فعالسازی', key: 'activationTicketCode' },
{ title: 'اتمام گارانتی', key: 'warrantyEndDate' } { title: 'اتمام گارانتی', key: 'warrantyEndDate' }

View file

@ -56,6 +56,9 @@ export default defineComponent({
chequeInput: '', chequeInput: '',
passChequeInput: '', passChequeInput: '',
rejectChequeInput: '' rejectChequeInput: ''
},
plugWarranty: {
sendSerial: ''
} }
} }
} }
@ -236,6 +239,14 @@ export default defineComponent({
v-model="form.plugRepservice.creating" type="text" prepend-inner-icon="mdi-card-text" v-model="form.plugRepservice.creating" type="text" prepend-inner-icon="mdi-card-text"
:rules="[() => Number(form.plugRepservice.creating) > 0 || $t('validator.required')]"></v-text-field> :rules="[() => Number(form.plugRepservice.creating) > 0 || $t('validator.required')]"></v-text-field>
</v-col> </v-col>
</v-row>
<h4 class="text-primary">افزونه گارانتی</h4>
<v-row class="mb-2">
<v-col cols="12" sm="12" md="4">
<v-text-field class="" hide-details="auto" :label="$t('pages.manager.sms_settings_warranty_send_serial')"
v-model="form.plugWarranty.sendSerial" type="text" prepend-inner-icon="mdi-card-text"
:rules="[() => Number(form.plugWarranty.sendSerial) > 0 || $t('validator.required')]"></v-text-field>
</v-col>
<v-col cols="12" sm="12" md="12"> <v-col cols="12" sm="12" md="12">
<v-btn type="submit" @click="submit()" color="primary" prepend-icon="mdi-content-save" :loading="loading"> <v-btn type="submit" @click="submit()" color="primary" prepend-icon="mdi-content-save" :loading="loading">
{{ $t('dialog.save') }} {{ $t('dialog.save') }}