Add Warranty & ImportWorkflow plugins/ add two-step approval for docs in accpro
This commit is contained in:
parent
ded4cff458
commit
aee56d5548
|
@ -16,6 +16,7 @@
|
|||
"doctrine/orm": "^3.2",
|
||||
"dompdf/dompdf": "^3.0",
|
||||
"melipayamak/php": "^1.0",
|
||||
"morilog/jalali": "*",
|
||||
"mpdf/mpdf": "^8.2",
|
||||
"nelmio/api-doc-bundle": "^4.35",
|
||||
"nelmio/cors-bundle": "^2.5",
|
||||
|
|
308
hesabixCore/composer.lock
generated
308
hesabixCore/composer.lock
generated
|
@ -4,8 +4,75 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "43db0ad2bb94569ed6d44cabf503210e",
|
||||
"content-hash": "01b5daf5a6fd011b4eb616e0e4ae18fe",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/assert",
|
||||
"version": "v3.3.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/beberlei/assert.git",
|
||||
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd",
|
||||
"reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-ctype": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-simplexml": "*",
|
||||
"php": "^7.1 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "*",
|
||||
"phpstan/phpstan": "*",
|
||||
"phpunit/phpunit": ">=6.0.0",
|
||||
"yoast/phpunit-polyfills": "^0.1.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"lib/Assert/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Assert\\": "lib/Assert"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de",
|
||||
"role": "Lead Developer"
|
||||
},
|
||||
{
|
||||
"name": "Richard Quadling",
|
||||
"email": "rquadling@gmail.com",
|
||||
"role": "Collaborator"
|
||||
}
|
||||
],
|
||||
"description": "Thin assertion library for input validation in business models.",
|
||||
"keywords": [
|
||||
"assert",
|
||||
"assertion",
|
||||
"validation"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/beberlei/assert/issues",
|
||||
"source": "https://github.com/beberlei/assert/tree/v3.3.3"
|
||||
},
|
||||
"time": "2024-07-15T13:18:35+00:00"
|
||||
},
|
||||
{
|
||||
"name": "brick/math",
|
||||
"version": "0.12.3",
|
||||
|
@ -66,6 +133,75 @@
|
|||
],
|
||||
"time": "2025-02-28T13:11:00+00:00"
|
||||
},
|
||||
{
|
||||
"name": "carbonphp/carbon-doctrine-types",
|
||||
"version": "3.2.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
|
||||
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
|
||||
"reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/dbal": "<4.0.0 || >=5.0.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^4.0.0",
|
||||
"nesbot/carbon": "^2.71.0 || ^3.0.0",
|
||||
"phpunit/phpunit": "^10.3"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "KyleKatarn",
|
||||
"email": "kylekatarnls@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Types to use Carbon in Doctrine",
|
||||
"keywords": [
|
||||
"carbon",
|
||||
"date",
|
||||
"datetime",
|
||||
"doctrine",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
|
||||
"source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/kylekatarnls",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/Carbon",
|
||||
"type": "open_collective"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-02-09T16:56:22+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/pcre",
|
||||
"version": "3.3.2",
|
||||
|
@ -2297,6 +2433,71 @@
|
|||
],
|
||||
"time": "2025-03-24T10:02:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "morilog/jalali",
|
||||
"version": "v3.4.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/morilog/jalali.git",
|
||||
"reference": "f475f4db7bd540c6abc01126e46824c897ed1e03"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/morilog/jalali/zipball/f475f4db7bd540c6abc01126e46824c897ed1e03",
|
||||
"reference": "f475f4db7bd540c6abc01126e46824c897ed1e03",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"beberlei/assert": "^3.0",
|
||||
"nesbot/carbon": "^1.21 || ^2.0 || ^3.0",
|
||||
"php": "^7.0 | ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": ">4.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"Morilog\\Jalali\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Milad Rey",
|
||||
"email": "miladr@gmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Morteza Parvini",
|
||||
"email": "m.parvini@outlook.com"
|
||||
}
|
||||
],
|
||||
"description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.",
|
||||
"keywords": [
|
||||
"Jalali",
|
||||
"date",
|
||||
"datetime",
|
||||
"laravel",
|
||||
"morilog"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/morilog/jalali/issues",
|
||||
"source": "https://github.com/morilog/jalali/tree/v3.4.2"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://issuehunt.io/r/morilog",
|
||||
"type": "issuehunt"
|
||||
}
|
||||
],
|
||||
"time": "2024-05-09T08:44:51+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mpdf/mpdf",
|
||||
"version": "v8.2.5",
|
||||
|
@ -2714,6 +2915,111 @@
|
|||
},
|
||||
"time": "2024-06-24T21:25:28+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "3.10.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/CarbonPHP/carbon.git",
|
||||
"reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24",
|
||||
"reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"carbonphp/carbon-doctrine-types": "<100.0",
|
||||
"ext-json": "*",
|
||||
"php": "^8.1",
|
||||
"psr/clock": "^1.0",
|
||||
"symfony/clock": "^6.3.12 || ^7.0",
|
||||
"symfony/polyfill-mbstring": "^1.0",
|
||||
"symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0"
|
||||
},
|
||||
"provide": {
|
||||
"psr/clock-implementation": "1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/dbal": "^3.6.3 || ^4.0",
|
||||
"doctrine/orm": "^2.15.2 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.75.0",
|
||||
"kylekatarnls/multi-tester": "^2.5.3",
|
||||
"phpmd/phpmd": "^2.15.0",
|
||||
"phpstan/extension-installer": "^1.4.3",
|
||||
"phpstan/phpstan": "^2.1.17",
|
||||
"phpunit/phpunit": "^10.5.46",
|
||||
"squizlabs/php_codesniffer": "^3.13.0"
|
||||
},
|
||||
"bin": [
|
||||
"bin/carbon"
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Carbon\\Laravel\\ServiceProvider"
|
||||
]
|
||||
},
|
||||
"phpstan": {
|
||||
"includes": [
|
||||
"extension.neon"
|
||||
]
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-2.x": "2.x-dev",
|
||||
"dev-master": "3.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Carbon\\": "src/Carbon/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Brian Nesbitt",
|
||||
"email": "brian@nesbot.com",
|
||||
"homepage": "https://markido.com"
|
||||
},
|
||||
{
|
||||
"name": "kylekatarnls",
|
||||
"homepage": "https://github.com/kylekatarnls"
|
||||
}
|
||||
],
|
||||
"description": "An API extension for DateTime that supports 281 different languages.",
|
||||
"homepage": "https://carbon.nesbot.com",
|
||||
"keywords": [
|
||||
"date",
|
||||
"datetime",
|
||||
"time"
|
||||
],
|
||||
"support": {
|
||||
"docs": "https://carbon.nesbot.com/docs",
|
||||
"issues": "https://github.com/CarbonPHP/carbon/issues",
|
||||
"source": "https://github.com/CarbonPHP/carbon"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/sponsors/kylekatarnls",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://opencollective.com/Carbon#sponsor",
|
||||
"type": "opencollective"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-08-02T09:36:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nikic/php-parser",
|
||||
"version": "v5.4.0",
|
||||
|
|
40
hesabixCore/migrations/Version20250113000000.php
Normal file
40
hesabixCore/migrations/Version20250113000000.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?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 Version20250113000000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add approval fields to HesabdariDoc and HesabdariRow tables';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Add approval fields to HesabdariDoc table
|
||||
$this->addSql('ALTER TABLE hesabdari_doc ADD is_preview TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE hesabdari_doc ADD is_approved TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE hesabdari_doc ADD approved_by_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE hesabdari_doc ADD CONSTRAINT FK_HESABDARI_DOC_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)');
|
||||
$this->addSql('CREATE INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc (approved_by_id)');
|
||||
|
||||
// Set default values for existing documents
|
||||
$this->addSql('UPDATE hesabdari_doc SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Remove approval fields from HesabdariDoc table
|
||||
$this->addSql('ALTER TABLE hesabdari_doc DROP FOREIGN KEY FK_HESABDARI_DOC_APPROVED_BY');
|
||||
$this->addSql('DROP INDEX IDX_HESABDARI_DOC_APPROVED_BY ON hesabdari_doc');
|
||||
$this->addSql('ALTER TABLE hesabdari_doc DROP is_preview, DROP is_approved, DROP approved_by_id');
|
||||
}
|
||||
}
|
31
hesabixCore/migrations/Version20250113000001.php
Normal file
31
hesabixCore/migrations/Version20250113000001.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?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 Version20250113000001 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Remove status field from StoreroomTicket table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Remove status field from storeroom_ticket table
|
||||
$this->addSql('ALTER TABLE storeroom_ticket DROP status');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Add status field back to storeroom_ticket table
|
||||
$this->addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL');
|
||||
}
|
||||
}
|
40
hesabixCore/migrations/Version20250113000002.php
Normal file
40
hesabixCore/migrations/Version20250113000002.php
Normal file
|
@ -0,0 +1,40 @@
|
|||
<?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 Version20250113000002 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add approval fields to StoreroomTicket table';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// Add approval fields to storeroom_ticket table
|
||||
$this->addSql('ALTER TABLE storeroom_ticket ADD is_preview TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE storeroom_ticket ADD is_approved TINYINT(1) DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE storeroom_ticket ADD approved_by_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE storeroom_ticket ADD CONSTRAINT FK_STOREROOM_TICKET_APPROVED_BY FOREIGN KEY (approved_by_id) REFERENCES user (id)');
|
||||
$this->addSql('CREATE INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket (approved_by_id)');
|
||||
|
||||
// Set default values for existing tickets
|
||||
$this->addSql('UPDATE storeroom_ticket SET is_preview = 0, is_approved = 1 WHERE is_preview IS NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// Remove approval fields from storeroom_ticket table
|
||||
$this->addSql('ALTER TABLE storeroom_ticket DROP FOREIGN KEY FK_STOREROOM_TICKET_APPROVED_BY');
|
||||
$this->addSql('DROP INDEX IDX_STOREROOM_TICKET_APPROVED_BY ON storeroom_ticket');
|
||||
$this->addSql('ALTER TABLE storeroom_ticket DROP is_preview, DROP is_approved, DROP approved_by_id');
|
||||
}
|
||||
}
|
101
hesabixCore/migrations/Version20250815143325.php
Normal file
101
hesabixCore/migrations/Version20250815143325.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?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 Version20250815143325 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 hesabdari_row DROP FOREIGN KEY FK_83B2C6EC2D234F6A
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE hesabdari_row DROP is_preview, DROP is_approved, DROP approved_by_id
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F4D9866B8
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD allocated_to_document_id INT DEFAULT NULL, ADD allocated_at DATETIME DEFAULT NULL, ADD bound_to_item_id INT DEFAULT NULL, ADD bound_at DATETIME DEFAULT NULL, DROP used, DROP used_at, CHANGE date_submit date_submit DATETIME NOT NULL, CHANGE description description LONGTEXT DEFAULT NULL, CHANGE warranty_start_date warranty_start_date DATETIME DEFAULT NULL, CHANGE warranty_end_date warranty_end_date DATETIME DEFAULT NULL, CHANGE status status VARCHAR(20) NOT NULL, CHANGE notes notes LONGTEXT DEFAULT NULL, CHANGE used_ticket_code void_reason VARCHAR(255) DEFAULT NULL, CHANGE bid_id business_id INT NOT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26FA89DB457 FOREIGN KEY (business_id) REFERENCES business (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial (business_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX idx_status_product ON plug_warranty_serial (status, commodity_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX idx_alloc_doc ON plug_warranty_serial (allocated_to_document_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial RENAME INDEX uniq_1a5dc26fd948ee2 TO uniq_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket RENAME INDEX idx_storeroom_ticket_approved_by TO IDX_9B4CC0F72D234F6A
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE hesabdari_row ADD is_preview TINYINT(1) DEFAULT NULL, ADD is_approved TINYINT(1) DEFAULT NULL, ADD approved_by_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE hesabdari_row ADD CONSTRAINT FK_83B2C6EC2D234F6A FOREIGN KEY (approved_by_id) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_83B2C6EC2D234F6A ON hesabdari_row (approved_by_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26FA89DB457
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_1A5DC26FA89DB457 ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX idx_status_product ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX idx_alloc_doc ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL, ADD used_at VARCHAR(50) DEFAULT NULL, DROP allocated_to_document_id, DROP allocated_at, DROP bound_to_item_id, DROP bound_at, CHANGE date_submit date_submit VARCHAR(25) NOT NULL, CHANGE description description VARCHAR(255) DEFAULT NULL, CHANGE warranty_start_date warranty_start_date VARCHAR(25) DEFAULT NULL, CHANGE warranty_end_date warranty_end_date VARCHAR(25) DEFAULT NULL, CHANGE status status VARCHAR(50) DEFAULT NULL, CHANGE notes notes VARCHAR(255) DEFAULT NULL, CHANGE business_id bid_id INT NOT NULL, CHANGE void_reason used_ticket_code VARCHAR(255) DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F4D9866B8 FOREIGN KEY (bid_id) REFERENCES business (id) ON UPDATE NO ACTION ON DELETE NO ACTION
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_1A5DC26F4D9866B8 ON plug_warranty_serial (bid_id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial RENAME INDEX uniq_warranty_serial TO UNIQ_1A5DC26FD948EE2
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE storeroom_ticket RENAME INDEX idx_9b4cc0f72d234f6a TO IDX_STOREROOM_TICKET_APPROVED_BY
|
||||
SQL);
|
||||
}
|
||||
}
|
47
hesabixCore/migrations/Version20250816171207.php
Normal file
47
hesabixCore/migrations/Version20250816171207.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250816171207 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD commodity_serial VARCHAR(255) DEFAULT NULL, ADD buyer_id INT DEFAULT NULL
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD CONSTRAINT FK_1A5DC26F6C755722 FOREIGN KEY (buyer_id) REFERENCES person (id)
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial (buyer_id)
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP FOREIGN KEY FK_1A5DC26F6C755722
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
DROP INDEX IDX_1A5DC26F6C755722 ON plug_warranty_serial
|
||||
SQL);
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP commodity_serial, DROP buyer_id
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration;
|
|||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250811113332 extends AbstractMigration
|
||||
final class Version20250816185111 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ final class Version20250811113332 extends AbstractMigration
|
|||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE person ADD require_two_step TINYINT(1) DEFAULT NULL
|
||||
ALTER TABLE plug_warranty_serial ADD activation VARCHAR(20) NOT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
|
@ -29,7 +29,7 @@ final class Version20250811113332 extends AbstractMigration
|
|||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE person DROP require_two_step
|
||||
ALTER TABLE plug_warranty_serial DROP activation
|
||||
SQL);
|
||||
}
|
||||
}
|
35
hesabixCore/migrations/Version20250816185556.php
Normal file
35
hesabixCore/migrations/Version20250816185556.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250816185556 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial ADD activation_at VARCHAR(20) NOT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial DROP activation_at
|
||||
SQL);
|
||||
}
|
||||
}
|
35
hesabixCore/migrations/Version20250818042052.php
Normal file
35
hesabixCore/migrations/Version20250818042052.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250818042052 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at DATETIME DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) NOT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
35
hesabixCore/migrations/Version20250818042232.php
Normal file
35
hesabixCore/migrations/Version20250818042232.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20250818042232 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at DATETIME DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql(<<<'SQL'
|
||||
ALTER TABLE plug_warranty_serial CHANGE activation_at activation_at VARCHAR(20) DEFAULT NULL
|
||||
SQL);
|
||||
}
|
||||
}
|
|
@ -288,10 +288,7 @@ class PersonService
|
|||
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
|
||||
if (isset($params['company'])) $person->setCompany($params['company']);
|
||||
if (isset($params['tags'])) $person->setTags($params['tags']);
|
||||
if (isset($params['requireTwoStep'])) {
|
||||
error_log("Setting requireTwoStep: " . var_export($params['requireTwoStep'], true));
|
||||
$person->setRequireTwoStep((bool)$params['requireTwoStep']);
|
||||
}
|
||||
|
||||
if (array_key_exists('prelabel', $params)) {
|
||||
if ($params['prelabel'] != '') {
|
||||
$prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]);
|
||||
|
|
426
hesabixCore/src/Controller/ApprovalController.php
Normal file
426
hesabixCore/src/Controller/ApprovalController.php
Normal file
|
@ -0,0 +1,426 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Business;
|
||||
use App\Entity\HesabdariDoc;
|
||||
use App\Entity\HesabdariRow;
|
||||
use App\Entity\Log;
|
||||
use App\Entity\Permission;
|
||||
use App\Entity\User;
|
||||
use App\Service\Access;
|
||||
use App\Service\Log as LogService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||
|
||||
class ApprovalController extends AbstractController
|
||||
{
|
||||
// تأیید حواله انبار
|
||||
#[Route('/api/approval/approve/storeroom/{ticketCode}', name: 'api_approval_approve_storeroom', methods: ['POST'])]
|
||||
public function approveStoreroomTicket(
|
||||
$ticketCode,
|
||||
#[CurrentUser] ?User $user,
|
||||
Access $access,
|
||||
LogService $logService,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response {
|
||||
try {
|
||||
// بررسی دسترسی کاربر
|
||||
$acc = $access->hasRole('settings');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$business = $acc['bid'];
|
||||
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
|
||||
|
||||
// بررسی اینکه آیا تأیید دو مرحلهای فعال است
|
||||
if (!$businessSettings->isRequireTwoStepApproval()) {
|
||||
return $this->json(['success' => false, 'message' => 'تأیید دو مرحلهای فعال نیست']);
|
||||
}
|
||||
|
||||
// پیدا کردن حواله انبار
|
||||
$ticket = $entityManager->getRepository(\App\Entity\StoreroomTicket::class)->findOneBy([
|
||||
'code' => $ticketCode,
|
||||
'bid' => $business
|
||||
]);
|
||||
|
||||
if (!$ticket) {
|
||||
return $this->json(['success' => false, 'message' => 'حواله انبار یافت نشد']);
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید
|
||||
$canApprove = $this->canUserApproveStoreroomTicket($user, $businessSettings);
|
||||
if (!$canApprove) {
|
||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این حواله را ندارید']);
|
||||
}
|
||||
|
||||
// تأیید حواله
|
||||
$ticket->setIsPreview(false);
|
||||
$ticket->setIsApproved(true);
|
||||
$ticket->setApprovedBy($user);
|
||||
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
|
||||
// ثبت لاگ
|
||||
$logService->insert(
|
||||
'تأیید حواله انبار',
|
||||
"حواله انبار {$ticket->getCode()} توسط {$user->getFullName()} تأیید شد",
|
||||
$user,
|
||||
$business
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'حواله انبار با موفقیت تأیید شد'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'خطا در تأیید حواله انبار: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// تأیید فاکتور فروش
|
||||
#[Route('/api/approval/approve/sales/{docId}', name: 'api_approval_approve_sales', methods: ['POST'])]
|
||||
public function approveSalesInvoice(
|
||||
$docId,
|
||||
#[CurrentUser] ?User $user,
|
||||
Access $access,
|
||||
LogService $logService,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response {
|
||||
try {
|
||||
// بررسی دسترسی کاربر
|
||||
$acc = $access->hasRole('settings');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$business = $acc['bid'];
|
||||
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
|
||||
|
||||
// بررسی اینکه آیا تأیید دو مرحلهای فعال است
|
||||
if (!$businessSettings->isRequireTwoStepApproval()) {
|
||||
return $this->json(['success' => false, 'message' => 'تأیید دو مرحلهای فعال نیست']);
|
||||
}
|
||||
|
||||
// پیدا کردن فاکتور فروش
|
||||
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'code' => $docId,
|
||||
'bid' => $business
|
||||
]);
|
||||
|
||||
if (!$document) {
|
||||
return $this->json(['success' => false, 'message' => 'فاکتور فروش یافت نشد']);
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید
|
||||
$canApprove = $this->canUserApproveSalesInvoice($user, $businessSettings);
|
||||
if (!$canApprove) {
|
||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این فاکتور را ندارید']);
|
||||
}
|
||||
|
||||
// تأیید فاکتور
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
||||
$entityManager->persist($document);
|
||||
$entityManager->flush();
|
||||
|
||||
// ثبت لاگ
|
||||
$logService->insert(
|
||||
'تأیید فاکتور فروش',
|
||||
"فاکتور فروش {$document->getCode()} توسط {$user->getFullName()} تأیید شد",
|
||||
$user,
|
||||
$business
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'فاکتور فروش با موفقیت تأیید شد'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'خطا در تأیید فاکتور فروش: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
// تأیید سند مالی
|
||||
#[Route('/api/approval/approve/financial/{docId}', name: 'api_approval_approve_financial', methods: ['POST'])]
|
||||
public function approveFinancialDocument(
|
||||
$docId,
|
||||
#[CurrentUser] ?User $user,
|
||||
Access $access,
|
||||
LogService $logService,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response {
|
||||
try {
|
||||
// بررسی دسترسی کاربر
|
||||
$acc = $access->hasRole('hasRole');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$business = $acc['bid'];
|
||||
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
|
||||
|
||||
// بررسی اینکه آیا تأیید دو مرحلهای فعال است
|
||||
if (!$businessSettings->isRequireTwoStepApproval()) {
|
||||
return $this->json(['success' => false, 'message' => 'تأیید دو مرحلهای فعال نیست']);
|
||||
}
|
||||
|
||||
// پیدا کردن سند مالی
|
||||
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'id' => $docId,
|
||||
'bid' => $business
|
||||
]);
|
||||
|
||||
if (!$document) {
|
||||
return $this->json(['success' => false, 'message' => 'سند مالی یافت نشد']);
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید
|
||||
$canApprove = $this->canUserApproveFinancialDocument($user, $businessSettings);
|
||||
if (!$canApprove) {
|
||||
return $this->json(['success' => false, 'message' => 'شما مجوز تأیید این سند را ندارید']);
|
||||
}
|
||||
|
||||
// تأیید سند
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(true);
|
||||
$document->setApprovedBy($user);
|
||||
|
||||
$entityManager->persist($document);
|
||||
$entityManager->flush();
|
||||
|
||||
// ثبت لاگ
|
||||
$logService->insert(
|
||||
'تأیید سند مالی',
|
||||
"سند مالی {$document->getCode()} توسط {$user->getFullName()} تأیید شد",
|
||||
$user,
|
||||
$business
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'سند مالی با موفقیت تأیید شد'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'خطا در تأیید سند مالی: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/api/approval/reject/{docId}', name: 'api_approval_reject', methods: ['POST'])]
|
||||
public function rejectDocument(
|
||||
$docId,
|
||||
Request $request,
|
||||
#[CurrentUser] ?User $user,
|
||||
Access $access,
|
||||
LogService $logService,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response {
|
||||
try {
|
||||
// بررسی دسترسی کاربر
|
||||
$acc = $access->hasRole('owner');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$business = $acc['bid'];
|
||||
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
|
||||
|
||||
// بررسی اینکه آیا تأیید دو مرحلهای فعال است
|
||||
if (!$businessSettings->isRequireTwoStepApproval()) {
|
||||
return $this->json(['success' => false, 'message' => 'تأیید دو مرحلهای فعال نیست']);
|
||||
}
|
||||
|
||||
// پیدا کردن سند
|
||||
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'id' => $docId,
|
||||
'bid' => $business
|
||||
]);
|
||||
|
||||
if (!$document) {
|
||||
return $this->json(['success' => false, 'message' => 'سند یافت نشد']);
|
||||
}
|
||||
|
||||
// دریافت دلیل رد
|
||||
$data = json_decode($request->getContent(), true);
|
||||
$rejectionReason = $data['reason'] ?? 'دلیل مشخص نشده';
|
||||
|
||||
// رد سند
|
||||
$document->setIsPreview(false);
|
||||
$document->setIsApproved(false);
|
||||
$document->setApprovedBy(null);
|
||||
|
||||
// ردیفها نیازی به تنظیم جداگانه ندارند - از سند پیروی میکنند
|
||||
|
||||
$entityManager->persist($document);
|
||||
$entityManager->flush();
|
||||
|
||||
// ثبت لاگ
|
||||
$logService->insert(
|
||||
'رد سند',
|
||||
"سند {$document->getCode()} توسط {$user->getFullName()} رد شد. دلیل: {$rejectionReason}",
|
||||
$user,
|
||||
$business
|
||||
);
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'سند با موفقیت رد شد'
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'خطا در رد سند: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
#[Route('/api/approval/check-permission/{docId}', name: 'api_approval_check_permission', methods: ['GET'])]
|
||||
public function checkApprovalPermission(
|
||||
$docId,
|
||||
#[CurrentUser] ?User $user,
|
||||
Access $access,
|
||||
EntityManagerInterface $entityManager
|
||||
): Response {
|
||||
try {
|
||||
$acc = $access->hasRole('settings');
|
||||
if (!$acc) {
|
||||
return $this->json(['canApprove' => false, 'message' => 'دسترسی محدود']);
|
||||
}
|
||||
|
||||
$business = $acc['bid'];
|
||||
$businessSettings = $entityManager->getRepository(Business::class)->find($business->getId());
|
||||
|
||||
// پیدا کردن سند
|
||||
$document = $entityManager->getRepository(HesabdariDoc::class)->findOneBy([
|
||||
'id' => $docId,
|
||||
'bid' => $business
|
||||
]);
|
||||
|
||||
if (!$document) {
|
||||
return $this->json(['canApprove' => false, 'message' => 'سند یافت نشد']);
|
||||
}
|
||||
|
||||
$canApprove = $this->canUserApproveDocument($user, $businessSettings, $document);
|
||||
|
||||
return $this->json([
|
||||
'canApprove' => $canApprove,
|
||||
'documentStatus' => [
|
||||
'isPreview' => $document->isPreview(),
|
||||
'isApproved' => $document->isApproved(),
|
||||
'approvedBy' => $document->getApprovedBy() ? $document->getApprovedBy()->getFullName() : null
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->json([
|
||||
'canApprove' => false,
|
||||
'message' => 'خطا در بررسی مجوز: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* بررسی اینکه آیا کاربر میتواند سند را تأیید کند
|
||||
*/
|
||||
private function canUserApproveDocument(User $user, Business $business, HesabdariDoc $document): bool
|
||||
{
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// بررسی تأییدکنندگان اختصاصی بر اساس نوع سند
|
||||
$documentType = $this->getDocumentType($document);
|
||||
|
||||
switch ($documentType) {
|
||||
case 'invoice':
|
||||
return $business->getInvoiceApprover() === $user->getEmail();
|
||||
case 'warehouse':
|
||||
return $business->getWarehouseApprover() === $user->getEmail();
|
||||
case 'financial':
|
||||
return $business->getFinancialApprover() === $user->getEmail();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* تشخیص نوع سند
|
||||
*/
|
||||
private function getDocumentType(HesabdariDoc $document): string
|
||||
{
|
||||
$type = $document->getType();
|
||||
|
||||
if (strpos($type, 'sell') !== false || strpos($type, 'invoice') !== false) {
|
||||
return 'invoice';
|
||||
}
|
||||
|
||||
if (strpos($type, 'warehouse') !== false || strpos($type, 'storeroom') !== false) {
|
||||
return 'warehouse';
|
||||
}
|
||||
|
||||
if (strpos($type, 'payment') !== false || strpos($type, 'receipt') !== false || strpos($type, 'hesabdari') !== false) {
|
||||
return 'financial';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید حواله انبار
|
||||
private function canUserApproveStoreroomTicket(User $user, Business $business): bool
|
||||
{
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// کاربر تأییدکننده انبار
|
||||
return $business->getWarehouseApprover() === $user->getEmail();
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید فاکتور فروش
|
||||
private function canUserApproveSalesInvoice(User $user, Business $business): bool
|
||||
{
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// کاربر تأییدکننده فاکتور فروش
|
||||
return $business->getInvoiceApprover() === $user->getEmail();
|
||||
}
|
||||
|
||||
// بررسی مجوز تأیید سند مالی
|
||||
private function canUserApproveFinancialDocument(User $user, Business $business): bool
|
||||
{
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
if ($user->getEmail() === $business->getOwner()->getEmail()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// کاربر تأییدکننده اسناد مالی
|
||||
return $business->getFinancialApprover() === $user->getEmail();
|
||||
}
|
||||
}
|
|
@ -246,6 +246,22 @@ class BusinessController extends AbstractController
|
|||
$business->setWalletEnable(false);
|
||||
}
|
||||
}
|
||||
if (array_key_exists('requireTwoStepApproval', $params)) {
|
||||
$business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval']);
|
||||
}
|
||||
|
||||
// Set approvers
|
||||
if (array_key_exists('invoiceApprover', $params)) {
|
||||
$business->setInvoiceApprover($params['invoiceApprover']);
|
||||
}
|
||||
|
||||
if (array_key_exists('warehouseApprover', $params)) {
|
||||
$business->setWarehouseApprover($params['warehouseApprover']);
|
||||
}
|
||||
|
||||
if (array_key_exists('financialApprover', $params)) {
|
||||
$business->setFinancialApprover($params['financialApprover']);
|
||||
}
|
||||
|
||||
//get Money type
|
||||
if (!array_key_exists('arzmain', $params) && $isNew) {
|
||||
|
|
|
@ -13,6 +13,7 @@ use App\Service\Access;
|
|||
use App\Service\Log;
|
||||
use App\Service\Provider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Morilog\Jalali\CalendarUtils;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
@ -22,6 +23,8 @@ use App\Entity\StoreroomTicket;
|
|||
use App\Entity\Storeroom;
|
||||
use App\Entity\Person;
|
||||
use App\Entity\HesabdariDoc;
|
||||
use App\Entity\Commodity;
|
||||
use App\Service\FileStorage;
|
||||
|
||||
class ImportWorkflowController extends AbstractController
|
||||
{
|
||||
|
@ -80,6 +83,33 @@ class ImportWorkflowController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/documents/{docId}/download', name: 'api_import_workflow_document_download', methods: ['GET'])]
|
||||
public function downloadDocument(string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): \Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId);
|
||||
if (!$doc) {
|
||||
throw $this->createNotFoundException('سند یافت نشد');
|
||||
}
|
||||
// Enforce business ownership
|
||||
$workflow = $doc->getImportWorkflow();
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
throw $this->createAccessDeniedException('دسترسی ندارید');
|
||||
}
|
||||
$abs = $storage->absolutePath((string)$doc->getFilePath());
|
||||
if (!is_file($abs) || !is_readable($abs)) {
|
||||
throw $this->createNotFoundException('فایل یافت نشد');
|
||||
}
|
||||
$response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs);
|
||||
$response->setContentDisposition(
|
||||
\Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
$doc->getFileName() ?: basename($abs)
|
||||
);
|
||||
$response->headers->set('Content-Type', $doc->getFileType() ?: 'application/octet-stream');
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/create', name: 'api_import_workflow_create', methods: ['POST'])]
|
||||
public function create(
|
||||
Request $request,
|
||||
|
@ -228,6 +258,11 @@ class ImportWorkflowController extends AbstractController
|
|||
foreach ($workflow->getItems() as $item) {
|
||||
$data['items'][] = [
|
||||
'id' => $item->getId(),
|
||||
'commodity' => $item->getCommodity() ? [
|
||||
'id' => $item->getCommodity()->getId(),
|
||||
'code' => method_exists($item->getCommodity(), 'getCode') ? $item->getCommodity()->getCode() : null,
|
||||
'name' => method_exists($item->getCommodity(), 'getName') ? $item->getCommodity()->getName() : null,
|
||||
] : null,
|
||||
'name' => $item->getName(),
|
||||
'productCode' => $item->getProductCode(),
|
||||
'brand' => $item->getBrand(),
|
||||
|
@ -270,14 +305,13 @@ class ImportWorkflowController extends AbstractController
|
|||
'id' => $document->getId(),
|
||||
'type' => $document->getType(),
|
||||
'title' => $document->getTitle(),
|
||||
'filePath' => $document->getFilePath(),
|
||||
'filePath' => $document->getFilePath(), // relative storage path
|
||||
'fileName' => $document->getFileName(),
|
||||
'fileSize' => $document->getFileSize(),
|
||||
'fileType' => $document->getFileType(),
|
||||
'description' => $document->getDescription(),
|
||||
'documentNumber' => $document->getDocumentNumber(),
|
||||
'issueDate' => $document->getIssueDate(),
|
||||
'expiryDate' => $document->getExpiryDate(),
|
||||
'status' => $document->getStatus(),
|
||||
'dateSubmit' => $document->getDateSubmit()
|
||||
];
|
||||
|
@ -348,6 +382,530 @@ class ImportWorkflowController extends AbstractController
|
|||
]);
|
||||
}
|
||||
|
||||
// Documents CRUD
|
||||
#[Route('/api/import-workflow/{id}/documents/create', name: 'api_import_workflow_document_create', methods: ['POST'])]
|
||||
public function createDocument(string $id, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = $request->request->all();
|
||||
$doc = new ImportWorkflowDocument();
|
||||
$doc->setImportWorkflow($workflow);
|
||||
$doc->setType($data['type'] ?? 'other');
|
||||
$doc->setTitle($data['title'] ?? '');
|
||||
$doc->setDocumentNumber($data['documentNumber'] ?? null);
|
||||
$doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null);
|
||||
$doc->setDescription($data['description'] ?? null);
|
||||
$doc->setStatus('active');
|
||||
// handle file
|
||||
$file = $request->files->get('file');
|
||||
if ($file) {
|
||||
$stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs');
|
||||
$doc->setFilePath($stored['relativePath']);
|
||||
$doc->setFileName($stored['originalName']);
|
||||
$doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null);
|
||||
$doc->setFileType($stored['mime']);
|
||||
}
|
||||
$entityManager->persist($doc);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/documents/{docId}/update', name: 'api_import_workflow_document_update', methods: ['PUT','POST'])]
|
||||
public function updateDocument(string $id, string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId);
|
||||
if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = $request->request->all();
|
||||
if (isset($data['type'])) $doc->setType($data['type']);
|
||||
if (isset($data['title'])) $doc->setTitle($data['title']);
|
||||
if (isset($data['documentNumber'])) $doc->setDocumentNumber($data['documentNumber']);
|
||||
if (isset($data['issueDate'])) $doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null);
|
||||
if (isset($data['description'])) $doc->setDescription($data['description']);
|
||||
$file = $request->files->get('file');
|
||||
if ($file) {
|
||||
$stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs');
|
||||
$doc->setFilePath($stored['relativePath']);
|
||||
$doc->setFileName($stored['originalName']);
|
||||
$doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null);
|
||||
$doc->setFileType($stored['mime']);
|
||||
}
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/documents/{docId}/delete', name: 'api_import_workflow_document_delete', methods: ['DELETE'])]
|
||||
public function deleteDocument(string $id, string $docId, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId);
|
||||
if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($doc);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
// Stages CRUD
|
||||
#[Route('/api/import-workflow/{id}/stages/create', name: 'api_import_workflow_stage_create', methods: ['POST'])]
|
||||
public function createStage(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
$st = new ImportWorkflowStage();
|
||||
$st->setImportWorkflow($workflow);
|
||||
$st->setStage($data['stage'] ?? '');
|
||||
$st->setStatus($data['status'] ?? 'pending');
|
||||
$st->setStartDate(isset($data['startDate']) ? ($this->jalaliToGregorian($data['startDate']) ?? null) : null);
|
||||
$st->setEndDate(isset($data['endDate']) ? ($this->jalaliToGregorian($data['endDate']) ?? null) : null);
|
||||
$st->setDescription($data['description'] ?? null);
|
||||
$st->setAssignedTo($data['assignedTo'] ?? null);
|
||||
$st->setNotes($data['notes'] ?? null);
|
||||
$entityManager->persist($st);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/stages/{sid}/update', name: 'api_import_workflow_stage_update', methods: ['PUT'])]
|
||||
public function updateStage(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid);
|
||||
if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
if (isset($data['stage'])) $st->setStage($data['stage']);
|
||||
if (isset($data['status'])) $st->setStatus($data['status']);
|
||||
if (isset($data['startDate'])) $st->setStartDate($this->jalaliToGregorian($data['startDate']) ?? null);
|
||||
if (isset($data['endDate'])) $st->setEndDate($this->jalaliToGregorian($data['endDate']) ?? null);
|
||||
if (isset($data['description'])) $st->setDescription($data['description']);
|
||||
if (isset($data['assignedTo'])) $st->setAssignedTo($data['assignedTo']);
|
||||
if (isset($data['notes'])) $st->setNotes($data['notes']);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/stages/{sid}/delete', name: 'api_import_workflow_stage_delete', methods: ['DELETE'])]
|
||||
public function deleteStage(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid);
|
||||
if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($st);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
// Shipping CRUD
|
||||
#[Route('/api/import-workflow/{id}/shipping/create', name: 'api_import_workflow_shipping_create', methods: ['POST'])]
|
||||
public function createShipping(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
$sh = new ImportWorkflowShipping();
|
||||
$sh->setImportWorkflow($workflow);
|
||||
$sh->setType($data['type'] ?? 'sea');
|
||||
$sh->setContainerNumber($data['containerNumber'] ?? null);
|
||||
$sh->setBillOfLading($data['billOfLading'] ?? null);
|
||||
$sh->setShippingDate(isset($data['shippingDate']) ? ($this->jalaliToGregorian($data['shippingDate']) ?? null) : null);
|
||||
$sh->setArrivalDate(isset($data['arrivalDate']) ? ($this->jalaliToGregorian($data['arrivalDate']) ?? null) : null);
|
||||
$sh->setUnloadingDate(isset($data['unloadingDate']) ? ($this->jalaliToGregorian($data['unloadingDate']) ?? null) : null);
|
||||
$sh->setShippingCompany($data['shippingCompany'] ?? null);
|
||||
$sh->setShippingCompanyPhone($data['shippingCompanyPhone'] ?? null);
|
||||
$sh->setShippingCompanyEmail($data['shippingCompanyEmail'] ?? null);
|
||||
$sh->setOriginPort($data['originPort'] ?? null);
|
||||
$sh->setDestinationPort($data['destinationPort'] ?? null);
|
||||
$sh->setVesselName($data['vesselName'] ?? null);
|
||||
$sh->setVoyageNumber($data['voyageNumber'] ?? null);
|
||||
$sh->setDescription($data['description'] ?? null);
|
||||
$sh->setStatus($data['status'] ?? 'active');
|
||||
$entityManager->persist($sh);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/shipping/{sid}/update', name: 'api_import_workflow_shipping_update', methods: ['PUT','POST'])]
|
||||
public function updateShipping(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid);
|
||||
if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
if (isset($data['type'])) $sh->setType($data['type']);
|
||||
if (isset($data['containerNumber'])) $sh->setContainerNumber($data['containerNumber']);
|
||||
if (isset($data['billOfLading'])) $sh->setBillOfLading($data['billOfLading']);
|
||||
if (isset($data['shippingDate'])) $sh->setShippingDate($this->jalaliToGregorian($data['shippingDate']) ?? null);
|
||||
if (isset($data['arrivalDate'])) $sh->setArrivalDate($this->jalaliToGregorian($data['arrivalDate']) ?? null);
|
||||
if (isset($data['unloadingDate'])) $sh->setUnloadingDate($this->jalaliToGregorian($data['unloadingDate']) ?? null);
|
||||
if (isset($data['shippingCompany'])) $sh->setShippingCompany($data['shippingCompany']);
|
||||
if (isset($data['shippingCompanyPhone'])) $sh->setShippingCompanyPhone($data['shippingCompanyPhone']);
|
||||
if (isset($data['shippingCompanyEmail'])) $sh->setShippingCompanyEmail($data['shippingCompanyEmail']);
|
||||
if (isset($data['originPort'])) $sh->setOriginPort($data['originPort']);
|
||||
if (isset($data['destinationPort'])) $sh->setDestinationPort($data['destinationPort']);
|
||||
if (isset($data['vesselName'])) $sh->setVesselName($data['vesselName']);
|
||||
if (isset($data['voyageNumber'])) $sh->setVoyageNumber($data['voyageNumber']);
|
||||
if (isset($data['description'])) $sh->setDescription($data['description']);
|
||||
if (isset($data['status'])) $sh->setStatus($data['status']);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/shipping/{sid}/delete', name: 'api_import_workflow_shipping_delete', methods: ['DELETE'])]
|
||||
public function deleteShipping(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid);
|
||||
if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($sh);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
// Customs CRUD
|
||||
#[Route('/api/import-workflow/{id}/customs/create', name: 'api_import_workflow_customs_create', methods: ['POST'])]
|
||||
public function createCustoms(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
$c = new ImportWorkflowCustoms();
|
||||
$c->setImportWorkflow($workflow);
|
||||
$c->setDeclarationNumber($data['declarationNumber'] ?? '');
|
||||
$c->setCustomsCode($data['customsCode'] ?? null);
|
||||
$c->setClearanceDate(isset($data['clearanceDate']) ? ($this->jalaliToGregorian($data['clearanceDate']) ?? null) : null);
|
||||
$c->setCustomsDuty(isset($data['customsDuty']) ? (string)$data['customsDuty'] : null);
|
||||
$c->setValueAddedTax(isset($data['valueAddedTax']) ? (string)$data['valueAddedTax'] : null);
|
||||
$c->setOtherCharges(isset($data['otherCharges']) ? (string)$data['otherCharges'] : null);
|
||||
$c->setTotalCustomsCharges(isset($data['totalCustomsCharges']) ? (string)$data['totalCustomsCharges'] : null);
|
||||
$c->setCustomsBroker($data['customsBroker'] ?? null);
|
||||
$c->setCustomsBrokerPhone($data['customsBrokerPhone'] ?? null);
|
||||
$c->setCustomsBrokerEmail($data['customsBrokerEmail'] ?? null);
|
||||
$c->setWarehouseNumber($data['warehouseNumber'] ?? null);
|
||||
$c->setWarehouseLocation($data['warehouseLocation'] ?? null);
|
||||
$c->setDescription($data['description'] ?? null);
|
||||
$c->setStatus($data['status'] ?? 'active');
|
||||
$entityManager->persist($c);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/customs/{cid}/update', name: 'api_import_workflow_customs_update', methods: ['PUT','POST'])]
|
||||
public function updateCustoms(string $id, string $cid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid);
|
||||
if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
if (isset($data['declarationNumber'])) $c->setDeclarationNumber($data['declarationNumber']);
|
||||
if (isset($data['customsCode'])) $c->setCustomsCode($data['customsCode']);
|
||||
if (isset($data['clearanceDate'])) $c->setClearanceDate($this->jalaliToGregorian($data['clearanceDate']) ?? null);
|
||||
if (isset($data['customsDuty'])) $c->setCustomsDuty((string)$data['customsDuty']);
|
||||
if (isset($data['valueAddedTax'])) $c->setValueAddedTax((string)$data['valueAddedTax']);
|
||||
if (isset($data['otherCharges'])) $c->setOtherCharges((string)$data['otherCharges']);
|
||||
if (isset($data['totalCustomsCharges'])) $c->setTotalCustomsCharges((string)$data['totalCustomsCharges']);
|
||||
if (isset($data['customsBroker'])) $c->setCustomsBroker($data['customsBroker']);
|
||||
if (isset($data['customsBrokerPhone'])) $c->setCustomsBrokerPhone($data['customsBrokerPhone']);
|
||||
if (isset($data['customsBrokerEmail'])) $c->setCustomsBrokerEmail($data['customsBrokerEmail']);
|
||||
if (isset($data['warehouseNumber'])) $c->setWarehouseNumber($data['warehouseNumber']);
|
||||
if (isset($data['warehouseLocation'])) $c->setWarehouseLocation($data['warehouseLocation']);
|
||||
if (isset($data['description'])) $c->setDescription($data['description']);
|
||||
if (isset($data['status'])) $c->setStatus($data['status']);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/customs/{cid}/delete', name: 'api_import_workflow_customs_delete', methods: ['DELETE'])]
|
||||
public function deleteCustoms(string $id, string $cid, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) { throw $this->createAccessDeniedException(); }
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid);
|
||||
if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($c);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
// Payments CRUD
|
||||
#[Route('/api/import-workflow/{id}/payments/create', name: 'api_import_workflow_payment_create', methods: ['POST'])]
|
||||
public function createPayment(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
$p = new ImportWorkflowPayment();
|
||||
$p->setImportWorkflow($workflow);
|
||||
$p->setType($data['type'] ?? 'other');
|
||||
$p->setAmount((string)($data['amount'] ?? '0'));
|
||||
$p->setCurrency($data['currency'] ?? 'IRR');
|
||||
$p->setAmountIRR(isset($data['amountIRR']) ? (string)$data['amountIRR'] : null);
|
||||
$p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d'));
|
||||
$p->setReferenceNumber($data['referenceNumber'] ?? null);
|
||||
$p->setBankName($data['bankName'] ?? null);
|
||||
$p->setAccountNumber($data['accountNumber'] ?? null);
|
||||
$p->setRecipientName($data['recipientName'] ?? null);
|
||||
$p->setStatus($data['status'] ?? 'pending');
|
||||
$p->setDescription($data['description'] ?? null);
|
||||
$p->setReceiptNumber($data['receiptNumber'] ?? null);
|
||||
$entityManager->persist($p);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/payments/{pid}/update', name: 'api_import_workflow_payment_update', methods: ['PUT'])]
|
||||
public function updatePayment(string $id, string $pid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid);
|
||||
if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
if (isset($data['type'])) $p->setType($data['type']);
|
||||
if (isset($data['amount'])) $p->setAmount((string)$data['amount']);
|
||||
if (isset($data['currency'])) $p->setCurrency($data['currency']);
|
||||
if (isset($data['amountIRR'])) $p->setAmountIRR((string)$data['amountIRR']);
|
||||
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['bankName'])) $p->setBankName($data['bankName']);
|
||||
if (isset($data['accountNumber'])) $p->setAccountNumber($data['accountNumber']);
|
||||
if (isset($data['recipientName'])) $p->setRecipientName($data['recipientName']);
|
||||
if (isset($data['status'])) $p->setStatus($data['status']);
|
||||
if (isset($data['description'])) $p->setDescription($data['description']);
|
||||
if (isset($data['receiptNumber'])) $p->setReceiptNumber($data['receiptNumber']);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/payments/{pid}/delete', name: 'api_import_workflow_payment_delete', methods: ['DELETE'])]
|
||||
public function deletePayment(string $id, string $pid, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid);
|
||||
if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($p);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/items/create', name: 'api_import_workflow_item_create', methods: ['POST'])]
|
||||
public function createItem(
|
||||
string $id,
|
||||
Request $request,
|
||||
Access $access,
|
||||
EntityManagerInterface $entityManager
|
||||
): JsonResponse {
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
$commodityId = isset($data['commodity_id']) ? (int)$data['commodity_id'] : 0;
|
||||
if ($commodityId <= 0) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>400,'ErrorMessage'=>'کالا انتخاب نشده است','Result'=>null], 400);
|
||||
}
|
||||
$commodity = $entityManager->getRepository(Commodity::class)->find($commodityId);
|
||||
if (!$commodity || $commodity->getBid()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'کالا یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
|
||||
$item = new ImportWorkflowItem();
|
||||
$item->setImportWorkflow($workflow);
|
||||
$item->setCommodity($commodity);
|
||||
$item->setName($data['name'] ?? ($commodity->getName() ?? ''));
|
||||
$item->setProductCode($data['productCode'] ?? ($commodity->getCode() ?? ''));
|
||||
$item->setBrand($data['brand'] ?? null);
|
||||
$item->setModel($data['model'] ?? null);
|
||||
$item->setOriginCountry($data['originCountry'] ?? null);
|
||||
$item->setQuantity($data['quantity'] ?? '0');
|
||||
$item->setUnitPrice($data['unitPrice'] ?? null);
|
||||
$item->setUnitPriceIRR($data['unitPriceIRR'] ?? null);
|
||||
$item->setTotalPrice($data['totalPrice'] ?? null);
|
||||
$item->setTotalPriceIRR($data['totalPriceIRR'] ?? null);
|
||||
$item->setWeight($data['weight'] ?? null);
|
||||
$item->setVolume($data['volume'] ?? null);
|
||||
$item->setDescription($data['description'] ?? null);
|
||||
$item->setSpecifications($data['specifications'] ?? null);
|
||||
|
||||
$entityManager->persist($item);
|
||||
$entityManager->flush();
|
||||
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/items/{itemId}/update', name: 'api_import_workflow_item_update', methods: ['PUT'])]
|
||||
public function updateItem(
|
||||
string $id,
|
||||
string $itemId,
|
||||
Request $request,
|
||||
Access $access,
|
||||
EntityManagerInterface $entityManager
|
||||
): JsonResponse {
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId);
|
||||
if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
|
||||
$data = json_decode($request->getContent() ?: '{}', true);
|
||||
if (isset($data['commodity_id'])) {
|
||||
$commodity = $entityManager->getRepository(Commodity::class)->find((int)$data['commodity_id']);
|
||||
if ($commodity && $commodity->getBid()->getId() === $acc['bid']->getId()) {
|
||||
$item->setCommodity($commodity);
|
||||
if (!isset($data['name'])) $item->setName($commodity->getName());
|
||||
if (!isset($data['productCode'])) $item->setProductCode($commodity->getCode());
|
||||
}
|
||||
}
|
||||
if (isset($data['name'])) $item->setName($data['name']);
|
||||
if (isset($data['productCode'])) $item->setProductCode($data['productCode']);
|
||||
if (isset($data['brand'])) $item->setBrand($data['brand']);
|
||||
if (isset($data['model'])) $item->setModel($data['model']);
|
||||
if (isset($data['originCountry'])) $item->setOriginCountry($data['originCountry']);
|
||||
if (isset($data['quantity'])) $item->setQuantity($data['quantity']);
|
||||
if (isset($data['unitPrice'])) $item->setUnitPrice($data['unitPrice']);
|
||||
if (isset($data['unitPriceIRR'])) $item->setUnitPriceIRR($data['unitPriceIRR']);
|
||||
if (isset($data['totalPrice'])) $item->setTotalPrice($data['totalPrice']);
|
||||
if (isset($data['totalPriceIRR'])) $item->setTotalPriceIRR($data['totalPriceIRR']);
|
||||
if (isset($data['weight'])) $item->setWeight($data['weight']);
|
||||
if (isset($data['volume'])) $item->setVolume($data['volume']);
|
||||
if (isset($data['description'])) $item->setDescription($data['description']);
|
||||
if (isset($data['specifications'])) $item->setSpecifications($data['specifications']);
|
||||
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/items/{itemId}/delete', name: 'api_import_workflow_item_delete', methods: ['DELETE'])]
|
||||
public function deleteItem(
|
||||
string $id,
|
||||
string $itemId,
|
||||
Access $access,
|
||||
EntityManagerInterface $entityManager
|
||||
): JsonResponse {
|
||||
$acc = $access->hasRole('import_workflow');
|
||||
if (!$acc) {
|
||||
throw $this->createAccessDeniedException();
|
||||
}
|
||||
$workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id);
|
||||
if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId);
|
||||
if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) {
|
||||
return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404);
|
||||
}
|
||||
$entityManager->remove($item);
|
||||
$entityManager->flush();
|
||||
return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]);
|
||||
}
|
||||
|
||||
#[Route('/api/import-workflow/{id}/update', name: 'api_import_workflow_update', methods: ['PUT'])]
|
||||
public function update(
|
||||
string $id,
|
||||
|
@ -497,7 +1055,7 @@ class ImportWorkflowController extends AbstractController
|
|||
$ticket->setType('input');
|
||||
$ticket->setTypeString('ورود از واردات');
|
||||
$ticket->setDes('ورود از پرونده واردات #' . $workflow->getCode());
|
||||
$ticket->setStatus('in_progress');
|
||||
// $ticket->setStatus('in_progress');
|
||||
$ticket->setImportWorkflowCode($workflow->getCode());
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
|
@ -513,4 +1071,41 @@ class ImportWorkflowController extends AbstractController
|
|||
]
|
||||
]);
|
||||
}
|
||||
|
||||
private function jalaliToGregorian(?string $input): ?string
|
||||
{
|
||||
if (!$input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$s = trim($input);
|
||||
|
||||
// پشتیبانی از جلالی با جداکننده '/'
|
||||
if (preg_match('/^(\d{4})\/(\d{2})\/(\d{2})$/', $s, $m)) {
|
||||
$y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3];
|
||||
try {
|
||||
$g = CalendarUtils::toGregorian($y, $mo, $d);
|
||||
return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]);
|
||||
} catch (\Throwable $e) {
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
|
||||
// پشتیبانی از جلالی با جداکننده '-'
|
||||
if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) {
|
||||
$y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3];
|
||||
// اگر سال جلالی باشد (مثلاً 1400) آن را تبدیل کن، در غیر اینصورت همان مقدار را برگردان
|
||||
if ($y < 1700) {
|
||||
try {
|
||||
$g = CalendarUtils::toGregorian($y, $mo, $d);
|
||||
return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]);
|
||||
} catch (\Throwable $e) {
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
return $s; // احتمالاً میلادی است
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
400
hesabixCore/src/Controller/PublicController.php
Normal file
400
hesabixCore/src/Controller/PublicController.php
Normal file
|
@ -0,0 +1,400 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Business;
|
||||
use App\Entity\PlugWarrantySerial;
|
||||
use App\Service\PluginService;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Morilog\Jalali\CalendarUtils;
|
||||
use App\Service\registryMGR;
|
||||
|
||||
class PublicController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* Check if warranty plugin is active for the business
|
||||
*/
|
||||
private function checkWarrantyPluginActive(Business $business, PluginService $pluginService): JsonResponse|null
|
||||
{
|
||||
if (!$pluginService->isActive('warranty', $business)) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست',
|
||||
'error' => 'PLUGIN_NOT_ACTIVE'
|
||||
], 404);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
#[Route('/api/public/{businessId}/warranty/check/{code}', name: 'api_public_warranty_check', methods: ['GET'])]
|
||||
public function checkWarrantyCode(string $businessId, string $code, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse
|
||||
{
|
||||
// Validate input
|
||||
if (empty($code)) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد گارانتی وارد نشده است'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Find business
|
||||
$business = $entityManager->getRepository(Business::class)->find($businessId);
|
||||
if (!$business) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کسب و کار مورد نظر یافت نشد'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if warranty plugin is active
|
||||
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
|
||||
if ($pluginCheck) {
|
||||
return $pluginCheck;
|
||||
}
|
||||
|
||||
// Find warranty serial
|
||||
$warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $business,
|
||||
'serialNumber' => $code
|
||||
]);
|
||||
|
||||
if (!$warrantySerial) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if already activated (used)
|
||||
if (!$warrantySerial->isUsed()) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'این گارانتی ثبت نشده است',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// if ($warrantySerial->getActivation() === 'active') {
|
||||
// return $this->json([
|
||||
// 'success' => false,
|
||||
// 'message' => 'این گارانتی قبلاً فعال شده است',
|
||||
// ], 400);
|
||||
// }
|
||||
|
||||
$activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
|
||||
$allocatedAt = $warrantySerial->getAllocatedAt();
|
||||
$currentDate = new \DateTime();
|
||||
$daysDiff = $currentDate->diff($allocatedAt)->days;
|
||||
|
||||
if ($daysDiff > $activationTimeLimit) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => "مهلت فعالسازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است"
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Get commodity information
|
||||
$commodity = $warrantySerial->getCommodity();
|
||||
|
||||
// Calculate warranty period end date
|
||||
$warrantyEndDate = null;
|
||||
if ($warrantySerial->getWarrantyEndDate()) {
|
||||
$warrantyEndDate = $warrantySerial->getWarrantyEndDate();
|
||||
} elseif ($warrantySerial->getWarrantyStartDate()) {
|
||||
// If only start date is set, assume 12 months warranty period
|
||||
$startDate = new \DateTime($warrantySerial->getWarrantyStartDate());
|
||||
$endDate = $startDate->add(new \DateInterval('P12M'));
|
||||
$warrantyEndDate = $endDate->format('Y-m-d');
|
||||
}
|
||||
|
||||
// Prepare product information
|
||||
$productInfo = [
|
||||
'serialNumber' => $warrantySerial->getSerialNumber(),
|
||||
'commoditySerial' => $warrantySerial->getCommoditySerial(),
|
||||
'productName' => $commodity ? $commodity->getName() : 'نامشخص',
|
||||
'productCode' => $commodity ? $commodity->getCode() : null,
|
||||
'description' => $warrantySerial->getDescription(),
|
||||
'submitDate' => $warrantySerial->getDateSubmit(),
|
||||
'warrantyStartDate' => $warrantySerial->getWarrantyStartDate(),
|
||||
'warrantyEndDate' => $warrantyEndDate,
|
||||
'status' => $warrantySerial->getStatus(),
|
||||
'activation' => $warrantySerial->getActivation(),
|
||||
'notes' => $warrantySerial->getNotes(),
|
||||
'activationTimeLimit' => $activationTimeLimit,
|
||||
'daysRemaining' => max(0, $activationTimeLimit - $daysDiff),
|
||||
'submitter' => $warrantySerial->getSubmitter() ? $warrantySerial->getSubmitter()->getFullName() : null,
|
||||
'businessName' => $business->getName(),
|
||||
'activationTicketCode' => $warrantySerial->getActivationTicketCode(),
|
||||
'requireActivationSecret' => true
|
||||
];
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => $productInfo
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/public/{businessId}/warranty/activate/{code}', name: 'api_public_warranty_activate', methods: ['POST'])]
|
||||
public function activateWarranty(string $businessId, string $code, Request $request, EntityManagerInterface $entityManager, PluginService $pluginService, registryMGR $registryMGR): JsonResponse
|
||||
{
|
||||
// Validate input
|
||||
if (empty($code)) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد گارانتی وارد نشده است'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Find business
|
||||
$business = $entityManager->getRepository(Business::class)->find($businessId);
|
||||
if (!$business) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کسب و کار مورد نظر یافت نشد'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if warranty plugin is active
|
||||
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
|
||||
if ($pluginCheck) {
|
||||
return $pluginCheck;
|
||||
}
|
||||
|
||||
// Find warranty serial
|
||||
$warrantySerial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $business,
|
||||
'serialNumber' => $code
|
||||
]);
|
||||
|
||||
if (!$warrantySerial) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد گارانتی نامعتبر است یا متعلق به این کسب و کار نیست'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if already activated
|
||||
if (!$warrantySerial->isUsed()) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'این گارانتی ثبت نشده است',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Check status
|
||||
if ($warrantySerial->getActivation() !== 'deactive') {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'وضعیت این گارانتی فعال است'
|
||||
], 400);
|
||||
}
|
||||
|
||||
$activationTimeLimit = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7);
|
||||
$allocatedAt = $warrantySerial->getAllocatedAt();
|
||||
$currentDate = new \DateTime();
|
||||
$daysDiff = $currentDate->diff($allocatedAt)->days;
|
||||
|
||||
if ($daysDiff > $activationTimeLimit) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => "مهلت فعالسازی این گارانتی ({$activationTimeLimit} روز) به پایان رسیده است"
|
||||
], 400);
|
||||
}
|
||||
|
||||
$secret = json_decode($request->getContent() ?: '{}', true)['activationSecret'] ?? '';
|
||||
if ($warrantySerial->getActivationTicketSecret()) {
|
||||
if (!$secret || $secret !== $warrantySerial->getActivationTicketSecret()) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد فعالسازی حواله نامعتبر است'
|
||||
], 400);
|
||||
}
|
||||
}
|
||||
|
||||
$warrantySerial->setActivation('active');
|
||||
$warrantySerial->setActivationAt(new \DateTimeImmutable());
|
||||
|
||||
// Set warranty start date to current date if not already set
|
||||
// if (!$warrantySerial->getWarrantyStartDate()) {
|
||||
// $warrantySerial->setWarrantyStartDate(date('Y-m-d'));
|
||||
// }
|
||||
|
||||
// Set warranty end date if not already set (default 12 months)
|
||||
// if (!$warrantySerial->getWarrantyEndDate()) {
|
||||
// $endDate = new \DateTime();
|
||||
// $endDate->add(new \DateInterval('P12M'));
|
||||
// $warrantySerial->setWarrantyEndDate($endDate->format('Y-m-d'));
|
||||
// }
|
||||
|
||||
// Save changes
|
||||
$entityManager->persist($warrantySerial);
|
||||
$entityManager->flush();
|
||||
|
||||
// Get commodity information for response
|
||||
$commodity = $warrantySerial->getCommodity();
|
||||
|
||||
// Prepare activation information
|
||||
$activationInfo = [
|
||||
'serialNumber' => $warrantySerial->getSerialNumber(),
|
||||
'productName' => $commodity ? $commodity->getName() : 'نامشخص',
|
||||
'productCode' => $commodity ? $commodity->getCode() : null,
|
||||
'activationDate' => $warrantySerial->getActivationAt()?->format('Y-m-d'),
|
||||
'warrantyStartDate' => $warrantySerial->getWarrantyStartDate() ? jalaliToGregorian($warrantySerial->getWarrantyStartDate()->format('Y/m/d')) : null,
|
||||
'warrantyEndDate' => $warrantySerial->getWarrantyEndDate() ? jalaliToGregorian($warrantySerial->getWarrantyEndDate()->format('Y/m/d')) : null,
|
||||
'businessName' => $business->getName(),
|
||||
'notes' => $warrantySerial->getNotes()
|
||||
];
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'گارانتی با موفقیت فعال شد',
|
||||
'data' => $activationInfo
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/public/{businessId}/warranty/help', name: 'api_public_warranty_help', methods: ['GET'])]
|
||||
public function getWarrantyHelp(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
// Find business
|
||||
$business = $entityManager->getRepository(Business::class)->find($businessId);
|
||||
if (!$business) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کسب و کار مورد نظر یافت نشد'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if warranty plugin is active
|
||||
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
|
||||
if ($pluginCheck) {
|
||||
return $pluginCheck;
|
||||
}
|
||||
$helpInfo = [
|
||||
'codeFormat' => [
|
||||
'prefix' => 'WR',
|
||||
'length' => 11,
|
||||
'example' => 'WR-123456789',
|
||||
'description' => 'کد گارانتی با حروف WR شروع میشود و شامل 9 رقم است'
|
||||
],
|
||||
'locations' => [
|
||||
'محصول' => 'روی برچسب چسبیده شده به محصول',
|
||||
'فاکتور' => 'در فاکتور خرید یا رسید پرداخت',
|
||||
'بستهبندی' => 'در جعبه یا بستهبندی محصول',
|
||||
'ایمیل' => 'در ایمیل تأیید خرید'
|
||||
],
|
||||
'activationTimeLimit' => [
|
||||
'default' => 7,
|
||||
'unit' => 'روز',
|
||||
'description' => 'گارانتی باید حداکثر 7 روز پس از خرید فعال شود'
|
||||
],
|
||||
'support' => [
|
||||
'phone' => '021-88888888',
|
||||
'email' => 'support@example.com',
|
||||
'hours' => 'شنبه تا پنجشنبه، 8 صبح تا 8 شب'
|
||||
]
|
||||
];
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'data' => $helpInfo
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/public/{businessId}/status', name: 'api_public_business_status', methods: ['GET'])]
|
||||
public function getBusinessStatus(string $businessId, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
// Find business
|
||||
$business = $entityManager->getRepository(Business::class)->find($businessId);
|
||||
if (!$business) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کسب و کار مورد نظر یافت نشد',
|
||||
'error' => 'BUSINESS_NOT_FOUND'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if warranty plugin is active
|
||||
if (!$pluginService->isActive('warranty', $business)) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'پلاگین گارانتی برای این کسب و کار فعال نیست',
|
||||
'error' => 'PLUGIN_NOT_ACTIVE'
|
||||
], 404);
|
||||
}
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'message' => 'کسب و کار و پلاگین گارانتی فعال است',
|
||||
'data' => [
|
||||
'businessName' => $business->getName(),
|
||||
'pluginActive' => true
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/api/public/{businessId}/warranty/qr-scan', name: 'api_public_warranty_qr_scan', methods: ['POST'])]
|
||||
public function scanQrCode(string $businessId, Request $request, PluginService $pluginService, EntityManagerInterface $entityManager): JsonResponse
|
||||
{
|
||||
// Find business
|
||||
$business = $entityManager->getRepository(Business::class)->find($businessId);
|
||||
if (!$business) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کسب و کار مورد نظر یافت نشد'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Check if warranty plugin is active
|
||||
$pluginCheck = $this->checkWarrantyPluginActive($business, $pluginService);
|
||||
if ($pluginCheck) {
|
||||
return $pluginCheck;
|
||||
}
|
||||
|
||||
// TODO: Implement QR code scanning logic
|
||||
|
||||
$params = json_decode($request->getContent(), true);
|
||||
$qrData = $params['qrData'] ?? '';
|
||||
|
||||
if (empty($qrData)) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'داده QR خالی است'
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Extract warranty code from QR data
|
||||
// Assuming QR contains warranty code directly or in a URL
|
||||
$warrantyCode = $this->extractWarrantyCodeFromQr($qrData);
|
||||
|
||||
if (!$warrantyCode) {
|
||||
return $this->json([
|
||||
'success' => false,
|
||||
'message' => 'کد گارانتی در QR یافت نشد'
|
||||
], 400);
|
||||
}
|
||||
|
||||
return $this->json([
|
||||
'success' => true,
|
||||
'warrantyCode' => $warrantyCode
|
||||
]);
|
||||
}
|
||||
|
||||
private function extractWarrantyCodeFromQr(string $qrData): ?string
|
||||
{
|
||||
if (preg_match('/WR-\d{9}/', $qrData, $matches)) {
|
||||
return $matches[0];
|
||||
}
|
||||
|
||||
if (preg_match('/warranty.*code[=\/]([A-Za-z0-9-]+)/', $qrData, $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
function jalaliToGregorian($date) {
|
||||
$p = explode('/', $date);
|
||||
return implode('-', CalendarUtils::toGregorian($p[0], $p[1], $p[2]));
|
||||
}
|
|
@ -230,6 +230,16 @@ class SellController extends AbstractController
|
|||
$doc->setSubmitter($this->getUser());
|
||||
$doc->setMoney($acc['money']);
|
||||
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
|
||||
|
||||
// Set approval fields based on business settings
|
||||
$business = $acc['bid'];
|
||||
if ($business->isRequireTwoStepApproval()) {
|
||||
$doc->setIsPreview(true);
|
||||
$doc->setIsApproved(false);
|
||||
} else {
|
||||
$doc->setIsPreview(false);
|
||||
$doc->setIsApproved(true);
|
||||
}
|
||||
}
|
||||
if ($params['transferCost'] != 0) {
|
||||
$hesabdariRow = new HesabdariRow();
|
||||
|
@ -322,13 +332,17 @@ class SellController extends AbstractController
|
|||
$hesabdariRow->setPerson($person);
|
||||
$entityManager->persist($hesabdariRow);
|
||||
|
||||
// Two-step approval: اگر پرمیشن کسبوکار تأیید دو مرحلهای فروش را الزامی کرده باشد
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepSell()) || $personRequire) {
|
||||
$doc->setStatus('pending_approval');
|
||||
// Two-step approval: اگر کسبوکار تأیید دو مرحلهای را الزامی کرده باشد
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire) {
|
||||
$doc->setIsPreview(true);
|
||||
$doc->setIsApproved(false);
|
||||
$doc->setApprovedBy(null);
|
||||
} else {
|
||||
$doc->setStatus('approved');
|
||||
$doc->setIsPreview(false);
|
||||
$doc->setIsApproved(true);
|
||||
$doc->setApprovedBy($this->getUser());
|
||||
}
|
||||
$entityManager->persist($doc);
|
||||
$entityManager->flush();
|
||||
|
@ -467,10 +481,13 @@ class SellController extends AbstractController
|
|||
|
||||
$queryBuilder = $entityManager->createQueryBuilder()
|
||||
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
|
||||
->addSelect('d.isPreview, d.isApproved')
|
||||
->addSelect('u.fullName as submitter')
|
||||
->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail')
|
||||
->addSelect('l.code as labelCode, l.label as labelLabel')
|
||||
->from(HesabdariDoc::class, 'd')
|
||||
->leftJoin('d.submitter', 'u')
|
||||
->leftJoin('d.approvedBy', 'approver')
|
||||
->leftJoin('d.InvoiceLabel', 'l')
|
||||
->leftJoin('d.hesabdariRows', 'r')
|
||||
->where('d.bid = :bid')
|
||||
|
@ -532,7 +549,9 @@ class SellController extends AbstractController
|
|||
'plugin' => 'd.plugin',
|
||||
'refData' => 'd.refData',
|
||||
'shortlink' => 'd.shortlink',
|
||||
'status' => 'd.status',
|
||||
'isPreview' => 'd.isPreview',
|
||||
'isApproved' => 'd.isApproved',
|
||||
'approvedBy' => 'd.approvedBy',
|
||||
'submitter' => 'u.fullName',
|
||||
'label' => 'l.label', // از InvoiceLabel
|
||||
];
|
||||
|
@ -578,6 +597,13 @@ class SellController extends AbstractController
|
|||
'code' => $doc['labelCode'],
|
||||
'label' => $doc['labelLabel']
|
||||
] : null,
|
||||
'isPreview' => $doc['isPreview'],
|
||||
'isApproved' => $doc['isApproved'],
|
||||
'approvedBy' => $doc['approvedByName'] ? [
|
||||
'fullName' => $doc['approvedByName'],
|
||||
'id' => $doc['approvedById'],
|
||||
'email' => $doc['approvedByEmail']
|
||||
] : null,
|
||||
];
|
||||
|
||||
$mainRow = $entityManager->getRepository(HesabdariRow::class)
|
||||
|
@ -1234,6 +1260,19 @@ class SellController extends AbstractController
|
|||
$hesabdariRow->setPerson($person);
|
||||
$entityManager->persist($hesabdariRow);
|
||||
|
||||
// Two-step approval: اگر کسبوکار تأیید دو مرحلهای را الزامی کرده باشد
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire) {
|
||||
$doc->setIsPreview(true);
|
||||
$doc->setIsApproved(false);
|
||||
$doc->setApprovedBy(null);
|
||||
} else {
|
||||
$doc->setIsPreview(false);
|
||||
$doc->setIsApproved(true);
|
||||
$doc->setApprovedBy($this->getUser());
|
||||
}
|
||||
|
||||
// ذخیره فاکتور
|
||||
$entityManager->persist($doc);
|
||||
$entityManager->flush();
|
||||
|
@ -1320,13 +1359,13 @@ class SellController extends AbstractController
|
|||
$entityManager->persist($receiveRow);
|
||||
|
||||
// Two-step approval برای دریافت/پرداخت
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepPayment()) || $personRequire) {
|
||||
$paymentDoc->setStatus('pending_approval');
|
||||
} else {
|
||||
$paymentDoc->setStatus('approved');
|
||||
}
|
||||
// $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
// $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
// if ($businessRequire) {
|
||||
// $paymentDoc->setStatus('pending_approval');
|
||||
// } else {
|
||||
// $paymentDoc->setStatus('approved');
|
||||
// }
|
||||
$entityManager->persist($paymentDoc);
|
||||
}
|
||||
$entityManager->flush();
|
||||
|
|
|
@ -57,7 +57,7 @@ class StoreroomController extends AbstractController
|
|||
}
|
||||
|
||||
#[Route('/api/storeroom/ticket/attachments/upload/{code}', name: 'app_storeroom_ticket_upload_attachment', methods: ['POST'])]
|
||||
public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||
public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
|
@ -68,23 +68,21 @@ class StoreroomController extends AbstractController
|
|||
if (!$file) {
|
||||
return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400);
|
||||
}
|
||||
$uploadDir = __DIR__ . '/../../../public_html/uploads/storeroom/';
|
||||
if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true);
|
||||
$safeName = uniqid('st_', true) . '_' . preg_replace('/[^A-Za-z0-9_\.-]/','_', $file->getClientOriginalName());
|
||||
$file->move($uploadDir, $safeName);
|
||||
// Store securely in var/storage
|
||||
$stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments');
|
||||
|
||||
$archive = new ArchiveFile();
|
||||
$archive->setBid($acc['bid']);
|
||||
$archive->setSubmitter($acc['user']);
|
||||
$archive->setDateSubmit(date('Y-m-d H:i:s'));
|
||||
$archive->setFilename('/uploads/storeroom/' . $safeName);
|
||||
$archive->setFilename($stored['relativePath']);
|
||||
$archive->setCat('storeroom_ticket');
|
||||
$archive->setFileType($file->getClientMimeType() ?: 'application/octet-stream');
|
||||
$archive->setFileType($stored['mime'] ?: 'application/octet-stream');
|
||||
$archive->setPublic(false);
|
||||
$archive->setDes($request->request->get('des'));
|
||||
$archive->setRelatedDocType('storeroom_ticket');
|
||||
$archive->setRelatedDocCode($ticket->getCode());
|
||||
$archive->setFileSize((string) $file->getSize());
|
||||
$archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null);
|
||||
$entityManager->persist($archive);
|
||||
$entityManager->flush();
|
||||
|
||||
|
@ -115,6 +113,28 @@ class StoreroomController extends AbstractController
|
|||
}, $items));
|
||||
}
|
||||
|
||||
#[Route('/api/storeroom/ticket/attachments/download/{id}', name: 'app_storeroom_ticket_download_attachment', methods: ['GET'])]
|
||||
public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response
|
||||
{
|
||||
$acc = $access->hasRole('store');
|
||||
if (!$acc) throw $this->createAccessDeniedException();
|
||||
$a = $entityManager->getRepository(ArchiveFile::class)->find($id);
|
||||
if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) {
|
||||
throw $this->createNotFoundException('فایل یافت نشد');
|
||||
}
|
||||
$abs = $storage->absolutePath((string)$a->getFilename());
|
||||
if (!is_file($abs) || !is_readable($abs)) {
|
||||
throw $this->createNotFoundException('فایل موجود نیست');
|
||||
}
|
||||
$response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs);
|
||||
$response->setContentDisposition(
|
||||
\Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT,
|
||||
basename($abs)
|
||||
);
|
||||
$response->headers->set('Content-Type', $a->getFileType() ?: 'application/octet-stream');
|
||||
return $response;
|
||||
}
|
||||
|
||||
#[Route('/api/storeroom/mod/{code}', name: 'app_storeroom_mod')]
|
||||
public function app_storeroom_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
|
||||
{
|
||||
|
@ -195,8 +215,12 @@ class StoreroomController extends AbstractController
|
|||
foreach ($buys as $buy) {
|
||||
$temp = $provider->Entity2Array($buy, 0);
|
||||
$person = $this->getPerson($buy);
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
if ($person) {
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
} else {
|
||||
$temp['person'] = null;
|
||||
}
|
||||
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
||||
//check storeroom exist
|
||||
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
||||
|
@ -216,8 +240,12 @@ class StoreroomController extends AbstractController
|
|||
foreach ($sells as $sell) {
|
||||
$temp = $provider->Entity2Array($sell, 0);
|
||||
$person = $this->getPerson($sell);
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
if ($person) {
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
} else {
|
||||
$temp['person'] = null;
|
||||
}
|
||||
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
||||
//check storeroom exist
|
||||
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
||||
|
@ -237,8 +265,12 @@ class StoreroomController extends AbstractController
|
|||
foreach ($rfsells as $sell) {
|
||||
$temp = $provider->Entity2Array($sell, 0);
|
||||
$person = $this->getPerson($sell);
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
if ($person) {
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
} else {
|
||||
$temp['person'] = null;
|
||||
}
|
||||
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
||||
//check storeroom exist
|
||||
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
||||
|
@ -258,8 +290,12 @@ class StoreroomController extends AbstractController
|
|||
foreach ($rfbuys as $buy) {
|
||||
$temp = $provider->Entity2Array($buy, 0);
|
||||
$person = $this->getPerson($buy);
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
if ($person) {
|
||||
$temp['person'] = Explore::ExplorePerson($person);
|
||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
} else {
|
||||
$temp['person'] = null;
|
||||
}
|
||||
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
||||
//check storeroom exist
|
||||
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
||||
|
@ -342,8 +378,12 @@ class StoreroomController extends AbstractController
|
|||
}
|
||||
}
|
||||
$res = $provider->Entity2Array($doc, 0);
|
||||
$res['person'] = $provider->Entity2Array($person, 0);
|
||||
$res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
if ($person) {
|
||||
$res['person'] = $provider->Entity2Array($person, 0);
|
||||
$res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||
} else {
|
||||
$res['person'] = null;
|
||||
}
|
||||
$res['commodities'] = $provider->ArrayEntity2Array($commodities, 1, ['doc', 'bid', 'year']);
|
||||
//calculate rows data
|
||||
$this->calcStoreRemaining($res, $doc, $entityManager);
|
||||
|
@ -464,6 +504,10 @@ class StoreroomController extends AbstractController
|
|||
$ticket->setTransfer($params['ticket']['transfer']);
|
||||
$ticket->setYear($acc['year']);
|
||||
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
|
||||
$alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
$rand = '';
|
||||
for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; }
|
||||
$ticket->setActivationCode($rand);
|
||||
$ticket->setReceiver($params['ticket']['receiver']);
|
||||
$ticket->setTransferType($transferType);
|
||||
$ticket->setReferral($params['ticket']['referral']);
|
||||
|
@ -472,9 +516,6 @@ class StoreroomController extends AbstractController
|
|||
$ticket->setType($params['ticket']['type']);
|
||||
$ticket->setTypeString($params['ticket']['typeString']);
|
||||
$ticket->setDes($params['ticket']['des']);
|
||||
// وضعیت اولیه
|
||||
$ticket->setStatus('in_progress');
|
||||
// اگر از پرونده واردات آمده
|
||||
if (array_key_exists('importWorkflowCode', $params['ticket'])) {
|
||||
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
|
||||
}
|
||||
|
@ -485,37 +526,22 @@ class StoreroomController extends AbstractController
|
|||
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
||||
'doc' => $doc
|
||||
]);
|
||||
// بررسی الزام سریال: اگر در بدنه درخواست گزینه requireWarrantySerial=true باشد، قبل از ثبت نهایی، کف سریالهای آزاد کالا را چک میکنیم
|
||||
$requireWarrantySerial = isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true;
|
||||
|
||||
// Determine if warranty serials are required based on flag or provided lines
|
||||
$hasSerialLines = false;
|
||||
foreach (($params['items'] ?? []) as $it) {
|
||||
if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; }
|
||||
}
|
||||
$requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines;
|
||||
if ($requireWarrantySerial) {
|
||||
if (!$pluginService->isActive('warranty', $acc['bid'])) {
|
||||
return $this->json([
|
||||
'result' => -5,
|
||||
'message' => 'افزونه گارانتی فعال نیست'
|
||||
], 403);
|
||||
return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403);
|
||||
}
|
||||
}
|
||||
if ($requireWarrantySerial) {
|
||||
// Validate counts up-front
|
||||
foreach ($params['items'] as $item) {
|
||||
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
||||
'bid' => $acc['bid'],
|
||||
'doc' => $doc,
|
||||
'id' => $item['id'],
|
||||
]);
|
||||
if (!$row || !$row->getCommodity())
|
||||
throw $this->createNotFoundException('کالا یافت نشد!');
|
||||
$commodity = $row->getCommodity();
|
||||
$freeSerialCount = $entityManager->getRepository(PlugWarrantySerial::class)->count([
|
||||
'bid' => $acc['bid'],
|
||||
'commodity' => $commodity,
|
||||
'used' => null,
|
||||
'status' => 'active'
|
||||
]);
|
||||
if ($freeSerialCount < (int)$item['ticketCount']) {
|
||||
return $this->json([
|
||||
'result' => -2,
|
||||
'message' => 'تعداد سریال گارانتی آزاد برای کالا کافی نیست'
|
||||
], 400);
|
||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||
if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) {
|
||||
return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -543,44 +569,57 @@ class StoreroomController extends AbstractController
|
|||
$ticketItem->setCommodity($row->getCommodity());
|
||||
$ticketItem->setType($item['type']);
|
||||
$entityManager->persist($ticketItem);
|
||||
// اگر الزام سریال فعال است و سریالها هم در همین درخواست آمدهاند، آنها را مصرف کنیم
|
||||
if ($requireWarrantySerial && isset($item['warrantySerials']) && is_array($item['warrantySerials'])) {
|
||||
$serials = $item['warrantySerials'];
|
||||
if (count($serials) != (int)$item['ticketCount']) {
|
||||
return $this->json([
|
||||
'result' => -3,
|
||||
'message' => 'تعداد سریالهای ارسالی با تعداد حواله همخوانی ندارد'
|
||||
], 400);
|
||||
}
|
||||
foreach ($serials as $serialNumber) {
|
||||
/** @var PlugWarrantySerial|null $serial */
|
||||
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'bid' => $acc['bid'],
|
||||
'serialNumber' => $serialNumber,
|
||||
'commodity' => $row->getCommodity(),
|
||||
'status' => 'active'
|
||||
]);
|
||||
if (!$serial || $serial->isUsed()) {
|
||||
return $this->json([
|
||||
'result' => -4,
|
||||
'message' => 'سریال نامعتبر یا قبلاً مصرف شده: ' . $serialNumber
|
||||
], 400);
|
||||
|
||||
// Bind warranty serials per item if provided
|
||||
if ($requireWarrantySerial) {
|
||||
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||
if ((int)$item['ticketCount'] > 0) {
|
||||
// Ensure we have an id to bind to
|
||||
$entityManager->flush();
|
||||
$lines = array_slice($lines, 0, (int)$item['ticketCount']);
|
||||
foreach ($lines as $ln) {
|
||||
$warrantyCode = $ln['warranty'] ?? null;
|
||||
$deviceSerial = $ln['serial'] ?? null;
|
||||
if (!$warrantyCode) {
|
||||
return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400);
|
||||
}
|
||||
/** @var PlugWarrantySerial|null $serial */
|
||||
$serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([
|
||||
'business' => $acc['bid'],
|
||||
'serialNumber' => $warrantyCode,
|
||||
'commodity' => $row->getCommodity(),
|
||||
]);
|
||||
if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) {
|
||||
return $this->json(['result' => -4, 'message' => 'گارانتی نامعتبر یا آزاد نیست: ' . $warrantyCode], 400);
|
||||
}
|
||||
$serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED);
|
||||
$serial->setCommoditySerial($deviceSerial);
|
||||
$serial->setBuyer($person);
|
||||
$serial->setAllocatedToDocumentId($doc->getId());
|
||||
$serial->setAllocatedAt(new \DateTimeImmutable());
|
||||
$serial->setBoundToItemId($ticketItem->getId());
|
||||
$serial->setBoundAt(new \DateTimeImmutable());
|
||||
$serial->setActivationTicketCode($ticket->getCode());
|
||||
$serial->setActivationTicketSecret($ticket->getActivationCode());
|
||||
$entityManager->persist($serial);
|
||||
}
|
||||
$serial->setUsed(true);
|
||||
$serial->setUsedAt(date('Y-m-d H:i:s'));
|
||||
$serial->setUsedTicketCode($ticket->getCode());
|
||||
$entityManager->persist($serial);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$entityManager->flush();
|
||||
// اگر تأیید دو مرحلهای حواله فعال باشد، وضعیت را pending_approval بگذاریم
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
$personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false;
|
||||
if (($permission && $permission->isRequireTwoStepStore()) || $personRequire) {
|
||||
$ticket->setStatus('pending_approval');
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire) {
|
||||
$ticket->setIsPreview(true);
|
||||
$ticket->setIsApproved(false);
|
||||
$ticket->setApprovedBy(null); // هنوز تأیید نشده
|
||||
} else {
|
||||
$ticket->setIsPreview(false);
|
||||
$ticket->setIsApproved(true);
|
||||
$ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی
|
||||
}
|
||||
//save logs
|
||||
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
||||
|
@ -660,7 +699,7 @@ class StoreroomController extends AbstractController
|
|||
if (!$ticket) {
|
||||
throw $this->createNotFoundException('حواله یافت نشد.');
|
||||
}
|
||||
$ticket->setStatus($status);
|
||||
// $ticket->setStatus($status);
|
||||
$entityManager->persist($ticket);
|
||||
$entityManager->flush();
|
||||
return $this->json(['result' => 0]);
|
||||
|
@ -687,7 +726,6 @@ class StoreroomController extends AbstractController
|
|||
'date' => $t->getDate(),
|
||||
'type' => $t->getType(),
|
||||
'typeString' => $t->getTypeString(),
|
||||
'status' => $t->getStatus(),
|
||||
'importWorkflowCode' => $t->getImportWorkflowCode(),
|
||||
'person' => $t->getPerson() ? $t->getPerson()->getNikename() : null,
|
||||
'storeroom' => $t->getStoreroom() ? $t->getStoreroom()->getName() : null,
|
||||
|
@ -708,15 +746,33 @@ class StoreroomController extends AbstractController
|
|||
], [
|
||||
'date' => 'DESC'
|
||||
]);
|
||||
return $this->json($provider->ArrayEntity2ArrayJustIncludes($tickets, [
|
||||
$result = $provider->ArrayEntity2ArrayJustIncludes($tickets, [
|
||||
'getDes',
|
||||
'getCode',
|
||||
'getDate',
|
||||
'getPerson',
|
||||
'getNikename',
|
||||
'getDoc',
|
||||
'getTypeString'
|
||||
]));
|
||||
'getTypeString',
|
||||
'isPreview',
|
||||
'isApproved'
|
||||
], 2);
|
||||
|
||||
foreach ($result as $key => &$ticket) {
|
||||
$ticketEntity = $tickets[$key];
|
||||
if ($ticketEntity->getApprovedBy()) {
|
||||
$approvedBy = $ticketEntity->getApprovedBy();
|
||||
$ticket['approvedBy'] = [
|
||||
'id' => $approvedBy->getId(),
|
||||
'fullName' => $approvedBy->getFullName(),
|
||||
'email' => $approvedBy->getEmail()
|
||||
];
|
||||
} else {
|
||||
$ticket['approvedBy'] = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->json($result);
|
||||
}
|
||||
|
||||
#[Route('/api/storeroom/tickets/info/{code}', name: 'app_storeroom_ticket_view')]
|
||||
|
@ -734,7 +790,7 @@ class StoreroomController extends AbstractController
|
|||
//get items
|
||||
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
||||
$res = [];
|
||||
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString'], 2);
|
||||
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2);
|
||||
$res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0);
|
||||
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
||||
//get rows
|
||||
|
@ -851,10 +907,12 @@ class StoreroomController extends AbstractController
|
|||
} else {
|
||||
$title = 'حواله خروج از انبار';
|
||||
}
|
||||
// جلوگیری از چاپ پیش از تایید در صورت نیاز
|
||||
$permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]);
|
||||
if ($permission && $permission->isRequireTwoStepStore()) {
|
||||
if ($doc->getStatus() !== 'approved' && $doc->getStatus() !== 'done') {
|
||||
|
||||
$business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']);
|
||||
$businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false;
|
||||
if ($businessRequire) {
|
||||
// بررسی وضعیت تأیید از طریق StoreroomTicket
|
||||
if ($doc->isPreview()) {
|
||||
return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -313,6 +313,18 @@ class Business
|
|||
#[ORM\OneToMany(mappedBy: 'business', targetEntity: ImportWorkflow::class, orphanRemoval: true)]
|
||||
private Collection $importWorkflows;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStepApproval = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $invoiceApprover = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $warehouseApprover = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $financialApprover = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->logs = new ArrayCollection();
|
||||
|
@ -2198,4 +2210,48 @@ class Business
|
|||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStepApproval(): ?bool
|
||||
{
|
||||
return $this->requireTwoStepApproval;
|
||||
}
|
||||
|
||||
public function setRequireTwoStepApproval(?bool $requireTwoStepApproval): static
|
||||
{
|
||||
$this->requireTwoStepApproval = $requireTwoStepApproval;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getInvoiceApprover(): ?string
|
||||
{
|
||||
return $this->invoiceApprover;
|
||||
}
|
||||
|
||||
public function setInvoiceApprover(?string $invoiceApprover): static
|
||||
{
|
||||
$this->invoiceApprover = $invoiceApprover;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWarehouseApprover(): ?string
|
||||
{
|
||||
return $this->warehouseApprover;
|
||||
}
|
||||
|
||||
public function setWarehouseApprover(?string $warehouseApprover): static
|
||||
{
|
||||
$this->warehouseApprover = $warehouseApprover;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getFinancialApprover(): ?string
|
||||
{
|
||||
return $this->financialApprover;
|
||||
}
|
||||
|
||||
public function setFinancialApprover(?string $financialApprover): static
|
||||
{
|
||||
$this->financialApprover = $financialApprover;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -726,4 +726,48 @@ class HesabdariDoc
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Approval fields
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $isPreview = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $isApproved = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $approvedBy = null;
|
||||
|
||||
public function isPreview(): ?bool
|
||||
{
|
||||
return $this->isPreview;
|
||||
}
|
||||
|
||||
public function setIsPreview(?bool $isPreview): static
|
||||
{
|
||||
$this->isPreview = $isPreview;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isApproved(): ?bool
|
||||
{
|
||||
return $this->isApproved;
|
||||
}
|
||||
|
||||
public function setIsApproved(?bool $isApproved): static
|
||||
{
|
||||
$this->isApproved = $isApproved;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApprovedBy(): ?User
|
||||
{
|
||||
return $this->approvedBy;
|
||||
}
|
||||
|
||||
public function setApprovedBy(?User $approvedBy): static
|
||||
{
|
||||
$this->approvedBy = $approvedBy;
|
||||
return $this;
|
||||
}
|
||||
}
|
|
@ -368,4 +368,6 @@ class HesabdariRow
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ use App\Repository\ImportWorkflowItemRepository;
|
|||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||
use App\Entity\Commodity;
|
||||
|
||||
#[ORM\Entity(repositoryClass: ImportWorkflowItemRepository::class)]
|
||||
class ImportWorkflowItem
|
||||
|
@ -20,6 +21,10 @@ class ImportWorkflowItem
|
|||
#[Ignore]
|
||||
private ?ImportWorkflow $importWorkflow = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?Commodity $commodity = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $name = null;
|
||||
|
||||
|
@ -86,6 +91,17 @@ class ImportWorkflowItem
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getCommodity(): ?Commodity
|
||||
{
|
||||
return $this->commodity;
|
||||
}
|
||||
|
||||
public function setCommodity(?Commodity $commodity): static
|
||||
{
|
||||
$this->commodity = $commodity;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getName(): ?string
|
||||
{
|
||||
return $this->name;
|
||||
|
|
|
@ -147,15 +147,6 @@ class Permission
|
|||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $importWorkflow = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStepSell = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStepPayment = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStepStore = null;
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
|
@ -688,37 +679,4 @@ class Permission
|
|||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStepSell(): ?bool
|
||||
{
|
||||
return $this->requireTwoStepSell;
|
||||
}
|
||||
|
||||
public function setRequireTwoStepSell(?bool $requireTwoStepSell): static
|
||||
{
|
||||
$this->requireTwoStepSell = $requireTwoStepSell;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStepPayment(): ?bool
|
||||
{
|
||||
return $this->requireTwoStepPayment;
|
||||
}
|
||||
|
||||
public function setRequireTwoStepPayment(?bool $requireTwoStepPayment): static
|
||||
{
|
||||
$this->requireTwoStepPayment = $requireTwoStepPayment;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStepStore(): ?bool
|
||||
{
|
||||
return $this->requireTwoStepStore;
|
||||
}
|
||||
|
||||
public function setRequireTwoStepStore(?bool $requireTwoStepStore): static
|
||||
{
|
||||
$this->requireTwoStepStore = $requireTwoStepStore;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -161,8 +161,7 @@ class Person
|
|||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||
private ?string $tags = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $requireTwoStep = null;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -917,14 +916,5 @@ class Person
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function isRequireTwoStep(): ?bool
|
||||
{
|
||||
return $this->requireTwoStep;
|
||||
}
|
||||
|
||||
public function setRequireTwoStep(?bool $requireTwoStep): self
|
||||
{
|
||||
$this->requireTwoStep = $requireTwoStep;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,212 +2,177 @@
|
|||
|
||||
namespace App\Entity;
|
||||
|
||||
use App\Repository\PlugWarrantySerialRepository;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||
|
||||
#[ORM\Entity(repositoryClass: PlugWarrantySerialRepository::class)]
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'plug_warranty_serial')]
|
||||
#[ORM\UniqueConstraint(name: 'uniq_warranty_serial', columns: ['serial_number'])]
|
||||
#[ORM\Index(name: 'idx_status_product', columns: ['status', 'commodity_id'])]
|
||||
#[ORM\Index(name: 'idx_alloc_doc', columns: ['allocated_to_document_id'])]
|
||||
class PlugWarrantySerial
|
||||
{
|
||||
public const STATUS_AVAILABLE = 'available';
|
||||
public const STATUS_ALLOCATED = 'allocated';
|
||||
public const STATUS_VERIFIED = 'verified';
|
||||
public const STATUS_BOUND = 'bound';
|
||||
public const STATUS_CONSUMED = 'consumed';
|
||||
public const STATUS_VOID = 'void';
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Ignore]
|
||||
private ?Business $bid = null;
|
||||
private ?Business $business = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
#[Ignore]
|
||||
private ?Commodity $commodity = null;
|
||||
|
||||
#[ORM\Column(length: 255, unique: true)]
|
||||
#[ORM\Column(name: 'serial_number', length: 255, unique: true)]
|
||||
private ?string $serialNumber = null;
|
||||
|
||||
#[ORM\Column(length: 25)]
|
||||
private ?string $dateSubmit = null;
|
||||
#[ORM\Column(type: 'datetime_immutable')]
|
||||
private \DateTimeImmutable $dateSubmit;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||
#[Ignore]
|
||||
#[ORM\ManyToOne]
|
||||
private ?User $submitter = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $description = null;
|
||||
|
||||
#[ORM\Column(length: 25, nullable: true)]
|
||||
private ?string $warrantyStartDate = null;
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $warrantyStartDate = null;
|
||||
|
||||
#[ORM\Column(length: 25, nullable: true)]
|
||||
private ?string $warrantyEndDate = null;
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $warrantyEndDate = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $status = 'active';
|
||||
#[ORM\Column(length: 20)]
|
||||
private string $status = self::STATUS_AVAILABLE;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[ORM\Column(type: 'string', length: 20)]
|
||||
private string $activation = 'deactive';
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $activationAt = null;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $notes = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $used = null;
|
||||
#[ORM\Column(name: 'allocated_to_document_id', type: 'integer', nullable: true)]
|
||||
private ?int $allocatedToDocumentId = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $usedAt = null;
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $allocatedAt = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $usedTicketCode = null;
|
||||
#[ORM\Column(name: 'bound_to_item_id', type: 'integer', nullable: true)]
|
||||
private ?int $boundToItemId = null;
|
||||
|
||||
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||
private ?\DateTimeImmutable $boundAt = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $voidReason = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 255, nullable: true)]
|
||||
private ?string $commoditySerial = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
private ?Person $buyer = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 32, nullable: true)]
|
||||
private ?string $activationTicketCode = null;
|
||||
|
||||
#[ORM\Column(type: 'string', length: 32, nullable: true)]
|
||||
private ?string $activationTicketSecret = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||
$this->dateSubmit = new \DateTimeImmutable();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
public function getId(): ?int { return $this->id; }
|
||||
|
||||
public function getBusiness(): ?Business { return $this->business; }
|
||||
public function setBusiness(?Business $business): self { $this->business = $business; return $this; }
|
||||
|
||||
public function getCommodity(): ?Commodity { return $this->commodity; }
|
||||
public function setCommodity(?Commodity $commodity): self { $this->commodity = $commodity; return $this; }
|
||||
|
||||
public function getSerialNumber(): ?string { return $this->serialNumber; }
|
||||
public function setSerialNumber(string $serialNumber): self { $this->serialNumber = $serialNumber; return $this; }
|
||||
|
||||
public function getDateSubmit(): \DateTimeImmutable { return $this->dateSubmit; }
|
||||
public function setDateSubmit(\DateTimeImmutable $d): self { $this->dateSubmit = $d; return $this; }
|
||||
|
||||
public function getSubmitter(): ?User { return $this->submitter; }
|
||||
public function setSubmitter(?User $submitter): self { $this->submitter = $submitter; return $this; }
|
||||
|
||||
public function getDescription(): ?string { return $this->description; }
|
||||
public function setDescription(?string $description): self { $this->description = $description; return $this; }
|
||||
|
||||
public function getWarrantyStartDate(): ?\DateTimeImmutable { return $this->warrantyStartDate; }
|
||||
public function setWarrantyStartDate(?\DateTimeImmutable $d): self { $this->warrantyStartDate = $d; return $this; }
|
||||
|
||||
public function getWarrantyEndDate(): ?\DateTimeImmutable { return $this->warrantyEndDate; }
|
||||
public function setWarrantyEndDate(?\DateTimeImmutable $d): self { $this->warrantyEndDate = $d; return $this; }
|
||||
|
||||
public function getStatus(): string { return $this->status; }
|
||||
public function setStatus(string $status): self { $this->status = $status; return $this; }
|
||||
|
||||
public function getActivation(): string { return $this->activation; }
|
||||
public function setActivation(string $activation): self { $this->activation = $activation; return $this; }
|
||||
|
||||
public function getActivationAt(): ?\DateTimeImmutable { return $this->activationAt; }
|
||||
public function setActivationAt(?\DateTimeImmutable $d): self { $this->activationAt = $d; return $this; }
|
||||
|
||||
public function getNotes(): ?string { return $this->notes; }
|
||||
public function setNotes(?string $notes): self { $this->notes = $notes; return $this; }
|
||||
|
||||
public function getAllocatedToDocumentId(): ?int { return $this->allocatedToDocumentId; }
|
||||
public function setAllocatedToDocumentId(?int $id): self { $this->allocatedToDocumentId = $id; return $this; }
|
||||
|
||||
public function getAllocatedAt(): ?\DateTimeImmutable { return $this->allocatedAt; }
|
||||
public function setAllocatedAt(?\DateTimeImmutable $d): self { $this->allocatedAt = $d; return $this; }
|
||||
|
||||
public function getBoundToItemId(): ?int { return $this->boundToItemId; }
|
||||
public function setBoundToItemId(?int $id): self { $this->boundToItemId = $id; return $this; }
|
||||
|
||||
public function getBoundAt(): ?\DateTimeImmutable { return $this->boundAt; }
|
||||
public function setBoundAt(?\DateTimeImmutable $d): self { $this->boundAt = $d; return $this; }
|
||||
|
||||
public function getVoidReason(): ?string { return $this->voidReason; }
|
||||
public function setVoidReason(?string $r): self { $this->voidReason = $r; return $this; }
|
||||
|
||||
public function getCommoditySerial(): ?string { return $this->commoditySerial; }
|
||||
public function setCommoditySerial(?string $commoditySerial): self { $this->commoditySerial = $commoditySerial; return $this; }
|
||||
|
||||
public function getBuyer(): ?Person { return $this->buyer; }
|
||||
public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; }
|
||||
|
||||
public function getActivationTicketCode(): ?string { return $this->activationTicketCode; }
|
||||
public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; }
|
||||
public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; }
|
||||
public function setActivationTicketSecret(?string $secret): self { $this->activationTicketSecret = $secret; return $this; }
|
||||
|
||||
public function isUsed(): bool {
|
||||
return $this->status === self::STATUS_CONSUMED;
|
||||
}
|
||||
|
||||
public function getBid(): ?Business
|
||||
{
|
||||
return $this->bid;
|
||||
}
|
||||
|
||||
public function setBid(?Business $bid): static
|
||||
{
|
||||
$this->bid = $bid;
|
||||
public function setUsed(bool $used): self {
|
||||
$this->status = $used ? self::STATUS_CONSUMED : self::STATUS_AVAILABLE;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCommodity(): ?Commodity
|
||||
{
|
||||
return $this->commodity;
|
||||
}
|
||||
|
||||
public function setCommodity(?Commodity $commodity): static
|
||||
{
|
||||
$this->commodity = $commodity;
|
||||
public function setUsedAt(string $usedAt): self {
|
||||
$this->usedAt = new \DateTimeImmutable($usedAt);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSerialNumber(): ?string
|
||||
{
|
||||
return $this->serialNumber;
|
||||
}
|
||||
|
||||
public function setSerialNumber(string $serialNumber): static
|
||||
{
|
||||
$this->serialNumber = $serialNumber;
|
||||
public function setUsedTicketCode(string $ticketCode): self {
|
||||
$this->usedTicketCode = $ticketCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDateSubmit(): ?string
|
||||
{
|
||||
return $this->dateSubmit;
|
||||
}
|
||||
|
||||
public function setDateSubmit(string $dateSubmit): static
|
||||
{
|
||||
$this->dateSubmit = $dateSubmit;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSubmitter(): ?User
|
||||
{
|
||||
return $this->submitter;
|
||||
}
|
||||
|
||||
public function setSubmitter(?User $submitter): static
|
||||
{
|
||||
$this->submitter = $submitter;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDescription(): ?string
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
public function setDescription(?string $description): static
|
||||
{
|
||||
$this->description = $description;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWarrantyStartDate(): ?string
|
||||
{
|
||||
return $this->warrantyStartDate;
|
||||
}
|
||||
|
||||
public function setWarrantyStartDate(?string $warrantyStartDate): static
|
||||
{
|
||||
$this->warrantyStartDate = $warrantyStartDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWarrantyEndDate(): ?string
|
||||
{
|
||||
return $this->warrantyEndDate;
|
||||
}
|
||||
|
||||
public function setWarrantyEndDate(?string $warrantyEndDate): static
|
||||
{
|
||||
$this->warrantyEndDate = $warrantyEndDate;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(?string $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNotes(): ?string
|
||||
{
|
||||
return $this->notes;
|
||||
}
|
||||
|
||||
public function setNotes(?string $notes): static
|
||||
{
|
||||
$this->notes = $notes;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isUsed(): ?bool
|
||||
{
|
||||
return $this->used;
|
||||
}
|
||||
|
||||
public function setUsed(?bool $used): static
|
||||
{
|
||||
$this->used = $used;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsedAt(): ?string
|
||||
{
|
||||
return $this->usedAt;
|
||||
}
|
||||
|
||||
public function setUsedAt(?string $usedAt): static
|
||||
{
|
||||
$this->usedAt = $usedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsedTicketCode(): ?string
|
||||
{
|
||||
return $this->usedTicketCode;
|
||||
}
|
||||
|
||||
public function setUsedTicketCode(?string $usedTicketCode): static
|
||||
{
|
||||
$this->usedTicketCode = $usedTicketCode;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -77,12 +77,25 @@ class StoreroomTicket
|
|||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $canShare = null;
|
||||
|
||||
#[ORM\Column(length: 50, nullable: true)]
|
||||
private ?string $status = null; // in_progress | done | rejected
|
||||
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
private ?string $importWorkflowCode = null;
|
||||
|
||||
#[ORM\Column(length: 32, nullable: true)]
|
||||
private ?string $activationCode = null;
|
||||
|
||||
// Approval fields
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $isPreview = null;
|
||||
|
||||
#[ORM\Column(nullable: true)]
|
||||
private ?bool $isApproved = null;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(nullable: true)]
|
||||
private ?User $approvedBy = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->storeroomItems = new ArrayCollection();
|
||||
|
@ -339,16 +352,7 @@ class StoreroomTicket
|
|||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?string
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(?string $status): static
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getImportWorkflowCode(): ?string
|
||||
{
|
||||
|
@ -360,4 +364,49 @@ class StoreroomTicket
|
|||
$this->importWorkflowCode = $importWorkflowCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getActivationCode(): ?string
|
||||
{
|
||||
return $this->activationCode;
|
||||
}
|
||||
|
||||
public function setActivationCode(?string $activationCode): static
|
||||
{
|
||||
$this->activationCode = $activationCode;
|
||||
return $this;
|
||||
}
|
||||
|
||||
// Approval methods
|
||||
public function isPreview(): ?bool
|
||||
{
|
||||
return $this->isPreview;
|
||||
}
|
||||
|
||||
public function setIsPreview(?bool $isPreview): static
|
||||
{
|
||||
$this->isPreview = $isPreview;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isApproved(): ?bool
|
||||
{
|
||||
return $this->isApproved;
|
||||
}
|
||||
|
||||
public function setIsApproved(?bool $isApproved): static
|
||||
{
|
||||
$this->isApproved = $isApproved;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getApprovedBy(): ?User
|
||||
{
|
||||
return $this->approvedBy;
|
||||
}
|
||||
|
||||
public function setApprovedBy(?User $approvedBy): static
|
||||
{
|
||||
$this->approvedBy = $approvedBy;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,7 +22,8 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
|||
public function findByBusiness($bid): array
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.bid = :val')
|
||||
->join('p.business', 'b')
|
||||
->andWhere('b.id = :val')
|
||||
->setParameter('val', $bid)
|
||||
->orderBy('p.dateSubmit', 'DESC')
|
||||
->getQuery()
|
||||
|
@ -36,7 +37,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
|||
public function findByCommodity($bid, $commodityId): array
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.bid = :bid')
|
||||
->andWhere('p.business = :bid')
|
||||
->andWhere('p.commodity = :commodityId')
|
||||
->setParameter('bid', $bid)
|
||||
->setParameter('commodityId', $commodityId)
|
||||
|
@ -52,7 +53,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
|||
public function findByStatus($bid, $status): array
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.bid = :bid')
|
||||
->andWhere('p.business = :bid')
|
||||
->andWhere('p.status = :status')
|
||||
->setParameter('bid', $bid)
|
||||
->setParameter('status', $status)
|
||||
|
@ -72,7 +73,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
|||
->setParameter('serialNumber', $serialNumber);
|
||||
|
||||
if ($bid) {
|
||||
$qb->andWhere('p.bid = :bid')
|
||||
$qb->andWhere('p.business = :bid')
|
||||
->setParameter('bid', $bid);
|
||||
}
|
||||
|
||||
|
@ -86,7 +87,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
|||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->leftJoin('p.commodity', 'c')
|
||||
->andWhere('p.bid = :bid')
|
||||
->andWhere('p.business = :bid')
|
||||
->andWhere('p.serialNumber LIKE :keyword OR c.name LIKE :keyword OR p.description LIKE :keyword')
|
||||
->setParameter('bid', $bid)
|
||||
->setParameter('keyword', '%' . $keyword . '%')
|
||||
|
|
|
@ -171,6 +171,10 @@ class Explore
|
|||
'amount' => $doc->getAmount(),
|
||||
'mdate' => '',
|
||||
'plugin' => $doc->getPlugin(),
|
||||
// Approval fields
|
||||
'isPreview' => $doc->isPreview(),
|
||||
'isApproved' => $doc->isApproved(),
|
||||
'approvedBy' => $doc->getApprovedBy() ? self::ExploreUser($doc->getApprovedBy()) : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -210,6 +214,9 @@ class Explore
|
|||
$row->setDiscount(0);
|
||||
$temp['tax'] = $row->getTax();
|
||||
$temp['discount'] = $row->getDiscount();
|
||||
|
||||
// Approval fields - از سند اصلی گرفته میشود
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
|
@ -347,7 +354,7 @@ class Explore
|
|||
'address' => $person->getAddress(),
|
||||
'prelabel' => null,
|
||||
'tags' => $person->getTags(),
|
||||
'requireTwoStep' => $person->isRequireTwoStep(),
|
||||
'requireTwoStep' => $person->getBid() ? $person->getBid()->isRequireTwoStepApproval() : false,
|
||||
];
|
||||
if ($person->getPrelabel()) {
|
||||
$res['prelabel'] = $person->getPrelabel()->getLabel();
|
||||
|
@ -571,6 +578,10 @@ class Explore
|
|||
'shortlinks' => $item->isShortlinks(),
|
||||
'walletEnabled' => $item->isWalletEnable(),
|
||||
'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null,
|
||||
'requireTwoStepApproval' => $item->isRequireTwoStepApproval(),
|
||||
'invoiceApprover' => $item->getInvoiceApprover(),
|
||||
'warehouseApprover' => $item->getWarehouseApprover(),
|
||||
'financialApprover' => $item->getFinancialApprover(),
|
||||
'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(),
|
||||
'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(),
|
||||
];
|
||||
|
|
42
hesabixCore/src/Service/FileStorage.php
Normal file
42
hesabixCore/src/Service/FileStorage.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class FileStorage
|
||||
{
|
||||
public function __construct(private KernelInterface $kernel)
|
||||
{
|
||||
}
|
||||
|
||||
public function store(UploadedFile $file, string $businessId, string $context = 'general'): array
|
||||
{
|
||||
$safeOriginal = preg_replace('/[^A-Za-z0-9_.-]/', '_', $file->getClientOriginalName());
|
||||
$relativeDir = 'storage/' . trim($businessId) . '/' . trim($context);
|
||||
$absDir = rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativeDir;
|
||||
if (!is_dir($absDir)) {
|
||||
@mkdir($absDir, 0775, true);
|
||||
}
|
||||
$newName = uniqid('f_', true) . '_' . $safeOriginal;
|
||||
$file->move($absDir, $newName);
|
||||
$absPath = $absDir . '/' . $newName;
|
||||
$size = @filesize($absPath) ?: null;
|
||||
$mime = function_exists('mime_content_type') ? @mime_content_type($absPath) : null;
|
||||
return [
|
||||
'relativePath' => $relativeDir . '/' . $newName,
|
||||
'originalName' => $file->getClientOriginalName(),
|
||||
'size' => $size,
|
||||
'mime' => $mime ?: $file->getClientMimeType(),
|
||||
];
|
||||
}
|
||||
|
||||
public function absolutePath(string $relativePath): string
|
||||
{
|
||||
$relativePath = ltrim($relativePath, '/');
|
||||
return rtrim($this->kernel->getProjectDir(), '/').'/var/' . $relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 110 KiB |
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
Binary file not shown.
After Width: | Height: | Size: 2 KiB |
Binary file not shown.
|
@ -41,6 +41,7 @@
|
|||
"maz-ui": "^3.50.1",
|
||||
"monaco-editor": "^0.52.2",
|
||||
"pinia": "^3.0.2",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"sweetalert2": "^11.4.8",
|
||||
"v-money3": "^3.24.1",
|
||||
"vue": "^3.5.13",
|
||||
|
|
268
webUI/src/components/common/ApprovalManager.vue
Normal file
268
webUI/src/components/common/ApprovalManager.vue
Normal file
|
@ -0,0 +1,268 @@
|
|||
<template>
|
||||
<div class="approval-manager">
|
||||
<!-- دکمه تأیید -->
|
||||
<v-btn
|
||||
v-if="showApprovalButton"
|
||||
:color="approvalButtonColor"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@click="showApprovalDialog = true"
|
||||
:loading="processing"
|
||||
:disabled="processing"
|
||||
>
|
||||
<v-icon size="small" class="me-1">
|
||||
{{ approvalButtonIcon }}
|
||||
</v-icon>
|
||||
{{ approvalButtonText }}
|
||||
</v-btn>
|
||||
|
||||
<!-- دکمه رد -->
|
||||
<v-btn
|
||||
v-if="showRejectButton"
|
||||
color="error"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@click="showRejectDialog = true"
|
||||
:loading="processing"
|
||||
:disabled="processing"
|
||||
class="ms-2"
|
||||
>
|
||||
<v-icon size="small" class="me-1">mdi-close</v-icon>
|
||||
رد سند
|
||||
</v-btn>
|
||||
|
||||
<!-- دیالوگ تأیید -->
|
||||
<v-dialog v-model="showApprovalDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon color="success" class="me-2">mdi-check-circle</v-icon>
|
||||
تأیید سند
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>آیا از تأیید این سند اطمینان دارید؟</p>
|
||||
<div class="mt-3">
|
||||
<strong>نوع سند:</strong> {{ documentTypeText }}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<strong>شماره سند:</strong> {{ documentNumber }}
|
||||
</div>
|
||||
<div class="mt-1">
|
||||
<strong>مبلغ:</strong> {{ formatCurrency(document.amount || 0) }}
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" @click="showApprovalDialog = false">انصراف</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
@click="handleApproval"
|
||||
:loading="processing"
|
||||
>
|
||||
تأیید
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- دیالوگ رد -->
|
||||
<v-dialog v-model="showRejectDialog" max-width="500">
|
||||
<v-card>
|
||||
<v-card-title class="text-h6">
|
||||
<v-icon color="error" class="me-2">mdi-close-circle</v-icon>
|
||||
رد سند
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<p>لطفاً دلیل رد این سند را وارد کنید:</p>
|
||||
<v-textarea
|
||||
v-model="rejectionReason"
|
||||
label="دلیل رد"
|
||||
variant="outlined"
|
||||
rows="3"
|
||||
:rules="[v => !!v || 'دلیل رد الزامی است']"
|
||||
required
|
||||
></v-textarea>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="grey" @click="showRejectDialog = false">انصراف</v-btn>
|
||||
<v-btn
|
||||
color="error"
|
||||
@click="handleRejection"
|
||||
:loading="processing"
|
||||
:disabled="!rejectionReason"
|
||||
>
|
||||
رد سند
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- Snackbar برای نمایش پیامها -->
|
||||
<v-snackbar
|
||||
v-model="showSnackbar"
|
||||
:color="snackbarColor"
|
||||
:timeout="3000"
|
||||
location="bottom"
|
||||
>
|
||||
{{ snackbarText }}
|
||||
<template v-slot:actions>
|
||||
<v-btn icon variant="text" @click="showSnackbar = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import {
|
||||
shouldShowApprovalButton,
|
||||
getApprovalButtonText,
|
||||
getDocumentType
|
||||
} from '@/utils/approvalUtils'
|
||||
|
||||
const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
businessSettings: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
currentUserEmail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isBusinessOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['approve', 'reject'])
|
||||
|
||||
const showApprovalDialog = ref(false)
|
||||
const showRejectDialog = ref(false)
|
||||
const rejectionReason = ref('')
|
||||
const processing = ref(false)
|
||||
const showSnackbar = ref(false)
|
||||
const snackbarText = ref('')
|
||||
const snackbarColor = ref('success')
|
||||
|
||||
// محاسبه نمایش دکمهها
|
||||
const showApprovalButton = computed(() => {
|
||||
return shouldShowApprovalButton(
|
||||
props.businessSettings,
|
||||
props.document,
|
||||
props.currentUserEmail,
|
||||
props.isBusinessOwner
|
||||
)
|
||||
})
|
||||
|
||||
const showRejectButton = computed(() => {
|
||||
// فقط مدیر کسب و کار میتواند سند را رد کند
|
||||
return props.isBusinessOwner && props.document.approved !== false
|
||||
})
|
||||
|
||||
// محاسبه متن و رنگ دکمه تأیید
|
||||
const approvalButtonText = computed(() => getApprovalButtonText(props.document))
|
||||
const approvalButtonColor = computed(() => {
|
||||
if (props.document.approved === false) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
const approvalButtonIcon = computed(() => {
|
||||
if (props.document.approved === false) return 'mdi-refresh'
|
||||
return 'mdi-check'
|
||||
})
|
||||
|
||||
// محاسبه نوع سند
|
||||
const documentType = computed(() => getDocumentType(props.document))
|
||||
const documentTypeText = computed(() => {
|
||||
switch (documentType.value) {
|
||||
case 'invoice':
|
||||
return 'فاکتور فروش'
|
||||
case 'warehouse':
|
||||
return 'حواله انبار'
|
||||
case 'financial':
|
||||
return 'سند مالی'
|
||||
default:
|
||||
return 'سند'
|
||||
}
|
||||
})
|
||||
|
||||
const documentNumber = computed(() => {
|
||||
return props.document.invoiceNumber ||
|
||||
props.document.warehouseNumber ||
|
||||
props.document.documentNumber ||
|
||||
props.document.id ||
|
||||
'نامشخص'
|
||||
})
|
||||
|
||||
// مدیریت تأیید
|
||||
const handleApproval = async () => {
|
||||
try {
|
||||
processing.value = true
|
||||
showApprovalDialog.value = false
|
||||
|
||||
await emit('approve', props.document)
|
||||
|
||||
showSnackbarText('سند با موفقیت تأیید شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در تأیید سند:', error)
|
||||
showSnackbarText('خطا در تأیید سند', 'error')
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// مدیریت رد
|
||||
const handleRejection = async () => {
|
||||
try {
|
||||
processing.value = true
|
||||
showRejectDialog.value = false
|
||||
|
||||
await emit('reject', {
|
||||
document: props.document,
|
||||
reason: rejectionReason.value
|
||||
})
|
||||
|
||||
rejectionReason.value = ''
|
||||
showSnackbarText('سند با موفقیت رد شد', 'success')
|
||||
} catch (error) {
|
||||
console.error('خطا در رد سند:', error)
|
||||
showSnackbarText('خطا در رد سند', 'error')
|
||||
} finally {
|
||||
processing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// نمایش پیام
|
||||
const showSnackbarText = (text, color = 'success') => {
|
||||
snackbarText.value = text
|
||||
snackbarColor.value = color
|
||||
showSnackbar.value = true
|
||||
}
|
||||
|
||||
// فرمت مبلغ
|
||||
const formatCurrency = (amount) => {
|
||||
return new Intl.NumberFormat('fa-IR').format(amount) + ' ریال'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-manager {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.approval-manager .v-btn {
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
163
webUI/src/components/common/ApprovalStatus.vue
Normal file
163
webUI/src/components/common/ApprovalStatus.vue
Normal file
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<div class="approval-status">
|
||||
<!-- نمایش وضعیت تأیید -->
|
||||
<v-chip
|
||||
:color="statusColor"
|
||||
size="small"
|
||||
class="me-2"
|
||||
>
|
||||
<v-icon size="small" class="me-1">
|
||||
{{ statusIcon }}
|
||||
</v-icon>
|
||||
{{ statusText }}
|
||||
</v-chip>
|
||||
|
||||
<!-- دکمه تأیید (فقط اگر کاربر مجوز داشته باشد) -->
|
||||
<v-btn
|
||||
v-if="showApprovalButton"
|
||||
:color="approvalButtonColor"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
@click="handleApproval"
|
||||
:loading="approving"
|
||||
:disabled="approving"
|
||||
>
|
||||
<v-icon size="small" class="me-1">
|
||||
{{ approvalButtonIcon }}
|
||||
</v-icon>
|
||||
{{ approvalButtonText }}
|
||||
</v-btn>
|
||||
|
||||
<!-- اطلاعات تأییدکننده -->
|
||||
<div v-if="document.approvedBy" class="text-caption text-medium-emphasis mt-1">
|
||||
تایید شده توسط: {{ document.approvedBy.name || document.approvedBy }}
|
||||
<span v-if="document.approvedAt">
|
||||
در تاریخ: {{ formatDate(document.approvedAt) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- اطلاعات ردکننده -->
|
||||
<div v-if="document.rejectedBy" class="text-caption text-error mt-1">
|
||||
رد شده توسط: {{ document.rejectedBy.name || document.rejectedBy }}
|
||||
<span v-if="document.rejectedAt">
|
||||
در تاریخ: {{ formatDate(document.rejectedAt) }}
|
||||
</span>
|
||||
<span v-if="document.rejectionReason" class="ms-2">
|
||||
دلیل: {{ document.rejectionReason }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import {
|
||||
getApprovalStatusText,
|
||||
getApprovalStatusColor,
|
||||
shouldShowApprovalButton,
|
||||
getApprovalButtonText,
|
||||
getDocumentType
|
||||
} from '@/utils/approvalUtils'
|
||||
|
||||
const props = defineProps({
|
||||
document: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
businessSettings: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
currentUserEmail: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
isBusinessOwner: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['approve', 'reject'])
|
||||
|
||||
const approving = ref(false)
|
||||
|
||||
// محاسبه وضعیت تأیید
|
||||
const statusText = computed(() => getApprovalStatusText(props.document))
|
||||
const statusColor = computed(() => getApprovalStatusColor(props.document))
|
||||
const statusIcon = computed(() => {
|
||||
if (props.document.approved === true) return 'mdi-check-circle'
|
||||
if (props.document.approved === false) return 'mdi-close-circle'
|
||||
return 'mdi-clock-outline'
|
||||
})
|
||||
|
||||
// محاسبه دکمه تأیید
|
||||
const showApprovalButton = computed(() => {
|
||||
return shouldShowApprovalButton(
|
||||
props.businessSettings,
|
||||
props.document,
|
||||
props.currentUserEmail,
|
||||
props.isBusinessOwner
|
||||
)
|
||||
})
|
||||
|
||||
const approvalButtonText = computed(() => getApprovalButtonText(props.document))
|
||||
const approvalButtonColor = computed(() => {
|
||||
if (props.document.approved === false) return 'warning'
|
||||
return 'success'
|
||||
})
|
||||
const approvalButtonIcon = computed(() => {
|
||||
if (props.document.approved === false) return 'mdi-refresh'
|
||||
return 'mdi-check'
|
||||
})
|
||||
|
||||
// مدیریت تأیید
|
||||
const handleApproval = async () => {
|
||||
try {
|
||||
approving.value = true
|
||||
|
||||
// اگر سند رد شده، برای تأیید مجدد ارسال کن
|
||||
if (props.document.approved === false) {
|
||||
await emit('approve', props.document)
|
||||
} else {
|
||||
// تأیید عادی
|
||||
await emit('approve', props.document)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('خطا در تأیید سند:', error)
|
||||
} finally {
|
||||
approving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// فرمت تاریخ
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
|
||||
try {
|
||||
const dateObj = new Date(date)
|
||||
if (isNaN(dateObj.getTime())) return date
|
||||
|
||||
return dateObj.toLocaleDateString('fa-IR')
|
||||
} catch (error) {
|
||||
return date
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.approval-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.approval-status .v-chip {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.approval-status .v-btn {
|
||||
text-transform: none;
|
||||
}
|
||||
</style>
|
|
@ -22,7 +22,7 @@
|
|||
no-data-text="اطلاعات ترخیص گمرکی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.totalCustomsCharges="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.totalCustomsCharges) }}
|
||||
<small class="text-medium-emphasis">ریال</small>
|
||||
</div>
|
||||
|
@ -128,7 +128,7 @@
|
|||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(totalCharges)"
|
||||
:model-value="formatMoney(totalChargesNumeric)"
|
||||
label="کل هزینههای گمرکی"
|
||||
readonly
|
||||
></v-text-field>
|
||||
|
@ -281,13 +281,13 @@ const formatMoney = (value) => {
|
|||
}
|
||||
|
||||
// Computed
|
||||
const totalCharges = computed(() => {
|
||||
const totalChargesNumeric = computed(() => {
|
||||
const duty = parseFloat(formData.value.customsDuty) || 0
|
||||
const vat = parseFloat(formData.value.valueAddedTax) || 0
|
||||
const other = parseFloat(formData.value.otherCharges) || 0
|
||||
const total = duty + vat + other
|
||||
formData.value.totalCustomsCharges = total.toString()
|
||||
return formatNumber(total)
|
||||
return total
|
||||
})
|
||||
|
||||
// Methods
|
||||
|
|
|
@ -44,7 +44,7 @@
|
|||
|
||||
<template v-slot:item.actions="{ item }">
|
||||
<v-btn
|
||||
v-if="item.filePath"
|
||||
v-if="item.id"
|
||||
icon="mdi-download"
|
||||
size="small"
|
||||
variant="text"
|
||||
|
@ -116,11 +116,13 @@
|
|||
<v-row>
|
||||
<v-col cols="12">
|
||||
<v-file-input
|
||||
v-model="selectedFile"
|
||||
:model-value="fileInputValue"
|
||||
label="فایل سند"
|
||||
accept=".pdf,.jpg,.jpeg,.png,.doc,.docx"
|
||||
prepend-icon="mdi-paperclip"
|
||||
:multiple="false"
|
||||
:rules="editingDocument ? [] : [rules.fileRequired]"
|
||||
@update:modelValue="onFileChange"
|
||||
></v-file-input>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -183,7 +185,8 @@ const editingDocument = ref(null)
|
|||
const form = ref()
|
||||
const valid = ref(false)
|
||||
const saveLoading = ref(false)
|
||||
const selectedFile = ref([])
|
||||
const selectedFile = ref(null)
|
||||
const fileInputValue = ref([])
|
||||
|
||||
const formData = ref({
|
||||
type: '',
|
||||
|
@ -220,7 +223,7 @@ const documentTypes = [
|
|||
// Validation rules
|
||||
const rules = {
|
||||
required: (value) => !!value || 'این فیلد الزامی است',
|
||||
fileRequired: (value) => (value && value.length > 0) || 'انتخاب فایل الزامی است'
|
||||
fileRequired: (value) => !!selectedFile.value || 'انتخاب فایل الزامی است'
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
@ -269,15 +272,15 @@ const saveDocument = async () => {
|
|||
})
|
||||
|
||||
// Add file if selected
|
||||
if (selectedFile.value && selectedFile.value.length > 0) {
|
||||
formDataToSend.append('file', selectedFile.value[0])
|
||||
if (selectedFile.value) {
|
||||
formDataToSend.append('file', selectedFile.value)
|
||||
}
|
||||
|
||||
const url = editingDocument.value
|
||||
? `/api/import-workflow/${props.workflowId}/documents/${editingDocument.value.id}/update`
|
||||
: `/api/import-workflow/${props.workflowId}/documents/create`
|
||||
|
||||
const method = editingDocument.value ? 'PUT' : 'POST'
|
||||
const method = 'POST'
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
|
@ -311,15 +314,48 @@ const saveDocument = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const downloadFile = (document) => {
|
||||
if (document.filePath) {
|
||||
window.open(`/api/import-workflow/documents/${document.id}/download`, '_blank')
|
||||
const onFileChange = (val) => {
|
||||
// Normalize input to single File
|
||||
if (Array.isArray(val)) {
|
||||
fileInputValue.value = val
|
||||
selectedFile.value = val.length > 0 ? val[0] : null
|
||||
} else {
|
||||
fileInputValue.value = val ? [val] : []
|
||||
selectedFile.value = val || null
|
||||
}
|
||||
}
|
||||
|
||||
const downloadFile = async (doc) => {
|
||||
try {
|
||||
const url = `/api/import-workflow/documents/${doc.id}/download`
|
||||
const res = await axios.get(url, { responseType: 'blob' })
|
||||
|
||||
// Try to extract filename from headers
|
||||
const cd = (res.headers && (res.headers['content-disposition'] || res.headers['Content-Disposition'])) || ''
|
||||
let filename = doc.fileName || `document-${doc.id}`
|
||||
const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(cd)
|
||||
if (match) {
|
||||
filename = decodeURIComponent(match[1] || match[2] || filename)
|
||||
}
|
||||
|
||||
const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' })
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const link = window.document.createElement('a')
|
||||
link.href = blobUrl
|
||||
link.download = filename
|
||||
window.document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
} catch (e) {
|
||||
Swal.fire('خطا', 'دانلود فایل ناموفق بود', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
editingDocument.value = null
|
||||
selectedFile.value = []
|
||||
selectedFile.value = null
|
||||
fileInputValue.value = []
|
||||
formData.value = {
|
||||
type: '',
|
||||
title: '',
|
||||
|
|
|
@ -22,14 +22,14 @@
|
|||
no-data-text="آیتمی ثبت نشده است"
|
||||
>
|
||||
<template v-slot:item.unitPrice="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.unitPrice) }}
|
||||
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.totalPrice="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.totalPrice) }}
|
||||
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||
</div>
|
||||
|
@ -64,23 +64,30 @@
|
|||
|
||||
<v-form ref="form" v-model="valid" @submit.prevent="saveItem">
|
||||
<v-card-text>
|
||||
<!-- Commodity selector / viewer -->
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.name"
|
||||
label="نام کالا"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.productCode"
|
||||
label="کد محصول"
|
||||
></v-text-field>
|
||||
<v-col cols="12">
|
||||
<template v-if="!editingItem">
|
||||
<Hcommoditysearch
|
||||
v-model="selectedCommodity"
|
||||
:return-object="true"
|
||||
label="انتخاب کالا"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<v-text-field
|
||||
:model-value="selectedCommodity ? (selectedCommodity.name + (selectedCommodity.code ? ` (${selectedCommodity.code})` : '')) : ''"
|
||||
label="کالا"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
disabled
|
||||
/>
|
||||
</template>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
|
@ -142,7 +149,7 @@
|
|||
</v-col>
|
||||
<v-col cols="12" md="4">
|
||||
<v-text-field
|
||||
:model-value="formatMoney(totalPrice)"
|
||||
:model-value="(formData.quantity && formData.unitPrice) ? formatMoney(totalPrice) : ''"
|
||||
label="قیمت کل"
|
||||
readonly
|
||||
></v-text-field>
|
||||
|
@ -232,6 +239,7 @@
|
|||
import { ref, computed, watch } from 'vue'
|
||||
import axios from 'axios'
|
||||
import Swal from 'sweetalert2'
|
||||
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
|
@ -242,6 +250,10 @@ const props = defineProps({
|
|||
items: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'USD'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -258,6 +270,16 @@ const valid = ref(false)
|
|||
const saveLoading = ref(false)
|
||||
const deleteLoading = ref(false)
|
||||
|
||||
// Commodity selection
|
||||
const selectedCommodity = ref(null)
|
||||
watch(selectedCommodity, (val) => {
|
||||
if (val) {
|
||||
// Auto-fill name and product code from selected commodity
|
||||
formData.value.name = val.name || ''
|
||||
formData.value.productCode = val.code || ''
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
productCode: '',
|
||||
|
@ -322,9 +344,9 @@ const totalPrice = computed(() => {
|
|||
if (formData.value.quantity && formData.value.unitPrice) {
|
||||
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPrice)
|
||||
formData.value.totalPrice = total.toString()
|
||||
return formatNumber(total)
|
||||
return total
|
||||
}
|
||||
return ''
|
||||
return 0
|
||||
})
|
||||
|
||||
// Watch for unit price IRR and quantity changes
|
||||
|
@ -340,6 +362,12 @@ const editItem = (item) => {
|
|||
editingItem.value = item
|
||||
formData.value = { ...item }
|
||||
showAddDialog.value = true
|
||||
// set selected commodity from item if exists
|
||||
if (item && item.commodity) {
|
||||
selectedCommodity.value = item.commodity
|
||||
} else {
|
||||
selectedCommodity.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const deleteItem = (item) => {
|
||||
|
@ -349,6 +377,10 @@ const deleteItem = (item) => {
|
|||
|
||||
const saveItem = async () => {
|
||||
if (!valid.value) return
|
||||
if (!selectedCommodity.value) {
|
||||
Swal.fire({ title: 'هشدار', text: 'انتخاب کالا الزامی است', icon: 'warning' })
|
||||
return
|
||||
}
|
||||
|
||||
saveLoading.value = true
|
||||
try {
|
||||
|
@ -358,10 +390,15 @@ const saveItem = async () => {
|
|||
|
||||
const method = editingItem.value ? 'PUT' : 'POST'
|
||||
|
||||
const payload = {
|
||||
...formData.value,
|
||||
commodity_id: selectedCommodity.value.id,
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method,
|
||||
url,
|
||||
data: formData.value
|
||||
data: payload
|
||||
})
|
||||
|
||||
if (response.data.Success) {
|
||||
|
@ -439,16 +476,18 @@ const cancelEdit = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Backward-compat handler for template bindings
|
||||
const closeDialog = () => {
|
||||
cancelEdit()
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const formatNumber = (number) => {
|
||||
if (!number) return '0'
|
||||
return new Intl.NumberFormat('fa-IR').format(number)
|
||||
}
|
||||
|
||||
const getCurrency = (item) => {
|
||||
// This should be based on the workflow's currency
|
||||
return 'USD'
|
||||
}
|
||||
const getCurrency = () => props.currency || 'USD'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -32,14 +32,14 @@
|
|||
</template>
|
||||
|
||||
<template v-slot:item.amount="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.amount) }}
|
||||
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-slot:item.amountIRR="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.amountIRR) }}
|
||||
<small class="text-medium-emphasis">ریال</small>
|
||||
</div>
|
||||
|
@ -49,7 +49,6 @@
|
|||
<v-chip
|
||||
:color="getStatusColor(item.status)"
|
||||
size="small"
|
||||
variant="flat"
|
||||
>
|
||||
{{ getStatusText(item.status) }}
|
||||
</v-chip>
|
||||
|
|
|
@ -118,13 +118,13 @@
|
|||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.originPort"
|
||||
label="بندر مبدا"
|
||||
label="محل مبدا حمل"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.destinationPort"
|
||||
label="بندر مقصد"
|
||||
label="محل مقصد حمل"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -133,13 +133,13 @@
|
|||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.vesselName"
|
||||
label="نام کشتی"
|
||||
label="نام وسیله/حامل"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.voyageNumber"
|
||||
label="شماره سفر"
|
||||
label="شماره سفر/پرواز"
|
||||
></v-text-field>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
</v-btn>
|
||||
</div>
|
||||
|
||||
<v-timeline direction="horizontal" class="mb-4">
|
||||
<v-timeline :direction="$vuetify.display.smAndDown ? 'vertical' : 'horizontal'" class="mb-4">
|
||||
<v-timeline-item
|
||||
v-for="stage in stages"
|
||||
:key="stage.id"
|
||||
|
@ -350,6 +350,22 @@ const cancelEdit = () => {
|
|||
closeDialog()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
stage: '',
|
||||
status: 'pending',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
assignedTo: '',
|
||||
description: '',
|
||||
notes: ''
|
||||
}
|
||||
if (form.value) {
|
||||
try { form.value.reset() } catch (e) {}
|
||||
try { form.value.resetValidation && form.value.resetValidation() } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
// Utilities
|
||||
const getStageIcon = (stage) => {
|
||||
const icons = {
|
||||
|
|
|
@ -137,16 +137,55 @@
|
|||
<v-window-item value="file">
|
||||
<div class="mt-4">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-subtitle-1 mb-3">آپلود فایل Excel</div>
|
||||
<div class="d-flex justify-space-between align-center mb-3">
|
||||
<div class="text-subtitle-1">آپلود فایل Excel یا CSV</div>
|
||||
<div class="d-flex gap-2">
|
||||
<v-btn
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
prepend-icon="mdi-download"
|
||||
@click="downloadSampleFile"
|
||||
>
|
||||
دانلود CSV نمونه
|
||||
</v-btn>
|
||||
<v-btn
|
||||
color="success"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
prepend-icon="mdi-download"
|
||||
@click="downloadExcelSample"
|
||||
>
|
||||
دانلود Excel نمونه
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<v-alert
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="mb-3"
|
||||
>
|
||||
<div class="text-body-2">
|
||||
<strong>نکات مهم:</strong>
|
||||
<ul class="mt-2 mb-0">
|
||||
<li>فایل باید شامل ستونهای: شماره سریال، کد کالا، توضیحات، شروع گارانتی، پایان گارانتی، وضعیت باشد</li>
|
||||
<li>شماره سریال و کد کالا فیلدهای الزامی هستند</li>
|
||||
<li>کد کالا باید دقیقاً مطابق با کد موجود در سیستم باشد</li>
|
||||
<li>تاریخها باید به فرمت YYYY/MM/DD یا YYYY-MM-DD باشند</li>
|
||||
<li>وضعیت میتواند: active، inactive، یا expired باشد</li>
|
||||
</ul>
|
||||
</div>
|
||||
</v-alert>
|
||||
|
||||
<v-file-input
|
||||
v-model="uploadedFile"
|
||||
label="انتخاب فایل"
|
||||
accept=".xlsx,.xls"
|
||||
prepend-icon="mdi-file-excel"
|
||||
accept=".xlsx,.xls,.csv"
|
||||
prepend-icon="mdi-file-upload"
|
||||
show-size
|
||||
counter
|
||||
@change="handleFileUpload"
|
||||
@update:model-value="handleFileUpload"
|
||||
></v-file-input>
|
||||
|
||||
<v-alert
|
||||
|
@ -213,7 +252,7 @@ const emit = defineEmits<{
|
|||
|
||||
const loading = ref(false)
|
||||
const activeTab = ref('manual')
|
||||
const uploadedFile = ref(null)
|
||||
const uploadedFile = ref<File | null>(null)
|
||||
const filePreview = ref<any[]>([])
|
||||
const fileErrors = ref<string[]>([])
|
||||
|
||||
|
@ -285,7 +324,7 @@ const tableHeaders = [
|
|||
|
||||
const previewHeaders = [
|
||||
{ title: 'شماره سریال', key: 'serialNumber' },
|
||||
{ title: 'محصول', key: 'commodity' },
|
||||
{ title: 'کد کالا', key: 'commodity_code' },
|
||||
{ title: 'توضیحات', key: 'description' },
|
||||
{ title: 'شروع گارانتی', key: 'warrantyStartDate' },
|
||||
{ title: 'پایان گارانتی', key: 'warrantyEndDate' },
|
||||
|
@ -336,28 +375,61 @@ const removeManualRow = (index: number) => {
|
|||
manualSerials.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
const handleFileUpload = async (files: File | File[] | null) => {
|
||||
if (!files) {
|
||||
filePreview.value = []
|
||||
fileErrors.value = []
|
||||
return
|
||||
}
|
||||
|
||||
const file = Array.isArray(files) ? files[0] : files
|
||||
|
||||
if (!file) {
|
||||
filePreview.value = []
|
||||
fileErrors.value = []
|
||||
return
|
||||
}
|
||||
|
||||
if (!(file instanceof File)) {
|
||||
fileErrors.value = ['فایل انتخاب شده معتبر نیست']
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
|
||||
const response = await axios.post('/api/plugins/warranty/serials/preview-import', formData, {
|
||||
const endpoint = '/api/plugins/warranty/serials/preview-import'
|
||||
const fileName = file.name || ''
|
||||
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
filePreview.value = response.data.preview || []
|
||||
let previewData = response.data.preview || []
|
||||
if (previewData.length > 0) {
|
||||
previewData = previewData.map((item: any) => {
|
||||
if (item.commodity && !item.commodity_code) {
|
||||
item.commodity_code = item.commodity
|
||||
}
|
||||
|
||||
if (item.commodity_id && !item.commodity_code) {
|
||||
item.commodity_code = item.commodity_id
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
filePreview.value = previewData
|
||||
fileErrors.value = response.data.errors || []
|
||||
} catch (error) {
|
||||
fileErrors.value = ['خطا در خواندن فایل']
|
||||
console.error(error)
|
||||
} catch (error: any) {
|
||||
if (error.response?.data?.error) {
|
||||
fileErrors.value = [error.response.data.error]
|
||||
} else {
|
||||
fileErrors.value = ['خطا در خواندن فایل']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -393,7 +465,14 @@ const importData = async () => {
|
|||
return
|
||||
}
|
||||
|
||||
data = validSerials
|
||||
data = validSerials.map(s => ({
|
||||
serialNumber: s.serialNumber,
|
||||
commodity_code: s.commodity_id,
|
||||
description: s.description,
|
||||
warrantyStartDate: s.warrantyStartDate,
|
||||
warrantyEndDate: s.warrantyEndDate,
|
||||
status: s.status
|
||||
}))
|
||||
} else {
|
||||
if (!uploadedFile.value) {
|
||||
await Swal.fire({
|
||||
|
@ -407,13 +486,29 @@ const importData = async () => {
|
|||
const formData = new FormData()
|
||||
formData.append('file', uploadedFile.value)
|
||||
|
||||
const response = await axios.post('/api/plugins/warranty/serials/import-excel', formData, {
|
||||
const endpoint = '/api/plugins/warranty/serials/import-excel'
|
||||
const fileName = uploadedFile.value?.name || ''
|
||||
|
||||
const response = await axios.post(endpoint, formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
}
|
||||
})
|
||||
|
||||
data = response.data.serials || []
|
||||
let fileData = response.data.serials || []
|
||||
if (fileData.length > 0) {
|
||||
fileData = fileData.map((item: any) => {
|
||||
if (item.commodity && !item.commodity_code) {
|
||||
return {
|
||||
...item,
|
||||
commodity_code: item.commodity
|
||||
}
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
|
||||
data = fileData
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
|
@ -438,7 +533,6 @@ const importData = async () => {
|
|||
icon: 'error',
|
||||
confirmButtonText: 'قبول'
|
||||
})
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
@ -462,6 +556,129 @@ const handleCommoditySelect = (selectedCommodity: any, index: number) => {
|
|||
}
|
||||
}
|
||||
|
||||
const downloadSampleFile = () => {
|
||||
const sampleData = [
|
||||
{
|
||||
serialNumber: 'SN001',
|
||||
commodity_code: '1012',
|
||||
description: 'لپتاپ گیمینگ',
|
||||
warrantyStartDate: '1403/01/01',
|
||||
warrantyEndDate: '1406/01/01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
serialNumber: 'SN002',
|
||||
commodity_code: '1012',
|
||||
description: 'ماوس بیسیم',
|
||||
warrantyStartDate: '1403/02/01',
|
||||
warrantyEndDate: '1405/02/01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
serialNumber: 'SN003',
|
||||
commodity_code: '1012',
|
||||
description: 'کیبورد مکانیکال',
|
||||
warrantyStartDate: '1402/12/01',
|
||||
warrantyEndDate: '1404/12/01',
|
||||
status: 'expired'
|
||||
}
|
||||
]
|
||||
|
||||
const headers = ['شماره سریال', 'کد کالا', 'توضیحات', 'شروع گارانتی', 'پایان گارانتی', 'وضعیت']
|
||||
const csvContent = [
|
||||
headers.join(','),
|
||||
...sampleData.map(row => [
|
||||
row.serialNumber,
|
||||
row.commodity_code,
|
||||
row.description,
|
||||
row.warrantyStartDate,
|
||||
row.warrantyEndDate,
|
||||
row.status
|
||||
].join(','))
|
||||
].join('\n')
|
||||
|
||||
const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' })
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', 'نمونه_سریال_گارانتی.csv')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
const downloadExcelSample = () => {
|
||||
const sampleData = [
|
||||
{
|
||||
serialNumber: 'SN001',
|
||||
commodity_code: '1012',
|
||||
description: 'لپتاپ گیمینگ',
|
||||
warrantyStartDate: '1403/01/01',
|
||||
warrantyEndDate: '1406/01/01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
serialNumber: 'SN002',
|
||||
commodity_code: '1012',
|
||||
description: 'ماوس بیسیم',
|
||||
warrantyStartDate: '1403/02/01',
|
||||
warrantyEndDate: '1405/02/01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
serialNumber: 'SN003',
|
||||
commodity_code: '1012',
|
||||
description: 'کیبورد مکانیکال',
|
||||
warrantyStartDate: '1402/12/01',
|
||||
warrantyEndDate: '1404/12/01',
|
||||
status: 'expired'
|
||||
}
|
||||
]
|
||||
|
||||
const headers = ['شماره سریال', 'کد کالا', 'توضیحات', 'شروع گارانتی', 'پایان گارانتی', 'وضعیت']
|
||||
|
||||
let htmlContent = `
|
||||
<html xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:x="urn:schemas-microsoft-com:office:excel" xmlns="http://www.w3.org/TR/REC-html40">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
table { border-collapse: collapse; width: 100%; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px; text-align: right; }
|
||||
th { background-color: #f2f2f2; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<table>
|
||||
<tr>
|
||||
${headers.map(h => `<th>${h}</th>`).join('')}
|
||||
</tr>
|
||||
${sampleData.map(row => `
|
||||
<tr>
|
||||
<td>${row.serialNumber}</td>
|
||||
<td>${row.commodity_code}</td>
|
||||
<td>${row.description}</td>
|
||||
<td>${row.warrantyStartDate}</td>
|
||||
<td>${row.warrantyEndDate}</td>
|
||||
<td>${row.status}</td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
const blob = new Blob([htmlContent], { type: 'application/vnd.ms-excel' })
|
||||
const link = document.createElement('a')
|
||||
const url = URL.createObjectURL(blob)
|
||||
link.setAttribute('href', url)
|
||||
link.setAttribute('download', 'نمونه_سریال_گارانتی.xls')
|
||||
link.style.visibility = 'hidden'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
const resetData = () => {
|
||||
manualSerials.value = [{
|
||||
serialNumber: '',
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" max-width="600px" persistent>
|
||||
<v-dialog v-model="dialog" max-width="640px" persistent>
|
||||
<v-card>
|
||||
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||
|
@ -10,104 +10,109 @@
|
|||
<v-form ref="form" v-model="valid">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-text-field
|
||||
v-model="formData.serialNumber"
|
||||
label="شماره سریال *"
|
||||
:rules="[rules.serialNumber]"
|
||||
required
|
||||
:disabled="isEdit"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
maxlength="50"
|
||||
counter
|
||||
></v-text-field>
|
||||
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
||||
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
||||
<template #append>
|
||||
<v-btn icon small @click="openScanner" :disabled="isEdit" color="primary" variant="text">
|
||||
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-text-field>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-autocomplete
|
||||
v-model="formData.commodity_id"
|
||||
<Hcommoditysearch
|
||||
:model-value="formData.commodity_id ?? undefined"
|
||||
@update:modelValue="(val: number | Record<string, any>) => { formData.commodity_id = typeof val === 'number' ? val : (val as any)?.id ?? null }"
|
||||
:return-object="false"
|
||||
label="محصول *"
|
||||
:items="commodities"
|
||||
item-title="name"
|
||||
item-value="id"
|
||||
:rules="[rules.commodity]"
|
||||
required
|
||||
:filter="customFilter"
|
||||
clearable
|
||||
return-object
|
||||
@update:model-value="handleCommoditySelect"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-autocomplete>
|
||||
</v-col>
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.warrantyStartDate"
|
||||
label="تاریخ شروع گارانتی"
|
||||
:rules="[rules.date]"
|
||||
class="serial-commodity"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<h-date-picker
|
||||
v-model="formData.warrantyEndDate"
|
||||
label="تاریخ پایان گارانتی"
|
||||
:rules="[rules.date, (value: any) => rules.endDate(value, formData.warrantyStartDate)]"
|
||||
/>
|
||||
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]" dense
|
||||
outlined hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select
|
||||
v-model="formData.status"
|
||||
label="وضعیت"
|
||||
:items="statusOptions"
|
||||
item-title="title"
|
||||
item-value="value"
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-select>
|
||||
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
||||
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" dense outlined
|
||||
hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-select v-model="formData.status" label="وضعیت" :items="statusOptions" item-title="title"
|
||||
item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col v-if="isEdit" cols="12" md="6">
|
||||
<v-select v-model="formData.activation" label="وضعیت فعالسازی" :items="activationOptions"
|
||||
item-title="title" item-value="value" variant="outlined" density="comfortable" hide-details="auto" />
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.description"
|
||||
label="توضیحات"
|
||||
rows="3"
|
||||
auto-grow
|
||||
variant="outlined"
|
||||
density="comfortable"
|
||||
hide-details="auto"
|
||||
></v-textarea>
|
||||
<v-textarea v-model="formData.description" label="توضیحات" rows="3" auto-grow variant="outlined"
|
||||
density="comfortable" hide-details="auto" />
|
||||
</v-col>
|
||||
<!-- <v-col cols="12">
|
||||
<v-textarea
|
||||
v-model="formData.notes"
|
||||
label="یادداشتها"
|
||||
rows="2"
|
||||
auto-grow
|
||||
></v-textarea>
|
||||
</v-col> -->
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-spacer />
|
||||
<v-btn color="grey" @click="close">انصراف</v-btn>
|
||||
<v-btn
|
||||
color="primary"
|
||||
@click="save"
|
||||
:loading="loading"
|
||||
:disabled="!valid"
|
||||
>
|
||||
<v-btn color="primary" @click="save" :loading="loading" :disabled="!valid">
|
||||
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
|
||||
<v-dialog v-model="showQrScanner" :max-width="isMobile ? '95vw' : 560" persistent>
|
||||
<v-card class="qr-card">
|
||||
<v-card-title class="qr-title">
|
||||
<v-icon left color="primary">mdi-qrcode-scan</v-icon>
|
||||
اسکن کد QR/بارکد
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
<div class="qr-wrap">
|
||||
<div id="reader" ref="readerRef" class="qr-reader"></div>
|
||||
</div>
|
||||
<div class="qr-status">
|
||||
<v-alert v-if="scanError" type="error" variant="tonal" density="comfortable">
|
||||
{{ scanError }}
|
||||
</v-alert>
|
||||
<v-progress-circular v-if="loadingScan" indeterminate size="28" class="mt-3" color="primary" />
|
||||
</div>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions class="qr-actions">
|
||||
<v-btn :disabled="loadingScan" variant="outlined" color="primary" prepend-icon="mdi-camera-switch"
|
||||
@click="switchCamera">
|
||||
تغییر دوربین
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" @click="closeScanner">انصراف</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-snackbar v-model="showNotification" :color="notificationColor" :timeout="3000" location="top">
|
||||
{{ notificationText }}
|
||||
<template #actions>
|
||||
<v-btn color="white" text @click="showNotification = false">بستن</v-btn>
|
||||
</template>
|
||||
</v-snackbar>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, nextTick } from 'vue'
|
||||
import { ref, computed, watch, nextTick, onBeforeUnmount } from 'vue'
|
||||
import { Html5Qrcode, Html5QrcodeSupportedFormats, Html5QrcodeScannerState } from 'html5-qrcode'
|
||||
import Hcommoditysearch from '@/components/forms/Hcommoditysearch.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
|
@ -124,165 +129,331 @@ const emit = defineEmits<{
|
|||
const loading = ref(false)
|
||||
const valid = ref(false)
|
||||
const form = ref()
|
||||
const commodityModel = ref<any>(null)
|
||||
|
||||
const formData = ref({
|
||||
serialNumber: '',
|
||||
commodity_id: '',
|
||||
commodity_id: null as number | null,
|
||||
description: '',
|
||||
warrantyStartDate: '',
|
||||
warrantyEndDate: '',
|
||||
status: 'active',
|
||||
status: 'available',
|
||||
activation: 'deactive',
|
||||
notes: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
required: (value: any) => !!value || 'این فیلد الزامی است',
|
||||
serialNumber: (value: any) => {
|
||||
if (!value) return 'شماره سریال الزامی است'
|
||||
if (!/^[A-Za-z0-9]+$/.test(value)) {
|
||||
return 'شماره سریال فقط میتواند شامل حروف انگلیسی و اعداد باشد'
|
||||
}
|
||||
if (value.length < 3) {
|
||||
return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
||||
}
|
||||
if (value.length > 50) {
|
||||
return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
||||
}
|
||||
required: (v: any) => !!v || 'این فیلد الزامی است',
|
||||
serialNumber: (v: any) => {
|
||||
if (!v) return 'شماره سریال الزامی است'
|
||||
if (!/^[A-Za-z0-9\-_.]+$/.test(v)) return 'شماره سریال نامعتبر است'
|
||||
if (v.length < 3) return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
||||
if (v.length > 50) return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
||||
return true
|
||||
},
|
||||
commodity: (value: any) => {
|
||||
if (!value) return 'انتخاب محصول الزامی است'
|
||||
return true
|
||||
commodity: (v: any) => !!v || 'انتخاب محصول الزامی است',
|
||||
date: (v: any) => {
|
||||
if (!v) return true
|
||||
const d = new Date(v)
|
||||
return !isNaN(d.getTime()) || 'تاریخ نامعتبر است'
|
||||
},
|
||||
date: (value: any) => {
|
||||
if (!value) return true
|
||||
const date = new Date(value)
|
||||
if (isNaN(date.getTime())) {
|
||||
return 'تاریخ نامعتبر است'
|
||||
}
|
||||
return true
|
||||
},
|
||||
endDate: (value: any, startDate: any) => {
|
||||
if (!value || !startDate) return true
|
||||
const end = new Date(value)
|
||||
const start = new Date(startDate)
|
||||
if (end <= start) {
|
||||
return 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
||||
}
|
||||
return true
|
||||
endDate: (v: any, s: any) => {
|
||||
if (!v || !s) return true
|
||||
const end = new Date(v); const start = new Date(s)
|
||||
return end > start || 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
||||
}
|
||||
}
|
||||
|
||||
const statusOptions = [
|
||||
{ title: 'فعال', value: 'active' },
|
||||
{ title: 'غیرفعال', value: 'inactive' },
|
||||
{ title: 'منقضی شده', value: 'expired' },
|
||||
// { title: 'استفاده شده', value: 'used' }
|
||||
{ title: 'آزاد', value: 'available' },
|
||||
{ title: 'تخصیص یافته', value: 'allocated' },
|
||||
{ title: 'تأیید شده', value: 'verified' },
|
||||
{ title: 'متصل', value: 'bound' },
|
||||
{ title: 'مصرف شده', value: 'consumed' },
|
||||
{ title: 'باطل', value: 'void' }
|
||||
]
|
||||
|
||||
const activationOptions = [
|
||||
{ title: 'غیرفعال', value: 'deactive' },
|
||||
{ title: 'فعال', value: 'active' }
|
||||
]
|
||||
|
||||
const dialog = computed({
|
||||
get: () => props.modelValue,
|
||||
set: (value) => emit('update:modelValue', value)
|
||||
set: (v) => emit('update:modelValue', v)
|
||||
})
|
||||
|
||||
const isEdit = computed(() => !!props.serial)
|
||||
const isMobile = computed(() => window.innerWidth <= 768)
|
||||
|
||||
const showNotification = ref(false)
|
||||
const notificationText = ref('')
|
||||
const notificationColor = ref<'success' | 'error' | 'warning' | 'info'>('success')
|
||||
const showNotify = (t: string, c: 'success' | 'error' | 'warning' | 'info' = 'success') => {
|
||||
notificationText.value = t
|
||||
notificationColor.value = c
|
||||
showNotification.value = true
|
||||
}
|
||||
|
||||
const showQrScanner = ref(false)
|
||||
const readerRef = ref<HTMLElement | null>(null)
|
||||
let qr: Html5Qrcode | null = null
|
||||
const loadingScan = ref(false)
|
||||
const scanError = ref('')
|
||||
const cameras = ref<{ id: string; label: string }[]>([])
|
||||
const currentCamIndex = ref(0)
|
||||
const qrboxSize = ref(240)
|
||||
|
||||
const computeQrBox = () => {
|
||||
const el = readerRef.value
|
||||
if (!el) return 260
|
||||
const w = Math.max(320, Math.floor(el.clientWidth))
|
||||
const size = Math.max(220, Math.min(380, Math.floor(w * 0.66)))
|
||||
return size
|
||||
}
|
||||
|
||||
const openScanner = async () => {
|
||||
showQrScanner.value = true
|
||||
await nextTick()
|
||||
await startScanner()
|
||||
}
|
||||
|
||||
const startScanner = async () => {
|
||||
try {
|
||||
loadingScan.value = true
|
||||
scanError.value = ''
|
||||
qrboxSize.value = computeQrBox()
|
||||
|
||||
const devices = await Html5Qrcode.getCameras()
|
||||
if (!devices.length) { throw new Error('دوربین یافت نشد') }
|
||||
cameras.value = devices.map(d => ({ id: d.id, label: d.label }))
|
||||
if (currentCamIndex.value >= cameras.value.length) currentCamIndex.value = 0
|
||||
|
||||
if (qr && (qr.getState?.() === Html5QrcodeScannerState.SCANNING)) await stopScanner()
|
||||
if (!qr) qr = new Html5Qrcode('reader', {
|
||||
verbose: false,
|
||||
formatsToSupport: [
|
||||
Html5QrcodeSupportedFormats.QR_CODE,
|
||||
Html5QrcodeSupportedFormats.CODE_128,
|
||||
Html5QrcodeSupportedFormats.CODE_39,
|
||||
Html5QrcodeSupportedFormats.EAN_13,
|
||||
Html5QrcodeSupportedFormats.UPC_A,
|
||||
Html5QrcodeSupportedFormats.DATA_MATRIX
|
||||
],
|
||||
experimentalFeatures: { useBarCodeDetectorIfSupported: true }
|
||||
})
|
||||
|
||||
await qr.start(
|
||||
{ deviceId: { exact: cameras.value[currentCamIndex.value].id } },
|
||||
{ fps: 12, qrbox: { width: qrboxSize.value, height: qrboxSize.value }, aspectRatio: 1.333 },
|
||||
(decodedText: string) => {
|
||||
if (decodedText) {
|
||||
formData.value.serialNumber = decodedText.trim()
|
||||
showNotify('کد با موفقیت اسکن شد', 'success')
|
||||
closeScanner()
|
||||
}
|
||||
},
|
||||
(_err: string) => { }
|
||||
)
|
||||
} catch (e: any) {
|
||||
scanError.value = e?.message || 'خطا در راهاندازی دوربین'
|
||||
} finally {
|
||||
loadingScan.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const stopScanner = async () => {
|
||||
if (!qr) return
|
||||
try {
|
||||
const state = qr.getState?.()
|
||||
if (state === Html5QrcodeScannerState.SCANNING) await qr.stop()
|
||||
await qr.clear()
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const closeScanner = async () => {
|
||||
await stopScanner()
|
||||
showQrScanner.value = false
|
||||
}
|
||||
|
||||
const switchCamera = async () => {
|
||||
if (!cameras.value.length) return
|
||||
currentCamIndex.value = (currentCamIndex.value + 1) % cameras.value.length
|
||||
await stopScanner()
|
||||
await nextTick()
|
||||
await startScanner()
|
||||
}
|
||||
|
||||
const save = async () => {
|
||||
const isValid = await form.value.validate()
|
||||
if (!isValid) {
|
||||
console.log('فرم دارای خطا است')
|
||||
return
|
||||
}
|
||||
|
||||
if (!formData.value.serialNumber || !formData.value.commodity_id) {
|
||||
console.log('فیلدهای الزامی پر نشدهاند')
|
||||
return
|
||||
}
|
||||
|
||||
const res = await form.value?.validate()
|
||||
const ok = typeof res === 'object' ? res.valid : !!res
|
||||
if (!ok) return
|
||||
if (!formData.value.serialNumber || !formData.value.commodity_id) return
|
||||
try {
|
||||
loading.value = true
|
||||
const data = { ...formData.value }
|
||||
emit('save', data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
emit('save', { ...formData.value })
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
clearValidationErrors()
|
||||
closeScanner()
|
||||
emit('close')
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
serialNumber: '',
|
||||
commodity_id: '',
|
||||
commodity_id: null,
|
||||
description: '',
|
||||
warrantyStartDate: '',
|
||||
warrantyEndDate: '',
|
||||
status: 'active',
|
||||
status: 'available',
|
||||
activation: 'deactive',
|
||||
notes: ''
|
||||
}
|
||||
if (form.value) {
|
||||
form.value.resetValidation()
|
||||
}
|
||||
}
|
||||
|
||||
const clearValidationErrors = () => {
|
||||
if (form.value) {
|
||||
form.value.resetValidation()
|
||||
}
|
||||
commodityModel.value = null
|
||||
form.value?.resetValidation()
|
||||
}
|
||||
|
||||
const loadSerialData = () => {
|
||||
if (props.serial) {
|
||||
formData.value = {
|
||||
serialNumber: props.serial.serialNumber || '',
|
||||
commodity_id: props.serial.commodity?.id || '',
|
||||
commodity_id: Number(props.serial.commodity?.id) || null,
|
||||
description: props.serial.description || '',
|
||||
warrantyStartDate: props.serial.warrantyStartDate || '',
|
||||
warrantyEndDate: props.serial.warrantyEndDate || '',
|
||||
status: props.serial.status || 'active',
|
||||
status: props.serial.status || 'available',
|
||||
activation: props.serial.activation || 'deactive',
|
||||
notes: props.serial.notes || ''
|
||||
}
|
||||
commodityModel.value = props.commodities.find(c => c.id === formData.value.commodity_id) || null
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
}
|
||||
|
||||
const customFilter = (item: any, queryText: string) => {
|
||||
const text = item.name.toLowerCase()
|
||||
const searchText = queryText.toLowerCase()
|
||||
return text.indexOf(searchText) > -1
|
||||
const customFilter = (item: any, q: string) => {
|
||||
const t = String(item?.name || '').toLowerCase()
|
||||
const s = String(q || '').toLowerCase()
|
||||
return t.indexOf(s) > -1
|
||||
}
|
||||
|
||||
const handleCommoditySelect = (selectedCommodity: any) => {
|
||||
if (selectedCommodity && selectedCommodity.id) {
|
||||
formData.value.commodity_id = selectedCommodity.id
|
||||
} else {
|
||||
formData.value.commodity_id = ''
|
||||
}
|
||||
const handleCommoditySelect = (c: any) => {
|
||||
formData.value.commodity_id = c?.id ? Number(c.id) : null
|
||||
}
|
||||
|
||||
watch(() => props.serial, () => {
|
||||
nextTick(() => {
|
||||
loadSerialData()
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
if (newVal) {
|
||||
nextTick(() => {
|
||||
loadSerialData()
|
||||
})
|
||||
}
|
||||
})
|
||||
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
||||
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
||||
onBeforeUnmount(() => { closeScanner() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-dialog {
|
||||
direction: rtl;
|
||||
<style>
|
||||
/* normalize Hcommoditysearch height with other inputs */
|
||||
.serial-commodity :deep(.v-field) { min-height: 56px; }
|
||||
.serial-commodity :deep(.v-field__input) { padding-top: 14px; padding-bottom: 14px; }
|
||||
#qr-shaded-region {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
video {
|
||||
border-radius: 12px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.v-dialog {
|
||||
direction: rtl
|
||||
}
|
||||
|
||||
.qr-card {
|
||||
max-width: 95vw;
|
||||
border-radius: 16px
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
text-align: center;
|
||||
padding: 14px 16px
|
||||
}
|
||||
|
||||
.qr-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
margin: 0 auto
|
||||
}
|
||||
|
||||
.qr-reader {
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
overflow: hidden;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
/* ویدئو تولیدی کتابخانه */
|
||||
:deep(#reader video) {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
display: block !important;
|
||||
object-fit: cover;
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
/* کانتینر داخلی کتابخانه راست به چپ نشه */
|
||||
:deep(#reader div) {
|
||||
direction: ltr
|
||||
}
|
||||
|
||||
/* وضعیتها */
|
||||
.qr-status {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-top: 12px
|
||||
}
|
||||
|
||||
.qr-actions {
|
||||
padding: 12px 16px
|
||||
}
|
||||
|
||||
/* ریسپانسیو */
|
||||
@media (max-width:1024px) {
|
||||
.qr-reader {
|
||||
min-height: 300px
|
||||
}
|
||||
|
||||
:deep(#reader video) {
|
||||
min-height: 220px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:768px) {
|
||||
.qr-card {
|
||||
max-width: 95vw
|
||||
}
|
||||
|
||||
.qr-reader {
|
||||
min-height: 260px
|
||||
}
|
||||
|
||||
:deep(#reader video) {
|
||||
min-height: 200px
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:480px) {
|
||||
.qr-reader {
|
||||
min-height: 220px
|
||||
}
|
||||
|
||||
:deep(#reader video) {
|
||||
min-height: 180px
|
||||
}
|
||||
}
|
||||
|
||||
.v-input.v-input--horizontal.v-input--center-affix.v-input--density-compact.v-theme--light.v-locale--is-rtl.v-input--error.v-text-field.my-0 {
|
||||
height: 3rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,43 +1,55 @@
|
|||
<template>
|
||||
<v-dialog v-model="dialog" max-width="700px">
|
||||
<v-card>
|
||||
<v-card class="serial-view">
|
||||
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||
<span>جزئیات سریال گارانتی</span>
|
||||
<v-spacer></v-spacer>
|
||||
<v-chip
|
||||
:color="getStatusColor(serial?.status)"
|
||||
size="small"
|
||||
>
|
||||
<v-chip :color="getStatusColor(serial?.status)" size="small">
|
||||
{{ getStatusText(serial?.status) }}
|
||||
</v-chip>
|
||||
<v-chip v-if="serial?.expired" color="error" size="small" class="opacity-100">
|
||||
منقضی شده
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text v-if="serial">
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-subtitle-2 text-grey mb-2">اطلاعات سریال</div>
|
||||
<div class="section-title mb-2">اطلاعات سریال</div>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-barcode</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>شماره سریال</v-list-item-title>
|
||||
<v-list-item-title>سریال گارانتی</v-list-item-title>
|
||||
<v-list-item-subtitle class="font-weight-bold">
|
||||
{{ serial.serialNumber }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="serial.commoditySerial">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-barcode</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>سریال کالا</v-list-item-title>
|
||||
<v-list-item-subtitle class="font-weight-bold">
|
||||
{{ serial.commoditySerial }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-package-variant</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>محصول</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<div class="font-weight-bold">{{ serial.commodity?.name }}</div>
|
||||
<div class="text-caption">کد: {{ serial.commodity?.code }}</div>
|
||||
</v-list-item-subtitle>
|
||||
<v-list-item-title>کالا</v-list-item-title>
|
||||
<router-link :to="`/acc/commodity/mod/${serial.commodity?.code}`">
|
||||
<v-list-item-subtitle>
|
||||
<div class="font-weight-bold">{{ serial.commodity?.name }}</div>
|
||||
<div class="text-caption">کد: {{ serial.commodity?.code }}</div>
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
|
@ -47,13 +59,24 @@
|
|||
<v-list-item-title>تاریخ ثبت</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(serial.dateSubmit) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="serial.allocatedToDocumentCode">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-file-document</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>سند مرتبط</v-list-item-title>
|
||||
<router-link :to="`/acc/storeroom/ticket/view/${serial.allocatedToDocumentCode}`">
|
||||
<v-list-item-subtitle class="font-weight-bold text-primary opacity-100">
|
||||
{{ serial.allocatedToDocumentCode }}
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-subtitle-2 text-grey mb-2">اطلاعات گارانتی</div>
|
||||
<div class="section-title mb-2">اطلاعات گارانتی</div>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template v-slot:prepend>
|
||||
|
@ -71,7 +94,9 @@
|
|||
</template>
|
||||
<v-list-item-title>پایان گارانتی</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ serial.warrantyEndDate ? formatDate(serial.warrantyEndDate) : 'تعیین نشده' }}
|
||||
<div class="d-inline-flex align-center">
|
||||
<span>{{ serial.warrantyEndDate ? formatDate(serial.warrantyEndDate) : 'تعیین نشده' }}</span>
|
||||
</div>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
|
@ -82,20 +107,54 @@
|
|||
<v-list-item-title>ثبت کننده</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ serial.submitter?.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="serial.buyer">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="info">mdi-account-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>خریدار</v-list-item-title>
|
||||
<router-link :to="`/acc/persons/card/view/${serial.buyer?.code}`">
|
||||
<v-list-item-subtitle class="opacity-100">
|
||||
<div class="font-weight-bold text-primary">{{ serial.buyer?.nikename || serial.buyer?.name }}
|
||||
</div>
|
||||
<div class="text-caption">{{ serial.buyer?.mobile }}</div>
|
||||
</v-list-item-subtitle>
|
||||
</router-link>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="serial.activation">
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="serial.activation === 'active' ? 'success' : 'warning'">mdi-shield-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>وضعیت فعالسازی</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip :color="serial.activation === 'active' ? 'success' : 'warning'" size="small">
|
||||
{{ serial.activation === 'active' ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="serial.activation && serial.activationAt">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-calendar-check</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تاریخ فعالسازی</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ formatDate(serial.activationAt) }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-if="serial.description">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-subtitle-2 text-grey mb-2">توضیحات</div>
|
||||
<div class="section-title mb-2">توضیحات</div>
|
||||
<p class="text-body-1">{{ serial.description }}</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" v-if="serial.notes">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<div class="text-subtitle-2 text-grey mb-2">یادداشتها</div>
|
||||
<div class="section-title mb-2">یادداشتها</div>
|
||||
<p class="text-body-1">{{ serial.notes }}</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -135,40 +194,44 @@ const close = () => {
|
|||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'success'
|
||||
case 'inactive': return 'grey'
|
||||
case 'expired': return 'warning'
|
||||
case 'used': return 'info'
|
||||
case 'available': return 'success'
|
||||
case 'allocated': return 'info'
|
||||
case 'verified': return 'primary'
|
||||
case 'bound': return 'warning'
|
||||
case 'consumed': return 'teal'
|
||||
case 'void': return 'grey'
|
||||
default: return 'grey'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active': return 'فعال'
|
||||
case 'inactive': return 'غیرفعال'
|
||||
case 'expired': return 'منقضی شده'
|
||||
// case 'used': return 'استفاده شده'
|
||||
default: return status
|
||||
case 'available': return 'آزاد'
|
||||
case 'allocated': return 'تخصیص یافته'
|
||||
case 'verified': return 'تأیید شده'
|
||||
case 'bound': return 'متصل'
|
||||
case 'consumed': return 'مصرف شده'
|
||||
case 'void': return 'باطل'
|
||||
default: return 'نامشخص'
|
||||
}
|
||||
}
|
||||
|
||||
const formatDate = (date: string) => {
|
||||
if (!date) return '-'
|
||||
|
||||
|
||||
if (date.includes('/') && date.split('/')[0].length === 4) {
|
||||
return date
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const dateObj = new Date(date)
|
||||
if (isNaN(dateObj.getTime())) return '-'
|
||||
|
||||
|
||||
const jMoment = moment(dateObj)
|
||||
const persianYear = jMoment.jYear()
|
||||
const persianMonth = jMoment.jMonth() + 1
|
||||
const persianDay = jMoment.jDate()
|
||||
|
||||
|
||||
return `${persianYear}/${persianMonth.toString().padStart(2, '0')}/${persianDay.toString().padStart(2, '0')}`
|
||||
} catch (error) {
|
||||
return date
|
||||
|
@ -176,8 +239,45 @@ const formatDate = (date: string) => {
|
|||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
<style>
|
||||
.v-dialog {
|
||||
direction: rtl;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* .serial-view .v-card-title {
|
||||
color: #fff;
|
||||
} */
|
||||
|
||||
.serial-view .v-icon {
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.serial-view .v-list-item-title {
|
||||
font-weight: 600;
|
||||
color: #263238;
|
||||
}
|
||||
|
||||
.serial-view .v-list-item-subtitle {
|
||||
color: #111;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.serial-view .v-list-item__prepend .v-icon {
|
||||
color: #0a4798 !important;
|
||||
}
|
||||
|
||||
.serial-view .section-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: #37474f;
|
||||
}
|
||||
|
||||
.serial-view .v-card[variant="outlined"] {
|
||||
border-color: #e0e0e0 !important;
|
||||
}
|
||||
|
||||
.v-list-item--one-line .v-list-item-subtitle {
|
||||
-webkit-line-clamp: unset !important;
|
||||
line-clamp: unset !important;
|
||||
}
|
||||
</style>
|
|
@ -1141,6 +1141,23 @@ const router = createRouter({
|
|||
'title': 'نصب وب اپلیکیشن ',
|
||||
}
|
||||
},
|
||||
// Public routes (no authentication required)
|
||||
{
|
||||
path: '/public/:businessId/',
|
||||
component: () => import('../views/public/PublicLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: 'warranty-activation',
|
||||
name: 'public_warranty_activation',
|
||||
component: () => import('../views/public/WarrantyActivation.vue'),
|
||||
meta: {
|
||||
'title': 'فعالسازی گارانتی',
|
||||
'public': true,
|
||||
'requiresPlugin': 'warranty'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/oauth/authorize',
|
||||
name: 'oauth_authorize',
|
||||
|
@ -1167,8 +1184,8 @@ router.beforeEach(async (to, from, next) => {
|
|||
} else {
|
||||
document.title = <string>to.meta.title;
|
||||
}
|
||||
//check user is login
|
||||
if (to.meta.login) {
|
||||
//check user is login (skip for public routes)
|
||||
if (to.meta.login && !to.meta.public) {
|
||||
let result = await axios.post('/api/user/check/login');
|
||||
if (result.status == 200 && result.data.Success == true) {
|
||||
//check user has role
|
||||
|
@ -1192,6 +1209,19 @@ router.beforeEach(async (to, from, next) => {
|
|||
}
|
||||
}
|
||||
|
||||
if (to.meta.public && to.meta.requiresPlugin && to.params.businessId) {
|
||||
try {
|
||||
const response = await axios.get(`/api/public/${to.params.businessId}/status`);
|
||||
if (!response.data.success) {
|
||||
next({ 'name': 'not-found', 'params': { catchAll: '404' } });
|
||||
return;
|
||||
}
|
||||
} catch (error: any) {
|
||||
next({ 'name': 'not-found', 'params': { catchAll: '404' } });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
return
|
||||
})
|
||||
|
|
146
webUI/src/utils/approvalUtils.js
Normal file
146
webUI/src/utils/approvalUtils.js
Normal file
|
@ -0,0 +1,146 @@
|
|||
/**
|
||||
* Utility functions for document approval management
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check if current user can approve a specific document type
|
||||
* @param {Object} businessSettings - Business settings object
|
||||
* @param {number} currentUserId - Current user ID
|
||||
* @param {boolean} isBusinessOwner - Whether current user is business owner
|
||||
* @param {string} documentType - Type of document (invoice, warehouse, financial)
|
||||
* @returns {boolean} - Whether user can approve
|
||||
*/
|
||||
export function canApproveDocument(businessSettings, currentUserEmail, isBusinessOwner, documentType) {
|
||||
// If two-step approval is not enabled, anyone can approve
|
||||
if (!businessSettings.requireTwoStepApproval) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Business owner can always approve
|
||||
if (isBusinessOwner) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check specific approver based on document type
|
||||
switch (documentType) {
|
||||
case 'invoice':
|
||||
return businessSettings.invoiceApprover === currentUserEmail;
|
||||
case 'warehouse':
|
||||
return businessSettings.warehouseApprover === currentUserEmail;
|
||||
case 'financial':
|
||||
return businessSettings.financialApprover === currentUserEmail;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get document type from document object
|
||||
* @param {Object} document - Document object
|
||||
* @returns {string} - Document type
|
||||
*/
|
||||
export function getDocumentType(document) {
|
||||
if (document.type) {
|
||||
return document.type;
|
||||
}
|
||||
|
||||
// Fallback based on document properties
|
||||
if (document.invoiceNumber) {
|
||||
return 'invoice';
|
||||
}
|
||||
if (document.warehouseNumber) {
|
||||
return 'warehouse';
|
||||
}
|
||||
if (document.paymentType || document.receiptType) {
|
||||
return 'financial';
|
||||
}
|
||||
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if document needs approval
|
||||
* @param {Object} businessSettings - Business settings object
|
||||
* @param {Object} document - Document object
|
||||
* @returns {boolean} - Whether document needs approval
|
||||
*/
|
||||
export function needsApproval(businessSettings, document) {
|
||||
if (!businessSettings.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if document is pending approval
|
||||
return document.status === 'pending_approval' ||
|
||||
document.approvalStatus === 'pending' ||
|
||||
document.approved === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval button visibility
|
||||
* @param {Object} businessSettings - Business settings object
|
||||
* @param {Object} document - Document object
|
||||
* @param {number} currentUserId - Current user ID
|
||||
* @param {boolean} isBusinessOwner - Whether current user is business owner
|
||||
* @returns {boolean} - Whether approval button should be visible
|
||||
*/
|
||||
export function shouldShowApprovalButton(businessSettings, document, currentUserEmail, isBusinessOwner) {
|
||||
// Check if document needs approval
|
||||
if (!needsApproval(businessSettings, document)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if user can approve
|
||||
const documentType = getDocumentType(document);
|
||||
return canApproveDocument(businessSettings, currentUserEmail, isBusinessOwner, documentType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval button text
|
||||
* @param {Object} document - Document object
|
||||
* @returns {string} - Button text
|
||||
*/
|
||||
export function getApprovalButtonText(document) {
|
||||
if (document.status === 'pending_approval' || document.approvalStatus === 'pending') {
|
||||
return 'تایید سند';
|
||||
}
|
||||
if (document.approved === false) {
|
||||
return 'تایید مجدد';
|
||||
}
|
||||
return 'تایید';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval status text
|
||||
* @param {Object} document - Document object
|
||||
* @returns {string} - Status text
|
||||
*/
|
||||
export function getApprovalStatusText(document) {
|
||||
if (document.status === 'pending_approval' || document.approvalStatus === 'pending') {
|
||||
return 'در انتظار تایید';
|
||||
}
|
||||
if (document.approved === true) {
|
||||
return 'تایید شده';
|
||||
}
|
||||
if (document.approved === false) {
|
||||
return 'رد شده';
|
||||
}
|
||||
return 'نامشخص';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get approval status color
|
||||
* @param {Object} document - Document object
|
||||
* @returns {string} - Status color
|
||||
*/
|
||||
export function getApprovalStatusColor(document) {
|
||||
if (document.status === 'pending_approval' || document.approvalStatus === 'pending') {
|
||||
return 'warning';
|
||||
}
|
||||
if (document.approved === true) {
|
||||
return 'success';
|
||||
}
|
||||
if (document.approved === false) {
|
||||
return 'error';
|
||||
}
|
||||
return 'grey';
|
||||
}
|
|
@ -152,20 +152,7 @@
|
|||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-2" />
|
||||
<v-card-subtitle class="px-0 text-primary">
|
||||
تأیید دو مرحلهای
|
||||
</v-card-subtitle>
|
||||
<v-switch
|
||||
v-model="editPersonData.requireTwoStep"
|
||||
label="فاکتورها و حوالههای این شخص نیاز به تأیید دو مرحلهای دارند"
|
||||
color="warning"
|
||||
hide-details
|
||||
class="mt-2"
|
||||
/>
|
||||
<v-alert type="info" variant="tonal" dense class="mt-2">
|
||||
اگر این گزینه فعال باشد، تمام فاکتورها، حوالههای انبار و اسناد مالی مرتبط با این شخص نیاز به تأیید دو مرحلهای خواهند داشت.
|
||||
</v-alert>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
|
@ -242,21 +229,6 @@
|
|||
selectedPerson.address || '-' }}</span></div>
|
||||
<div class="text-subtitle-2">{{ $t('pages.person.description') }}: <span class="text-primary">{{
|
||||
selectedPerson.des || '-' }}</span></div>
|
||||
<!-- <v-divider class="my-3" />
|
||||
<div class="text-subtitle-2 d-flex align-center justify-space-between">
|
||||
<span>تأیید دو مرحلهای:</span>
|
||||
<v-switch
|
||||
v-model="selectedPerson.requireTwoStep"
|
||||
color="warning"
|
||||
hide-details
|
||||
density="compact"
|
||||
@change="updateTwoStepApproval"
|
||||
:loading="twoStepUpdateLoading"
|
||||
/>
|
||||
</div>
|
||||
<div class="text-caption text-grey mt-1">
|
||||
{{ selectedPerson.requireTwoStep ? 'فعال - فاکتورها و حوالهها نیاز به تأیید دارند' : 'غیرفعال - طبق قوانین عادی' }}
|
||||
</div> -->
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -286,12 +258,6 @@
|
|||
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_balance') }}: <span class="text-primary">{{
|
||||
$filters.formatNumber(selectedPerson.balance) || '-' }}</span></div>
|
||||
<v-divider class="my-2" />
|
||||
<div class="text-subtitle-2">
|
||||
تأیید دو مرحلهای:
|
||||
<v-chip :color="selectedPerson.requireTwoStep ? 'warning' : 'success'" size="small" class="ml-2">
|
||||
{{ selectedPerson.requireTwoStep ? 'فعال' : 'غیرفعال' }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
@ -372,7 +338,7 @@ export default {
|
|||
searchValue: '',
|
||||
listPersons: [],
|
||||
itemsSelected: [],
|
||||
selectedPerson: { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false },
|
||||
selectedPerson: { accounts: [], balance: 0, bs: 0, bd: 0 },
|
||||
items: [],
|
||||
loading: ref(false),
|
||||
dialog: false,
|
||||
|
@ -386,7 +352,7 @@ export default {
|
|||
tel: '',
|
||||
address: '',
|
||||
des: '',
|
||||
requireTwoStep: false
|
||||
|
||||
},
|
||||
debounceTimeout: null, // برای مدیریت debounce
|
||||
headers: [
|
||||
|
@ -471,14 +437,14 @@ export default {
|
|||
tel: this.selectedPerson.tel || '',
|
||||
address: this.selectedPerson.address || '',
|
||||
des: this.selectedPerson.des || '',
|
||||
requireTwoStep: this.selectedPerson.requireTwoStep || false
|
||||
|
||||
};
|
||||
|
||||
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'person', id });
|
||||
this.items = rowsResponse.data;
|
||||
} catch (error) {
|
||||
console.error('Load person error:', error);
|
||||
this.selectedPerson = { accounts: [], balance: 0, bs: 0, bd: 0, requireTwoStep: false };
|
||||
this.selectedPerson = { accounts: [], balance: 0, bs: 0, bd: 0 };
|
||||
this.items = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
|
|
|
@ -65,23 +65,7 @@
|
|||
<v-text-field v-model="person.des" :label="$t('pages.person.description')" dense
|
||||
prepend-inner-icon="mdi-text" hide-details />
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-2" />
|
||||
<v-card-subtitle class="px-0 text-primary mt-2">
|
||||
تأیید دو مرحلهای
|
||||
</v-card-subtitle>
|
||||
<v-switch
|
||||
v-model="person.requireTwoStep"
|
||||
label="فاکتورها و حوالههای این شخص نیاز به تأیید دو مرحلهای دارند"
|
||||
color="warning"
|
||||
inset
|
||||
hide-details
|
||||
class="mt-2"
|
||||
/>
|
||||
<v-alert type="info" variant="tonal" dense class="mt-2">
|
||||
اگر این گزینه فعال باشد، تمام فاکتورها، حوالههای انبار و اسناد مالی مرتبط با این شخص نیاز به تأیید دو مرحلهای خواهند داشت.
|
||||
</v-alert>
|
||||
</v-col>
|
||||
|
||||
</v-row>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
@ -305,7 +289,7 @@ export default {
|
|||
accounts: [],
|
||||
prelabel: ref(null),
|
||||
speedAccess: false,
|
||||
requireTwoStep: false
|
||||
|
||||
},
|
||||
snackbar: {
|
||||
show: false,
|
||||
|
@ -365,7 +349,7 @@ export default {
|
|||
if (id) {
|
||||
axios.post('/api/person/info/' + id).then((response) => {
|
||||
console.log('Loaded person data:', response.data);
|
||||
console.log('requireTwoStep value:', response.data.requireTwoStep);
|
||||
|
||||
this.person = response.data;
|
||||
this.loading = false;
|
||||
}).catch((error) => {
|
||||
|
@ -425,7 +409,7 @@ export default {
|
|||
this.loading = true;
|
||||
try {
|
||||
console.log('Saving person data:', this.person);
|
||||
console.log('requireTwoStep value:', this.person.requireTwoStep);
|
||||
|
||||
const response = await axios.post('/api/person/mod/' + this.person.code, this.person);
|
||||
this.loading = false;
|
||||
if (response.data && response.data.result === 2) {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div class="import-workflow-list">
|
||||
<v-container fluid>
|
||||
<!-- Stats Cards -->
|
||||
<v-row>
|
||||
<!-- <v-row>
|
||||
<v-col cols="6" sm="6" md="3">
|
||||
<div class="stats-card total-card">
|
||||
<div class="stats-icon">
|
||||
|
@ -87,7 +87,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-row> -->
|
||||
|
||||
<!-- Data Table -->
|
||||
<v-row>
|
||||
|
@ -189,7 +189,7 @@
|
|||
</template>
|
||||
|
||||
<template v-slot:item.totalAmount="{ item }">
|
||||
<div class="text-left">
|
||||
<div>
|
||||
{{ formatNumber(item.totalAmount) }}
|
||||
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||
</div>
|
||||
|
@ -460,7 +460,7 @@ const confirmDelete = async () => {
|
|||
if (response.data.Success) {
|
||||
showNotification('پرونده واردات با موفقیت حذف شد')
|
||||
loadWorkflows()
|
||||
loadStats()
|
||||
// loadStats()
|
||||
} else {
|
||||
throw new Error(response.data.ErrorMessage)
|
||||
}
|
||||
|
@ -476,7 +476,7 @@ const confirmDelete = async () => {
|
|||
const onWorkflowCreated = () => {
|
||||
showCreateDialog.value = false
|
||||
loadWorkflows()
|
||||
loadStats()
|
||||
// loadStats()
|
||||
}
|
||||
|
||||
// Utilities
|
||||
|
@ -536,7 +536,7 @@ const formatDate = (date) => {
|
|||
// Lifecycle
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
loadStats(),
|
||||
// loadStats(),
|
||||
loadWorkflows()
|
||||
])
|
||||
})
|
||||
|
|
|
@ -31,14 +31,14 @@
|
|||
>
|
||||
{{ editMode ? 'لغو ویرایش' : 'ویرایش' }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
<!-- <v-btn
|
||||
class="mr-2"
|
||||
color="success"
|
||||
prepend-icon="mdi-warehouse"
|
||||
@click="openCreateTicketDialog"
|
||||
>
|
||||
ایجاد حواله ورود از پرونده
|
||||
</v-btn>
|
||||
</v-btn> -->
|
||||
</div>
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
|
@ -276,13 +276,13 @@
|
|||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-tabs v-model="activeTab" bg-color="primary">
|
||||
<v-tab value="items">آیتمها ({{ workflow.items?.length || 0 }})</v-tab>
|
||||
<v-tab value="payments">پرداختها ({{ workflow.payments?.length || 0 }})</v-tab>
|
||||
<v-tab value="documents">اسناد ({{ workflow.documents?.length || 0 }})</v-tab>
|
||||
<v-tab value="stages">مراحل ({{ workflow.stages?.length || 0 }})</v-tab>
|
||||
<v-tab value="shipping">حمل و نقل ({{ workflow.shipping?.length || 0 }})</v-tab>
|
||||
<v-tab value="customs">ترخیص ({{ workflow.customs?.length || 0 }})</v-tab>
|
||||
<v-tab value="tickets">حوالههای مرتبط</v-tab>
|
||||
<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="payments">پرداختها <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.payments?.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="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-tabs>
|
||||
|
||||
<v-tabs-window v-model="activeTab">
|
||||
|
@ -290,6 +290,7 @@
|
|||
<ImportWorkflowItems
|
||||
:workflow-id="workflowId"
|
||||
:items="workflow.items"
|
||||
:currency="workflow.currency"
|
||||
@updated="loadWorkflow"
|
||||
/>
|
||||
</v-tabs-window-item>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -65,6 +65,13 @@
|
|||
</template>
|
||||
</v-tooltip>
|
||||
</v-toolbar>
|
||||
<!-- Tabs for two-step approval -->
|
||||
<div v-if="business.requireTwoStepApproval" class="px-2 pt-2">
|
||||
<v-tabs v-model="currentTab" color="primary" density="comfortable">
|
||||
<v-tab value="approved">فاکتورهای تایید شده</v-tab>
|
||||
<v-tab value="pending">فاکتورهای در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
</div>
|
||||
<v-text-field hide-details color="green" class="pt-0 rounded-0 mb-0" density="compact" :placeholder="$t('dialog.search_txt')" v-model="searchValue" type="text" clearable>
|
||||
<template v-slot:prepend-inner>
|
||||
<v-tooltip location="bottom" :text="$t('dialog.search')">
|
||||
|
@ -105,8 +112,8 @@
|
|||
v-model:items-per-page="serverOptions.rowsPerPage"
|
||||
v-model:page="serverOptions.page"
|
||||
:headers="visibleHeaders"
|
||||
:items="items"
|
||||
:items-length="total"
|
||||
:items="displayItems"
|
||||
:items-length="displayTotal"
|
||||
:loading="loading"
|
||||
:no-data-text="$t('table.no_data')"
|
||||
v-model="itemsSelected"
|
||||
|
@ -154,6 +161,11 @@
|
|||
<v-icon color="orange" icon="mdi-cloud-upload"></v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item v-if="canShowApprovalButton(item)" class="text-dark" title="تایید فاکتور" @click="approveInvoice(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item class="text-dark" :title="$t('dialog.edit')" @click="canEditItem(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon icon="mdi-file-edit"></v-icon>
|
||||
|
@ -212,6 +224,14 @@
|
|||
</router-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
<template v-slot:item.approvalStatus="{ item }">
|
||||
<v-chip size="small" :color="getApprovalStatusColor(item)">
|
||||
{{ getApprovalStatusText(item) }}
|
||||
</v-chip>
|
||||
</template>
|
||||
<template v-slot:item.approvedBy="{ item }">
|
||||
{{ item.approvedBy?.fullName || '-' }}
|
||||
</template>
|
||||
<template v-slot:item.code="{ item }">
|
||||
<router-link :to="'/acc/sell/view/' + item.code">
|
||||
{{ item.code }}
|
||||
|
@ -356,6 +376,7 @@ export default defineComponent({
|
|||
data() {
|
||||
let self = this;
|
||||
return {
|
||||
currentTab: 'approved',
|
||||
paperSizes: [
|
||||
{ title: self.$t('dialog.a4p'), value: 'A4' },
|
||||
{ title: self.$t('dialog.a4l'), value: 'A4-L' },
|
||||
|
@ -375,6 +396,8 @@ export default defineComponent({
|
|||
invoiceIndex: true
|
||||
},
|
||||
plugins: {},
|
||||
business: { requireTwoStepApproval: false, invoiceApprover: null },
|
||||
currentUser: { email: '', owner: false },
|
||||
sumSelected: 0,
|
||||
sumSelectedProfit: 0,
|
||||
sumTotal: 0,
|
||||
|
@ -384,7 +407,11 @@ export default defineComponent({
|
|||
loading: false,
|
||||
bulkLoading: false,
|
||||
items: [],
|
||||
itemsApproved: [],
|
||||
itemsPending: [],
|
||||
total: 0,
|
||||
totalApproved: 0,
|
||||
totalPending: 0,
|
||||
expanded: [],
|
||||
serverOptions: reactive({
|
||||
page: 1,
|
||||
|
@ -397,6 +424,8 @@ export default defineComponent({
|
|||
{ title: "فاکتور", value: "code", sortable: true, visible: true, width: 120 },
|
||||
{ title: "تاریخ", value: "date", sortable: true, visible: true, width: 120 },
|
||||
{ title: "خریدار", value: "person", sortable: true, visible: true, width: 150 },
|
||||
{ title: "وضعیت تایید", value: "approvalStatus", sortable: true, visible: true, width: 150 },
|
||||
{ title: "تاییدکننده", value: "approvedBy", sortable: true, visible: true, width: 120 },
|
||||
{ title: "تخفیف", value: "discountAll", sortable: true, visible: true, width: 120 },
|
||||
{ title: "حمل و نقل", value: "transferCost", sortable: true, visible: true, width: 120 },
|
||||
{ title: "مبلغ", value: "amount", sortable: true, visible: true, width: 150 },
|
||||
|
@ -418,11 +447,25 @@ export default defineComponent({
|
|||
},
|
||||
computed: {
|
||||
visibleHeaders() {
|
||||
return this.allHeaders.filter(header => header.visible);
|
||||
return this.allHeaders.filter(header => {
|
||||
// اگر ستونهای تأیید هستند، باید دو مرحلهای فعال باشد
|
||||
if ((header.value === 'approvalStatus' || header.value === 'approvedBy') && !this.business.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
return header.visible;
|
||||
});
|
||||
},
|
||||
tableHeight() {
|
||||
return window.innerHeight - 200;
|
||||
},
|
||||
displayItems() {
|
||||
if (!this.business.requireTwoStepApproval) return this.items;
|
||||
return this.currentTab === 'pending' ? this.itemsPending : this.itemsApproved;
|
||||
},
|
||||
displayTotal() {
|
||||
if (!this.business.requireTwoStepApproval) return this.total;
|
||||
return this.currentTab === 'pending' ? this.totalPending : this.totalApproved;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
approveSelectedInvoices() {
|
||||
|
@ -460,6 +503,26 @@ export default defineComponent({
|
|||
this.plugins = {};
|
||||
}
|
||||
},
|
||||
// بارگذاری اطلاعات بیزنس
|
||||
async loadBusinessInfo() {
|
||||
try {
|
||||
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
|
||||
this.business = response.data || { requireTwoStepApproval: false, invoiceApprover: null };
|
||||
} catch (error) {
|
||||
console.error('Error loading business info:', error);
|
||||
this.business = { requireTwoStepApproval: false, invoiceApprover: null };
|
||||
}
|
||||
},
|
||||
// بارگذاری اطلاعات کاربر فعلی
|
||||
async loadCurrentUser() {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
this.currentUser = response.data || { email: '', owner: false };
|
||||
} catch (error) {
|
||||
console.error('Error loading current user:', error);
|
||||
this.currentUser = { email: '', owner: false };
|
||||
}
|
||||
},
|
||||
changeLabel(label) {
|
||||
if (this.itemsSelected.length === 0) {
|
||||
Swal.fire({
|
||||
|
@ -523,12 +586,19 @@ export default defineComponent({
|
|||
sortBy: this.serverOptions.sortBy, // ارسال اطلاعات مرتبسازی
|
||||
});
|
||||
|
||||
this.items = (response.data.items || []).map(item => ({
|
||||
const all = (response.data.items || []).map(item => ({
|
||||
...item,
|
||||
receivedAmount: item.relatedDocsPays || 0, // نگاشت به receivedAmount
|
||||
})).filter(item => item.code && typeof item.code !== 'undefined');
|
||||
this.items = all;
|
||||
this.total = Number(response.data.total) || 0;
|
||||
this.sumTotal = this.items.reduce((sum, item) => sum + parseInt(item.amount || 0), 0);
|
||||
if (this.business.requireTwoStepApproval) {
|
||||
this.itemsApproved = all.filter(i => i.isApproved === true);
|
||||
this.itemsPending = all.filter(i => i.isPreview === true && i.isApproved !== true);
|
||||
this.totalApproved = this.itemsApproved.length;
|
||||
this.totalPending = this.itemsPending.length;
|
||||
}
|
||||
this.sumTotal = this.displayItems.reduce((sum, item) => sum + parseInt(item.amount || 0), 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading data:', error);
|
||||
this.items = [];
|
||||
|
@ -543,6 +613,49 @@ export default defineComponent({
|
|||
this.loading = false;
|
||||
}
|
||||
},
|
||||
// نمایش متن وضعیت تأیید
|
||||
getApprovalStatusText(item) {
|
||||
if (!this.business?.requireTwoStepApproval) return 'تایید دو مرحلهای غیرفعال';
|
||||
|
||||
if (item.isPreview) return 'در انتظار تایید';
|
||||
if (item.isApproved) return 'تایید شده';
|
||||
return 'نامشخص';
|
||||
},
|
||||
// نمایش رنگ وضعیت تأیید
|
||||
getApprovalStatusColor(item) {
|
||||
if (!this.business?.requireTwoStepApproval) return 'default';
|
||||
|
||||
if (item.isPreview) return 'warning';
|
||||
if (item.isApproved) return 'success';
|
||||
return 'default';
|
||||
},
|
||||
// بررسی اینکه آیا دکمه تأیید باید نمایش داده شود
|
||||
canShowApprovalButton(item) {
|
||||
if (!this.business?.requireTwoStepApproval) return false;
|
||||
|
||||
// اگر سند قبلاً تأیید شده، دکمه تأیید نمایش داده نشود
|
||||
if (item?.isApproved) return false;
|
||||
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
// یا کاربر تأییدکننده فاکتور فروش
|
||||
return this.business?.invoiceApprover === this.currentUser?.email || this.currentUser?.owner === true;
|
||||
},
|
||||
// تایید فاکتور فروش
|
||||
async approveInvoice(code) {
|
||||
try {
|
||||
this.loading = true;
|
||||
await axios.post(`/api/approval/approve/sales/${code}`);
|
||||
|
||||
// بهروزرسانی دادهها
|
||||
await this.loadData();
|
||||
|
||||
Swal.fire({ text: 'فاکتور تایید شد', icon: 'success', confirmButtonText: 'قبول' });
|
||||
} catch (error) {
|
||||
Swal.fire({ text: 'خطا در تایید فاکتور: ' + (error.response?.data?.message || error.message), icon: 'error', confirmButtonText: 'قبول' });
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
canEditItem(code) {
|
||||
this.loading = true;
|
||||
axios.post('/api/sell/edit/can/' + code).then((response) => {
|
||||
|
@ -904,6 +1017,8 @@ export default defineComponent({
|
|||
created() {
|
||||
this.loadColumnSettings();
|
||||
this.loadPlugins();
|
||||
this.loadBusinessInfo();
|
||||
this.loadCurrentUser();
|
||||
this.loadData();
|
||||
},
|
||||
watch: {
|
||||
|
|
|
@ -311,6 +311,142 @@
|
|||
</v-card>
|
||||
|
||||
<!-- بخش کالا و خدمات -->
|
||||
|
||||
<h3 class="text-primary mt-4" v-if="isPluginActive('accpro')">تایید دومرحلهای</h3>
|
||||
<v-row v-if="isPluginActive('accpro')">
|
||||
<v-col cols="12">
|
||||
<v-switch
|
||||
v-model="content.requireTwoStepApproval"
|
||||
label="فعالسازی تایید دومرحلهای برای اسناد"
|
||||
color="primary"
|
||||
hide-details
|
||||
></v-switch>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
با فعالسازی این گزینه، تمام فاکتورها، حوالههای انبار، دریافتها و پرداختها نیاز به تایید مدیر خواهند داشت.
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- تنظیمات تاییدکنندگان -->
|
||||
<v-row v-if="content.requireTwoStepApproval && isPluginActive('accpro')">
|
||||
<v-col cols="12">
|
||||
<v-card variant="outlined" class="pa-4">
|
||||
<h4 class="text-subtitle-1 mb-3">تعیین تاییدکنندگان</h4>
|
||||
|
||||
<!-- تاییدکننده فاکتور فروش -->
|
||||
<div class="mb-4">
|
||||
<v-select
|
||||
v-model="content.invoiceApprover"
|
||||
:items="users"
|
||||
item-title="name"
|
||||
item-value="email"
|
||||
label="تاییدکننده فاکتور فروش"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hint="کاربری که میتواند فاکتورهای فروش را تایید کند"
|
||||
persistent-hint
|
||||
>
|
||||
<template v-slot:item="{ item, props }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
:color="item.owner ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
این کاربر افزون بر مدیر کسب و کار میتواند فاکتورهای فروش را تایید کند
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- تاییدکننده حواله انبار -->
|
||||
<div class="mb-4">
|
||||
<v-select
|
||||
v-model="content.warehouseApprover"
|
||||
:items="users"
|
||||
item-title="name"
|
||||
item-value="email"
|
||||
label="تاییدکننده حواله انبار"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hint="کاربری که میتواند حوالههای انبار را تایید کند"
|
||||
persistent-hint
|
||||
>
|
||||
<template v-slot:item="{ item, props }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
:color="item.owner ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
این کاربر افزون بر مدیر کسب و کار میتواند حوالههای انبار را تایید کند
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- تاییدکننده دریافت و پرداخت مالی -->
|
||||
<!-- <div class="mb-4">
|
||||
<v-select
|
||||
v-model="content.financialApprover"
|
||||
:items="users"
|
||||
item-title="name"
|
||||
item-value="email"
|
||||
label="تاییدکننده دریافت و پرداخت مالی"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
clearable
|
||||
hint="کاربری که میتواند دریافتها و پرداختها را تایید کند"
|
||||
persistent-hint
|
||||
>
|
||||
<template v-slot:item="{ item, props }">
|
||||
<v-list-item v-bind="props">
|
||||
<template v-slot:prepend>
|
||||
<v-icon
|
||||
:color="item.owner ? 'success' : 'primary'"
|
||||
size="small"
|
||||
>
|
||||
{{ item.owner ? 'mdi-crown' : 'mdi-account' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>{{ item.name }}</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ item.email }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</v-select>
|
||||
<div class="text-caption text-medium-emphasis mt-1">
|
||||
این کاربر افزون بر مدیر کسب و کار میتواند دریافتها و پرداختها را تایید کند
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="text-caption text-medium-emphasis">
|
||||
<v-icon size="small" color="info" class="me-1">mdi-information</v-icon>
|
||||
در صورت عدم انتخاب تاییدکننده، فقط مدیر کسب و کار میتواند اسناد را تایید کند
|
||||
</div>
|
||||
<div class="text-caption text-medium-emphasis mt-2">
|
||||
<v-icon size="small" color="success" class="me-1">mdi-check-circle</v-icon>
|
||||
<strong>نکته:</strong> صاحب کسب و کار همیشه میتواند تمام اسناد را تأیید کند و نیازی به تعیین مجدد ندارد
|
||||
</div>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-card variant="outlined">
|
||||
<v-card-title class="text-h6 text-primary">
|
||||
<v-icon icon="mdi-package-variant" class="mr-2"></v-icon>
|
||||
|
@ -418,7 +554,6 @@
|
|||
<v-card>
|
||||
<v-card-text>
|
||||
<h3 class="text-primary mb-4">نسخه پشتیبان از اطلاعات کسب و کار</h3>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="8">
|
||||
<v-card variant="outlined" class="mb-6">
|
||||
|
@ -563,6 +698,10 @@ export default {
|
|||
shortlinks: false,
|
||||
walletEnabled: false,
|
||||
walletMatchBank: null,
|
||||
requireTwoStepApproval: false,
|
||||
invoiceApprover: null,
|
||||
warehouseApprover: null,
|
||||
financialApprover: null,
|
||||
year: {
|
||||
startShamsi: '',
|
||||
endShamsi: '',
|
||||
|
@ -572,7 +711,9 @@ export default {
|
|||
updateBuyPrice: false,
|
||||
profitCalcType: 'lis'
|
||||
},
|
||||
users: [],
|
||||
listBanks: [],
|
||||
plugins: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
|
@ -585,6 +726,9 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
isPluginActive(plugName) {
|
||||
return this.plugins[plugName] !== undefined;
|
||||
},
|
||||
checkBanksExist() {
|
||||
if (this.listBanks.length === 0) {
|
||||
Swal.fire({
|
||||
|
@ -657,32 +801,36 @@ export default {
|
|||
//submit data
|
||||
this.loading = true;
|
||||
let data = {
|
||||
'bid': localStorage.getItem('activeBid'),
|
||||
'name': this.content.name,
|
||||
'legal_name': this.content.legal_name,
|
||||
'field': this.content.field,
|
||||
'type': this.content.type,
|
||||
'shenasemeli': this.content.shenasemeli,
|
||||
'codeeqtesadi': this.content.codeeqtesadi,
|
||||
'shomaresabt': this.content.shomaresabt,
|
||||
'country': this.content.country,
|
||||
'ostan': this.content.ostan,
|
||||
'shahrestan': this.content.shahrestan,
|
||||
'postalcode': this.content.postalcode,
|
||||
'tel': this.content.tel,
|
||||
'mobile': this.content.mobile,
|
||||
'address': this.content.address,
|
||||
'website': this.content.website,
|
||||
'email': this.content.email,
|
||||
'arzmain': this.content.arzmain,
|
||||
'maliyatafzode': this.content.maliyatafzode,
|
||||
'shortlinks': this.content.shortlinks,
|
||||
'walletEnabled': this.content.walletEnabled,
|
||||
'walletMatchBank': this.content.walletMatchBank,
|
||||
'year': this.content.year,
|
||||
'commodityUpdateBuyPriceAuto': this.content.updateBuyPrice,
|
||||
'commodityUpdateSellPriceAuto': this.content.updateSellPrice,
|
||||
'profitCalcType': this.content.profitCalcType
|
||||
'bid': localStorage.getItem('activeBid'),
|
||||
'name': this.content.name,
|
||||
'legal_name': this.content.legal_name,
|
||||
'field': this.content.field,
|
||||
'type': this.content.type,
|
||||
'shenasemeli': this.content.shenasemeli,
|
||||
'codeeqtesadi': this.content.codeeqtesadi,
|
||||
'shomaresabt': this.content.shomaresabt,
|
||||
'country': this.content.country,
|
||||
'ostan': this.content.ostan,
|
||||
'shahrestan': this.content.shahrestan,
|
||||
'postalcode': this.content.postalcode,
|
||||
'tel': this.content.tel,
|
||||
'mobile': this.content.mobile,
|
||||
'address': this.content.address,
|
||||
'website': this.content.website,
|
||||
'email': this.content.email,
|
||||
'arzmain': this.content.arzmain,
|
||||
'maliyatafzode': this.content.maliyatafzode,
|
||||
'shortlinks': this.content.shortlinks,
|
||||
'walletEnabled': this.content.walletEnabled,
|
||||
'walletMatchBank': this.content.walletMatchBank,
|
||||
'requireTwoStepApproval': this.content.requireTwoStepApproval,
|
||||
'invoiceApprover': this.content.invoiceApprover,
|
||||
'warehouseApprover': this.content.warehouseApprover,
|
||||
'financialApprover': this.content.financialApprover,
|
||||
'year': this.content.year,
|
||||
'commodityUpdateBuyPriceAuto': this.content.updateBuyPrice,
|
||||
'commodityUpdateSellPriceAuto': this.content.updateSellPrice,
|
||||
'profitCalcType': this.content.profitCalcType
|
||||
};
|
||||
|
||||
axios.post('/api/business/insert', data)
|
||||
|
@ -715,39 +863,69 @@ export default {
|
|||
},
|
||||
async beforeMount() {
|
||||
this.loading = true
|
||||
//get all money types
|
||||
axios.post("/api/money/get/all").then((response) => {
|
||||
this.moneys = response.data;
|
||||
this.content.arzmain = this.moneys[0];
|
||||
})
|
||||
|
||||
//get list of banks
|
||||
await axios.post('/api/bank/list').then((response) => {
|
||||
this.listBanks = response.data;
|
||||
});
|
||||
|
||||
//get business info
|
||||
let data = await axios.post('/api/business/get/info/' + localStorage.getItem('activeBid'))
|
||||
.then((response) => {
|
||||
this.content = response.data;
|
||||
// اگر walletMatchBank یک آبجکت است، فقط id آن را نگه میداریم
|
||||
if (this.content.walletMatchBank && typeof this.content.walletMatchBank === 'object') {
|
||||
this.content.walletMatchBank = this.content.walletMatchBank.id;
|
||||
}
|
||||
this.loading = false;
|
||||
});
|
||||
|
||||
// بررسی دسترسی settings و فعال بودن افزونه accpro
|
||||
|
||||
try {
|
||||
const permissionsResponse = await axios.post('/api/business/get/user/permissions');
|
||||
if (permissionsResponse.data.settings) {
|
||||
// بررسی فعال بودن افزونه accpro
|
||||
const pluginResponse = await axios.post('/api/plugin/check/accpro/' + localStorage.getItem('activeBid'));
|
||||
this.showBackupTab = pluginResponse.data.active;
|
||||
// ابتدا اطلاعات کسب و کار را بارگذاری کن
|
||||
const businessResponse = await axios.post('/api/business/get/info/' + localStorage.getItem('activeBid'));
|
||||
this.content = businessResponse.data;
|
||||
|
||||
// اگر walletMatchBank یک آبجکت است، فقط id آن را نگه میداریم
|
||||
if (this.content.walletMatchBank && typeof this.content.walletMatchBank === 'object') {
|
||||
this.content.walletMatchBank = this.content.walletMatchBank.id;
|
||||
}
|
||||
|
||||
// اطمینان از وجود فیلدهای تأییدکننده
|
||||
if (!this.content.hasOwnProperty('invoiceApprover')) {
|
||||
this.content.invoiceApprover = null;
|
||||
}
|
||||
if (!this.content.hasOwnProperty('warehouseApprover')) {
|
||||
this.content.warehouseApprover = null;
|
||||
}
|
||||
if (!this.content.hasOwnProperty('financialApprover')) {
|
||||
this.content.financialApprover = null;
|
||||
}
|
||||
if (!this.content.hasOwnProperty('requireTwoStepApproval')) {
|
||||
this.content.requireTwoStepApproval = false;
|
||||
}
|
||||
|
||||
// سپس سایر دادهها را بارگذاری کن
|
||||
const [moneyResponse, banksResponse, usersResponse, pluginsResponse] = await Promise.all([
|
||||
axios.post("/api/money/get/all"),
|
||||
axios.post('/api/bank/list'),
|
||||
axios.get(`/api/user/get/users/of/business/${localStorage.getItem('activeBid')}`),
|
||||
axios.post(`/api/plugin/get/actives`)
|
||||
]);
|
||||
this.plugins = pluginsResponse.data;
|
||||
this.moneys = moneyResponse.data;
|
||||
this.content.arzmain = this.moneys[0];
|
||||
this.listBanks = banksResponse.data;
|
||||
|
||||
// حذف صاحب کسب و کار از لیست کاربران تأییدکننده
|
||||
this.users = usersResponse.data.filter(user => !user.owner);
|
||||
|
||||
} catch (error) {
|
||||
console.error('خطا در بررسی دسترسیها:', error);
|
||||
this.showBackupTab = false;
|
||||
console.error('Error loading data:', error);
|
||||
Swal.fire({
|
||||
text: 'خطا در بارگذاری اطلاعات',
|
||||
icon: 'error',
|
||||
confirmButtonText: 'قبول'
|
||||
});
|
||||
} finally {
|
||||
try {
|
||||
// بررسی دسترسی settings و فعال بودن افزونه accpro و نمایش تب پشتیبان
|
||||
const permissionsResponse = await axios.post('/api/business/get/user/permissions');
|
||||
if (permissionsResponse.data.settings) {
|
||||
const pluginResponse = await axios.post('/api/plugin/check/accpro/' + localStorage.getItem('activeBid'));
|
||||
this.showBackupTab = !!pluginResponse.data.active;
|
||||
} else {
|
||||
this.showBackupTab = false;
|
||||
}
|
||||
} catch (permErr) {
|
||||
console.error('خطا در بررسی دسترسیها:', permErr);
|
||||
this.showBackupTab = false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -53,8 +53,12 @@
|
|||
<v-window-item value="output">
|
||||
<v-text-field v-model="outputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="outputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
<v-tab value="pending">در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="outputItems" :search="outputSearchValue" :loading="loading"
|
||||
<v-data-table :headers="visibleHeaders" :items="displayOutputItems" :search="outputSearchValue" :loading="loading"
|
||||
hover density="compact" class="elevation-1 text-center"
|
||||
:header-props="{ class: 'custom-header' }">
|
||||
<template v-slot:item="{ item }">
|
||||
|
@ -71,7 +75,7 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
|
@ -86,10 +90,15 @@
|
|||
</v-list>
|
||||
</v-menu>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
<td v-if="isColumnVisible('approvalStatus')" class="text-center">
|
||||
<v-chip size="small" :color="getApprovalStatusColor(item)">
|
||||
{{ getApprovalStatusText(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('approvedBy')" class="text-center">
|
||||
{{ item.approvedBy?.fullName || '-' }}
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
|
@ -103,8 +112,12 @@
|
|||
<v-window-item value="input">
|
||||
<v-text-field v-model="inputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="inputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
<v-tab value="pending">در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="inputItems" :search="inputSearchValue" :loading="loading" hover
|
||||
<v-data-table :headers="visibleHeaders" :items="displayInputItems" :search="inputSearchValue" :loading="loading" hover
|
||||
density="compact" class="elevation-1 text-center"
|
||||
:header-props="{ class: 'custom-header' }">
|
||||
<template v-slot:item="{ item }">
|
||||
|
@ -121,9 +134,9 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||
</v-list-item>
|
||||
|
@ -136,10 +149,15 @@
|
|||
</v-list>
|
||||
</v-menu>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
<td v-if="isColumnVisible('approvalStatus')" class="text-center">
|
||||
<v-chip size="small" :color="getApprovalStatusColor(item)">
|
||||
{{ getApprovalStatusText(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('approvedBy')" class="text-center">
|
||||
{{ item.approvedBy?.fullName || '-' }}
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
|
@ -153,8 +171,12 @@
|
|||
<v-window-item value="transfer">
|
||||
<v-text-field v-model="transferSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="transferSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
<v-tab value="pending">در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="transferItems" :search="transferSearchValue" :loading="loading" hover
|
||||
<v-data-table :headers="visibleHeaders" :items="displayTransferItems" :search="transferSearchValue" :loading="loading" hover
|
||||
density="compact" class="elevation-1 text-center"
|
||||
:header-props="{ class: 'custom-header' }">
|
||||
<template v-slot:item="{ item }">
|
||||
|
@ -171,7 +193,7 @@
|
|||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
|
@ -186,10 +208,15 @@
|
|||
</v-list>
|
||||
</v-menu>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ formatNumber(item.code) }}</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
<td v-if="isColumnVisible('approvalStatus')" class="text-center">
|
||||
<v-chip size="small" :color="getApprovalStatusColor(item)">
|
||||
{{ getApprovalStatusText(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('approvedBy')" class="text-center">
|
||||
{{ item.approvedBy?.fullName || '-' }}
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
|
@ -203,8 +230,12 @@
|
|||
<v-window-item value="waste">
|
||||
<v-text-field v-model="wasteSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||
density="compact" hide-details class="mb-1"></v-text-field>
|
||||
<v-tabs v-if="business.requireTwoStepApproval" v-model="wasteSubTab" color="primary" density="comfortable" class="mb-2">
|
||||
<v-tab value="approved">تایید شده</v-tab>
|
||||
<v-tab value="pending">در انتظار تایید</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-data-table :headers="visibleHeaders" :items="wasteItems" :search="wasteSearchValue" :loading="loading" hover
|
||||
<v-data-table :headers="visibleHeaders" :items="displayWasteItems" :search="wasteSearchValue" :loading="loading" hover
|
||||
density="compact" class="elevation-1 text-center"
|
||||
:header-props="{ class: 'custom-header' }">
|
||||
<template v-slot:item="{ item }">
|
||||
|
@ -217,11 +248,11 @@
|
|||
<v-list>
|
||||
<v-list-item :to="'/acc/storeroom/ticket/view/' + item.code">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="success">mdi-eye</v-icon>
|
||||
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>مشاهده</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item @click="approveTicket(item.code)">
|
||||
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||
<template v-slot:prepend>
|
||||
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||
</template>
|
||||
|
@ -238,8 +269,13 @@
|
|||
</td>
|
||||
<td v-if="isColumnVisible('code')" class="text-center">{{ item.code }}</td>
|
||||
<td v-if="isColumnVisible('date')" class="text-center">{{ item.date }}</td>
|
||||
<td v-if="isColumnVisible('status')" class="text-center">
|
||||
<v-chip size="small" :color="statusColor(item.status)">{{ statusLabel(item.status) }}</v-chip>
|
||||
<td v-if="isColumnVisible('approvalStatus')" class="text-center">
|
||||
<v-chip size="small" :color="getApprovalStatusColor(item)">
|
||||
{{ getApprovalStatusText(item) }}
|
||||
</v-chip>
|
||||
</td>
|
||||
<td v-if="isColumnVisible('approvedBy')" class="text-center">
|
||||
{{ item.approvedBy?.fullName || '-' }}
|
||||
</td>
|
||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc ? item.doc.code : '-' }}</td>
|
||||
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||
|
@ -301,14 +337,22 @@ import axios from "axios";
|
|||
interface Ticket {
|
||||
code: string;
|
||||
date: string;
|
||||
status?: string;
|
||||
doc: {
|
||||
code: string;
|
||||
preview?: boolean;
|
||||
approved?: boolean;
|
||||
};
|
||||
person: {
|
||||
nikename: string;
|
||||
};
|
||||
des: string;
|
||||
preview?: boolean;
|
||||
approved?: boolean;
|
||||
approvedBy?: {
|
||||
fullName: string;
|
||||
email: string;
|
||||
id: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface Header {
|
||||
|
@ -331,7 +375,16 @@ const outputSearchValue = ref('');
|
|||
const transferSearchValue = ref('');
|
||||
const wasteSearchValue = ref('');
|
||||
const activeTab = ref('output');
|
||||
const inputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const outputSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const transferSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const wasteSubTab = ref<'approved' | 'pending'>('approved');
|
||||
const showColumnDialog = ref(false);
|
||||
const business = ref({
|
||||
requireTwoStepApproval: false,
|
||||
warehouseApprover: null
|
||||
});
|
||||
const currentUser = ref({ email: '', owner: false });
|
||||
|
||||
// دیالوگها
|
||||
const deleteDialog = ref({
|
||||
|
@ -351,7 +404,8 @@ const allHeaders = ref<Header[]>([
|
|||
{ title: "عملیات", key: "operation", align: 'center' as const, sortable: false, width: 100, visible: true },
|
||||
{ title: "شماره", key: "code", align: 'center' as const, sortable: true, width: 100, visible: true },
|
||||
{ title: "تاریخ", key: "date", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "وضعیت", key: "status", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "وضعیت تایید", key: "approvalStatus", align: 'center' as const, sortable: true, width: 150, visible: true },
|
||||
{ title: "تاییدکننده", key: "approvedBy", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شماره فاکتور", key: "doc.code", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "شخص", key: "person.nikename", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||
{ title: "توضیحات", key: "des", align: 'center' as const, sortable: true, width: 200, visible: true },
|
||||
|
@ -359,12 +413,26 @@ const allHeaders = ref<Header[]>([
|
|||
|
||||
// ستونهای قابل نمایش
|
||||
const visibleHeaders = computed(() => {
|
||||
return allHeaders.value.filter((header: Header) => header.visible) as any;
|
||||
return allHeaders.value.filter((header: Header) => {
|
||||
// اگر ستونهای تأیید هستند، باید دو مرحلهای فعال باشد
|
||||
if ((header.key === 'approvalStatus' || header.key === 'approvedBy') && !business.value.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
return header.visible;
|
||||
}) as any;
|
||||
});
|
||||
|
||||
// بررسی نمایش ستون
|
||||
const isColumnVisible = (key: string) => {
|
||||
return allHeaders.value.find((header: Header) => header.key === key)?.visible;
|
||||
const header = allHeaders.value.find((header: Header) => header.key === key);
|
||||
if (!header) return false;
|
||||
|
||||
// اگر ستونهای تأیید هستند، باید دو مرحلهای فعال باشد
|
||||
if ((key === 'approvalStatus' || key === 'approvedBy') && !business.value.requireTwoStepApproval) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return header.visible;
|
||||
};
|
||||
|
||||
// کلید ذخیرهسازی در localStorage
|
||||
|
@ -395,6 +463,57 @@ const formatNumber = (value: string | number) => {
|
|||
return Number(value).toLocaleString('fa-IR');
|
||||
};
|
||||
|
||||
// فهرستهای نمایشی بر اساس ساب-تب و دو مرحلهای
|
||||
const displayInputItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return inputItems.value;
|
||||
return inputSubTab.value === 'pending'
|
||||
? inputItems.value.filter(i => i.preview && !i.approved)
|
||||
: inputItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayOutputItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return outputItems.value;
|
||||
return outputSubTab.value === 'pending'
|
||||
? outputItems.value.filter(i => i.preview && !i.approved)
|
||||
: outputItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayTransferItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return transferItems.value;
|
||||
return transferSubTab.value === 'pending'
|
||||
? transferItems.value.filter(i => i.preview && !i.approved)
|
||||
: transferItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
const displayWasteItems = computed(() => {
|
||||
if (!business.value.requireTwoStepApproval) return wasteItems.value;
|
||||
return wasteSubTab.value === 'pending'
|
||||
? wasteItems.value.filter(i => i.preview && !i.approved)
|
||||
: wasteItems.value.filter(i => i.approved);
|
||||
});
|
||||
|
||||
// بارگذاری اطلاعات بیزنس
|
||||
const loadBusinessInfo = async () => {
|
||||
try {
|
||||
const response = await axios.get('/api/business/get/info/' + localStorage.getItem('activeBid'));
|
||||
business.value = response.data || { requireTwoStepApproval: false, warehouseApprover: null };
|
||||
} catch (error: any) {
|
||||
console.error('Error loading business info:', error);
|
||||
business.value = { requireTwoStepApproval: false, warehouseApprover: null };
|
||||
}
|
||||
};
|
||||
|
||||
// بارگذاری اطلاعات کاربر فعلی
|
||||
const loadCurrentUser = async () => {
|
||||
try {
|
||||
const response = await axios.post('/api/business/get/user/permissions');
|
||||
currentUser.value = response.data || { email: '', owner: false };
|
||||
} catch (error: any) {
|
||||
console.error('Error loading current user:', error);
|
||||
currentUser.value = { email: '', owner: false };
|
||||
}
|
||||
};
|
||||
|
||||
// بارگذاری دادهها
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
|
@ -405,10 +524,10 @@ const loadData = async () => {
|
|||
axios.post('/api/storeroom/tickets/list/transfer'),
|
||||
axios.post('/api/storeroom/tickets/list/waste')
|
||||
]);
|
||||
inputItems.value = (inputResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
outputItems.value = (outputResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
transferItems.value = (transferResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
wasteItems.value = (wasteResponse.data || []).map((i: any) => ({ status: i.status || 'in_progress', ...i }));
|
||||
inputItems.value = (inputResponse.data || []);
|
||||
outputItems.value = (outputResponse.data || []);
|
||||
transferItems.value = (transferResponse.data || []);
|
||||
wasteItems.value = (wasteResponse.data || []);
|
||||
} catch (error: any) {
|
||||
console.error('Error loading data:', error);
|
||||
snackbar.value = {
|
||||
|
@ -421,16 +540,27 @@ const loadData = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// بررسی اینکه آیا دکمه تأیید باید نمایش داده شود
|
||||
const canShowApprovalButton = (item: Ticket) => {
|
||||
if (!business.value.requireTwoStepApproval) return false;
|
||||
|
||||
// اگر سند قبلاً تأیید شده، دکمه تأیید نمایش داده نشود
|
||||
if (item?.approved) return false;
|
||||
|
||||
// مدیر کسب و کار همیشه میتواند تأیید کند
|
||||
// یا کاربر تأییدکننده انبار
|
||||
return business.value.warehouseApprover === currentUser.value.email || currentUser.value.owner === true;
|
||||
};
|
||||
|
||||
// تایید حواله
|
||||
const approveTicket = async (code: string) => {
|
||||
try {
|
||||
loading.value = true;
|
||||
await axios.post(`/api/storeroom/ticket/status/${code}`, { status: 'approved' });
|
||||
// بهروزرسانی وضعیت در لیستهای محلی
|
||||
[inputItems, outputItems, transferItems, wasteItems].forEach(listRef => {
|
||||
const idx = listRef.value.findIndex((x: any) => x.code === code);
|
||||
if (idx !== -1) listRef.value[idx].status = 'approved';
|
||||
});
|
||||
await axios.post(`/api/approval/approve/storeroom/${code}`);
|
||||
|
||||
// بهروزرسانی دادهها
|
||||
await loadData();
|
||||
|
||||
snackbar.value = { show: true, message: 'حواله تایید شد', color: 'success' };
|
||||
} catch (error: any) {
|
||||
snackbar.value = { show: true, message: 'خطا در تایید حواله: ' + (error.response?.data?.message || error.message), color: 'error' };
|
||||
|
@ -439,27 +569,22 @@ const approveTicket = async (code: string) => {
|
|||
}
|
||||
};
|
||||
|
||||
// نمایش برچسب وضعیت
|
||||
const statusLabel = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'تایید شده';
|
||||
case 'pending_approval': return 'در انتظار تایید';
|
||||
case 'in_progress': return 'در حال انجام';
|
||||
case 'done': return 'انجام شده';
|
||||
case 'rejected': return 'رد شده';
|
||||
default: return status || '-';
|
||||
}
|
||||
// نمایش متن وضعیت تأیید
|
||||
const getApprovalStatusText = (item: Ticket) => {
|
||||
if (!business.value.requireTwoStepApproval) return 'تایید دو مرحلهای غیرفعال';
|
||||
|
||||
if (item?.preview) return 'در انتظار تایید';
|
||||
if (item?.approved) return 'تایید شده';
|
||||
return 'نامشخص';
|
||||
};
|
||||
|
||||
const statusColor = (status?: string) => {
|
||||
switch (status) {
|
||||
case 'approved': return 'success';
|
||||
case 'pending_approval': return 'warning';
|
||||
case 'in_progress': return 'info';
|
||||
case 'done': return 'primary';
|
||||
case 'rejected': return 'error';
|
||||
default: return 'default';
|
||||
}
|
||||
// نمایش رنگ وضعیت تأیید
|
||||
const getApprovalStatusColor = (item: Ticket) => {
|
||||
if (!business.value.requireTwoStepApproval) return 'default';
|
||||
|
||||
if (item?.preview) return 'warning';
|
||||
if (item?.approved) return 'success';
|
||||
return 'default';
|
||||
};
|
||||
|
||||
// حذف حواله
|
||||
|
@ -517,6 +642,8 @@ const confirmDelete = async () => {
|
|||
// مانت کامپوننت
|
||||
onMounted(() => {
|
||||
loadColumnSettings();
|
||||
loadBusinessInfo();
|
||||
loadCurrentUser();
|
||||
loadData();
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { ref, onMounted, computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import { useRouter } from 'vue-router'
|
||||
import moment from 'jalali-moment'
|
||||
|
||||
interface Business {
|
||||
legal_name: string
|
||||
|
@ -19,6 +20,8 @@ interface Ticket {
|
|||
type: string
|
||||
typeString: string
|
||||
storeroom: Storeroom
|
||||
preview: boolean
|
||||
approved: boolean
|
||||
}
|
||||
|
||||
interface Person {
|
||||
|
@ -70,7 +73,9 @@ const item = ref<Item>({
|
|||
typeString: '',
|
||||
storeroom: {
|
||||
manager: ''
|
||||
}
|
||||
},
|
||||
preview: false,
|
||||
approved: false
|
||||
},
|
||||
rows: [],
|
||||
person: {
|
||||
|
@ -136,13 +141,15 @@ const selectedFile = ref<File | null>(null)
|
|||
const attachDes = ref('')
|
||||
|
||||
const canPrint = computed(() => {
|
||||
// اگر نیاز به تایید دو مرحلهای وجود داشته باشد بکاند 403 میدهد، اما برای UX بهتر اینجا هم کنترل کنیم.
|
||||
// اگر وضعیت حواله pending_approval باشد، چاپ را غیرفعال کنیم
|
||||
const st = (item.value as any)?.ticket?.status as string | undefined
|
||||
if (!st) return true
|
||||
return st === 'approved' || st === 'done' || st === 'in_progress'
|
||||
})
|
||||
|
||||
const isAttachmentsDisabled = computed(() => {
|
||||
return item.value.ticket.preview
|
||||
})
|
||||
|
||||
const loadAttachments = async () => {
|
||||
try {
|
||||
loadingAttachments.value = true
|
||||
|
@ -175,6 +182,98 @@ const uploadAttachment = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
const downloadAttachment = async (att: any) => {
|
||||
try {
|
||||
const url = `/api/storeroom/ticket/attachments/download/${att.id}`
|
||||
const res = await axios.get(url, { responseType: 'blob' })
|
||||
const blob = new Blob([res.data], { type: res.headers['content-type'] || 'application/octet-stream' })
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = blobUrl
|
||||
const base = (att.filename || '').split('/').pop() || `attachment-${att.id}`
|
||||
link.download = base
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
link.remove()
|
||||
window.URL.revokeObjectURL(blobUrl)
|
||||
} catch (e) {
|
||||
// Silent for now or show toast
|
||||
}
|
||||
}
|
||||
|
||||
// UX helpers for attachments
|
||||
const getFileName = (path?: string) => {
|
||||
if (!path) return 'بدوننام'
|
||||
const clean = String(path).replace(/\\/g, '/')
|
||||
const parts = clean.split('/')
|
||||
return parts[parts.length - 1] || clean
|
||||
}
|
||||
|
||||
const formatSize = (size?: string | number | null) => {
|
||||
const n = Number(size || 0)
|
||||
if (!isFinite(n) || n <= 0) return '-'
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let idx = 0
|
||||
let val = n
|
||||
while (val >= 1024 && idx < units.length - 1) {
|
||||
val /= 1024
|
||||
idx++
|
||||
}
|
||||
return `${val.toFixed(idx === 0 ? 0 : 1)} ${units[idx]}`
|
||||
}
|
||||
|
||||
const getMimeIcon = (mime?: string | null) => {
|
||||
const m = (mime || '').toLowerCase()
|
||||
if (m.startsWith('image/')) return 'mdi-image'
|
||||
if (m.includes('pdf')) return 'mdi-file-pdf-box'
|
||||
if (m.includes('word') || m.endsWith('/msword') || m.includes('officedocument.word')) return 'mdi-file-word-box'
|
||||
if (m.includes('excel') || m.includes('spreadsheet')) return 'mdi-file-excel-box'
|
||||
if (m.includes('zip') || m.includes('rar') || m.includes('7z')) return 'mdi-zip-box'
|
||||
if (m.includes('text')) return 'mdi-file-document-outline'
|
||||
return 'mdi-file'
|
||||
}
|
||||
|
||||
const isPreviewable = (mime?: string | null) => {
|
||||
const m = (mime || '').toLowerCase()
|
||||
return m.startsWith('image/') || m.includes('pdf')
|
||||
}
|
||||
|
||||
const previewDialog = ref(false)
|
||||
const previewUrl = ref<string | null>(null)
|
||||
const previewMime = ref<string | null>(null)
|
||||
const previewName = ref<string>('')
|
||||
|
||||
const closePreview = () => {
|
||||
if (previewUrl.value) {
|
||||
try { window.URL.revokeObjectURL(previewUrl.value) } catch {}
|
||||
}
|
||||
previewUrl.value = null
|
||||
previewMime.value = null
|
||||
previewName.value = ''
|
||||
previewDialog.value = false
|
||||
}
|
||||
|
||||
const previewAttachment = async (att: any) => {
|
||||
try {
|
||||
const mime = att.fileType as string | undefined
|
||||
if (!isPreviewable(mime)) {
|
||||
await downloadAttachment(att)
|
||||
return
|
||||
}
|
||||
const url = `/api/storeroom/ticket/attachments/download/${att.id}`
|
||||
const res = await axios.get(url, { responseType: 'blob' })
|
||||
const blob = new Blob([res.data], { type: res.headers['content-type'] || mime || 'application/octet-stream' })
|
||||
const blobUrl = window.URL.createObjectURL(blob)
|
||||
previewUrl.value = blobUrl
|
||||
previewMime.value = mime || res.headers['content-type'] || 'application/octet-stream'
|
||||
previewName.value = getFileName(att.filename)
|
||||
previewDialog.value = true
|
||||
} catch (e) {
|
||||
// fallback to download on error
|
||||
await downloadAttachment(att)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadAttachments()
|
||||
|
@ -343,10 +442,11 @@ onMounted(() => {
|
|||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<v-card variant="outlined" class="mt-4">
|
||||
<v-card variant="outlined" class="mt-4" :class="{ 'opacity-50': isAttachmentsDisabled }">
|
||||
<v-card-title class="text-subtitle-1 font-weight-bold">
|
||||
<v-icon start>mdi-paperclip</v-icon>
|
||||
پیوستها
|
||||
<v-chip v-if="isAttachmentsDisabled" color="warning" size="small" class="ml-2">در انتظار تایید</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<div class="d-flex align-center gap-2 mb-3">
|
||||
|
@ -358,6 +458,7 @@ onMounted(() => {
|
|||
density="compact"
|
||||
hide-details
|
||||
style="max-width: 360px"
|
||||
:disabled="isAttachmentsDisabled"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="attachDes"
|
||||
|
@ -365,30 +466,92 @@ onMounted(() => {
|
|||
variant="outlined"
|
||||
density="compact"
|
||||
hide-details
|
||||
:disabled="isAttachmentsDisabled"
|
||||
/>
|
||||
<v-btn color="primary" :loading="uploading" :disabled="!selectedFile" @click="uploadAttachment">آپلود</v-btn>
|
||||
<v-btn color="primary" :loading="uploading" :disabled="!selectedFile || isAttachmentsDisabled" @click="uploadAttachment">آپلود</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn variant="text" icon="mdi-refresh" :loading="loadingAttachments" @click="loadAttachments" />
|
||||
<v-btn variant="text" icon="mdi-refresh" :loading="loadingAttachments" :disabled="isAttachmentsDisabled" @click="loadAttachments" />
|
||||
</div>
|
||||
<v-data-table
|
||||
:headers="[
|
||||
{ title: 'نام فایل', key: 'filename' },
|
||||
{ title: 'نام فایل', key: 'fileDisplay' },
|
||||
{ title: 'نوع', key: 'fileType' },
|
||||
{ title: 'حجم', key: 'fileSize' },
|
||||
{ title: 'توضیحات', key: 'des' },
|
||||
{ title: 'تاریخ', key: 'dateSubmit' },
|
||||
{ title: 'عملیات', key: 'actions' },
|
||||
]"
|
||||
:items="attachments"
|
||||
:loading="loadingAttachments"
|
||||
density="comfortable"
|
||||
class="elevation-1"
|
||||
>
|
||||
<template #item.filename="{ item }">
|
||||
<a :href="item.filename" target="_blank" rel="noreferrer noopener">{{ item.filename }}</a>
|
||||
<template #item.fileDisplay="{ item }">
|
||||
<div class="d-flex align-center justify-center">
|
||||
<span class="text-truncate" style="max-width: 320px">{{ getFileName(item.filename) }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #item.fileType="{ item }">
|
||||
<code class="text-caption text-center d-flex align-center justify-center text-black">{{ item.fileType.split('/')[1].toUpperCase() || '-' }}<v-icon class="ml-2">{{ getMimeIcon(item.fileType) }}</v-icon></code>
|
||||
</template>
|
||||
<template #item.fileSize="{ item }">
|
||||
{{ formatSize(item.fileSize) }}
|
||||
</template>
|
||||
<template #item.dateSubmit="{ item }">
|
||||
{{ moment(item.dateSubmit, 'YYYY/MM/DD').locale('fa').format('YYYY/MM/DD') }}
|
||||
</template>
|
||||
<template #item.actions="{ item }">
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
title="دانلود"
|
||||
@click="downloadAttachment(item)"
|
||||
>
|
||||
<v-icon>mdi-download</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
icon
|
||||
variant="text"
|
||||
size="small"
|
||||
title="پیشنمایش"
|
||||
@click="previewAttachment(item)"
|
||||
>
|
||||
<v-icon>mdi-eye</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-data-table>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
|
||||
<!-- Preview dialog -->
|
||||
<v-dialog v-model="previewDialog" max-width="900px">
|
||||
<v-card>
|
||||
<v-toolbar density="compact" color="primary">
|
||||
<v-toolbar-title class="text-subtitle-2 text-white">
|
||||
<v-icon start color="white">mdi-file-eye</v-icon>
|
||||
{{ previewName }}
|
||||
</v-toolbar-title>
|
||||
<v-spacer />
|
||||
<v-btn icon @click="closePreview"><v-icon color="white">mdi-close</v-icon></v-btn>
|
||||
</v-toolbar>
|
||||
<v-card-text class="pa-0">
|
||||
<div v-if="previewUrl && previewMime?.includes('pdf')" style="height:70vh">
|
||||
<iframe :src="previewUrl" style="width:100%;height:100%" frameborder="0"></iframe>
|
||||
</div>
|
||||
<div v-else-if="previewUrl && previewMime?.startsWith('image/')" class="d-flex justify-center">
|
||||
<img :src="previewUrl" :alt="previewName" style="max-width:100%;max-height:70vh" />
|
||||
</div>
|
||||
<div v-else class="pa-6 text-center">
|
||||
پیشنمایش در دسترس نیست. لطفاً دانلود کنید.
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn color="primary" @click="() => { /* fallback */ closePreview() }">بستن</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
|
|
25
webUI/src/views/public/PublicLayout.vue
Normal file
25
webUI/src/views/public/PublicLayout.vue
Normal file
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<v-app>
|
||||
<!-- Main Content -->
|
||||
<v-main>
|
||||
<router-view />
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'PublicLayout',
|
||||
methods: {
|
||||
goToLogin() {
|
||||
this.$router.push({ name: 'user_login' })
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.v-app-bar {
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
</style>
|
2291
webUI/src/views/public/WarrantyActivation.vue
Normal file
2291
webUI/src/views/public/WarrantyActivation.vue
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue