Compare commits
4 commits
50ca4045bc
...
0ca8557fca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ca8557fca | ||
|
|
ded4cff458 | ||
|
|
d231e81252 | ||
|
|
c09fe66a5f |
|
|
@ -16,6 +16,7 @@
|
||||||
"doctrine/orm": "^3.2",
|
"doctrine/orm": "^3.2",
|
||||||
"dompdf/dompdf": "^3.0",
|
"dompdf/dompdf": "^3.0",
|
||||||
"melipayamak/php": "^1.0",
|
"melipayamak/php": "^1.0",
|
||||||
|
"morilog/jalali": "*",
|
||||||
"mpdf/mpdf": "^8.2",
|
"mpdf/mpdf": "^8.2",
|
||||||
"nelmio/api-doc-bundle": "^4.35",
|
"nelmio/api-doc-bundle": "^4.35",
|
||||||
"nelmio/cors-bundle": "^2.5",
|
"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",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "43db0ad2bb94569ed6d44cabf503210e",
|
"content-hash": "01b5daf5a6fd011b4eb616e0e4ae18fe",
|
||||||
"packages": [
|
"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",
|
"name": "brick/math",
|
||||||
"version": "0.12.3",
|
"version": "0.12.3",
|
||||||
|
|
@ -66,6 +133,75 @@
|
||||||
],
|
],
|
||||||
"time": "2025-02-28T13:11:00+00:00"
|
"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",
|
"name": "composer/pcre",
|
||||||
"version": "3.3.2",
|
"version": "3.3.2",
|
||||||
|
|
@ -2297,6 +2433,71 @@
|
||||||
],
|
],
|
||||||
"time": "2025-03-24T10:02:05+00:00"
|
"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",
|
"name": "mpdf/mpdf",
|
||||||
"version": "v8.2.5",
|
"version": "v8.2.5",
|
||||||
|
|
@ -2714,6 +2915,111 @@
|
||||||
},
|
},
|
||||||
"time": "2024-06-24T21:25:28+00:00"
|
"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",
|
"name": "nikic/php-parser",
|
||||||
"version": "v5.4.0",
|
"version": "v5.4.0",
|
||||||
|
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
<?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 Version20241220000000 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Add memberCount field to chat_channel table';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
// Add memberCount column to chat_channel table
|
|
||||||
$this->addSql('ALTER TABLE chat_channel ADD member_count INT NOT NULL DEFAULT 0');
|
|
||||||
|
|
||||||
// Update existing channels with correct member count
|
|
||||||
$this->addSql('
|
|
||||||
UPDATE chat_channel c
|
|
||||||
SET member_count = (
|
|
||||||
SELECT COUNT(*)
|
|
||||||
FROM chat_channel_member m
|
|
||||||
WHERE m.channel_id = c.id AND m.is_active = 1
|
|
||||||
)
|
|
||||||
');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE chat_channel DROP member_count');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace DoctrineMigrations;
|
|
||||||
|
|
||||||
use Doctrine\DBAL\Schema\Schema;
|
|
||||||
use Doctrine\Migrations\AbstractMigration;
|
|
||||||
|
|
||||||
final class Version20250809100001 extends AbstractMigration
|
|
||||||
{
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return 'Create custom_invoice_template table';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function up(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('CREATE TABLE custom_invoice_template (id INT AUTO_INCREMENT NOT NULL, bid_id INT NOT NULL, submitter_id INT NOT NULL, name VARCHAR(255) NOT NULL, is_public TINYINT(1) NOT NULL, code LONGTEXT NOT NULL, INDEX IDX_CUSTOM_INV_TPL_BID (bid_id), INDEX IDX_CUSTOM_INV_TPL_SUBMITTER (submitter_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
|
||||||
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_BID FOREIGN KEY (bid_id) REFERENCES business (id)');
|
|
||||||
$this->addSql('ALTER TABLE custom_invoice_template ADD CONSTRAINT FK_CUSTOM_INV_TPL_SUBMITTER FOREIGN KEY (submitter_id) REFERENCES `user` (id)');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function down(Schema $schema): void
|
|
||||||
{
|
|
||||||
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_BID');
|
|
||||||
$this->addSql('ALTER TABLE custom_invoice_template DROP FOREIGN KEY FK_CUSTOM_INV_TPL_SUBMITTER');
|
|
||||||
$this->addSql('DROP TABLE custom_invoice_template');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
31
hesabixCore/migrations/Version20250811093832.php
Normal file
31
hesabixCore/migrations/Version20250811093832.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 Version20250811093832 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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
31
hesabixCore/migrations/Version20250811101253.php
Normal file
31
hesabixCore/migrations/Version20250811101253.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 Version20250811101253 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
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
32
hesabixCore/migrations/Version20250811120010.php
Normal file
32
hesabixCore/migrations/Version20250811120010.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250811120010 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add warranty usage columns to plug_warranty_serial table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial ADD used TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial ADD used_at VARCHAR(50) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial ADD used_ticket_code VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial DROP used');
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial DROP used_at');
|
||||||
|
$this->addSql('ALTER TABLE plug_warranty_serial DROP used_ticket_code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
30
hesabixCore/migrations/Version20250811123020.php
Normal file
30
hesabixCore/migrations/Version20250811123020.php
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250811123020 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add status and importWorkflowCode to storeroom_ticket';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE storeroom_ticket ADD status VARCHAR(50) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE storeroom_ticket ADD import_workflow_code VARCHAR(255) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE storeroom_ticket DROP status');
|
||||||
|
$this->addSql('ALTER TABLE storeroom_ticket DROP import_workflow_code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
32
hesabixCore/migrations/Version20250811124530.php
Normal file
32
hesabixCore/migrations/Version20250811124530.php
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20250811124530 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add two-step approval flags to permission table';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE permission ADD require_two_step_sell TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE permission ADD require_two_step_payment TINYINT(1) DEFAULT NULL');
|
||||||
|
$this->addSql('ALTER TABLE permission ADD require_two_step_store TINYINT(1) DEFAULT NULL');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('ALTER TABLE permission DROP require_two_step_sell');
|
||||||
|
$this->addSql('ALTER TABLE permission DROP require_two_step_payment');
|
||||||
|
$this->addSql('ALTER TABLE permission DROP require_two_step_store');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
hesabixCore/migrations/Version20250816185111.php
Normal file
35
hesabixCore/migrations/Version20250816185111.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 Version20250816185111 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 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
|
||||||
|
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,6 +288,7 @@ class PersonService
|
||||||
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
|
if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']);
|
||||||
if (isset($params['company'])) $person->setCompany($params['company']);
|
if (isset($params['company'])) $person->setCompany($params['company']);
|
||||||
if (isset($params['tags'])) $person->setTags($params['tags']);
|
if (isset($params['tags'])) $person->setTags($params['tags']);
|
||||||
|
|
||||||
if (array_key_exists('prelabel', $params)) {
|
if (array_key_exists('prelabel', $params)) {
|
||||||
if ($params['prelabel'] != '') {
|
if ($params['prelabel'] != '') {
|
||||||
$prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $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);
|
$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
|
//get Money type
|
||||||
if (!array_key_exists('arzmain', $params) && $isNew) {
|
if (!array_key_exists('arzmain', $params) && $isNew) {
|
||||||
|
|
@ -548,6 +564,8 @@ class BusinessController extends AbstractController
|
||||||
'plugWarranty' => true,
|
'plugWarranty' => true,
|
||||||
'inquiry' => true,
|
'inquiry' => true,
|
||||||
'ai' => true,
|
'ai' => true,
|
||||||
|
'warehouseManager' => true,
|
||||||
|
'importWorkflow' => true,
|
||||||
];
|
];
|
||||||
} elseif ($perm) {
|
} elseif ($perm) {
|
||||||
$result = [
|
$result = [
|
||||||
|
|
@ -595,7 +613,16 @@ class BusinessController extends AbstractController
|
||||||
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
'plugWarranty' => $perm->isPlugWarrantyManager(),
|
||||||
'inquiry' => $perm->isInquiry(),
|
'inquiry' => $perm->isInquiry(),
|
||||||
'ai' => $perm->isAi(),
|
'ai' => $perm->isAi(),
|
||||||
|
'warehouseManager' => $perm->isWarehouseManager(),
|
||||||
|
'importWorkflow' => $perm->isImportWorkflow(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if ($perm->isWarehouseManager()) {
|
||||||
|
$result['commodity'] = true;
|
||||||
|
$result['store'] = true;
|
||||||
|
$result['plugWarranty'] = true;
|
||||||
|
$result['permission'] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return $this->json($result);
|
return $this->json($result);
|
||||||
}
|
}
|
||||||
|
|
@ -668,6 +695,8 @@ class BusinessController extends AbstractController
|
||||||
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
$perm->setPlugTaxSettings($params['plugTaxSettings']);
|
||||||
$perm->setInquiry($params['inquiry']);
|
$perm->setInquiry($params['inquiry']);
|
||||||
$perm->setAi($params['ai']);
|
$perm->setAi($params['ai']);
|
||||||
|
$perm->setWarehouseManager($params['warehouseManager'] ?? false);
|
||||||
|
$perm->setImportWorkflow($params['importWorkflow'] ?? false);
|
||||||
$entityManager->persist($perm);
|
$entityManager->persist($perm);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
$log->insert('تنظیمات پایه', 'ویرایش دسترسیهای کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
|
$log->insert('تنظیمات پایه', 'ویرایش دسترسیهای کاربر با پست الکترونیکی ' . $user->getEmail(), $this->getUser(), $business);
|
||||||
|
|
|
||||||
1111
hesabixCore/src/Controller/ImportWorkflowController.php
Normal file
1111
hesabixCore/src/Controller/ImportWorkflowController.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -498,15 +498,24 @@ class PluginController extends AbstractController
|
||||||
'icon' => ' taxplugin.jpg',
|
'icon' => ' taxplugin.jpg',
|
||||||
'defaultOn' => null,
|
'defaultOn' => null,
|
||||||
],
|
],
|
||||||
// [
|
[
|
||||||
// 'name' => 'مدیریت گارانتی',
|
'name' => 'مدیریت گارانتی',
|
||||||
// 'code' => 'warranty',
|
'code' => 'warranty',
|
||||||
// 'timestamp' => '32104000',
|
'timestamp' => '32104000',
|
||||||
// 'timelabel' => 'یک سال',
|
'timelabel' => 'یک سال',
|
||||||
// 'price' => '200000',
|
'price' => '200000',
|
||||||
// 'icon' => 'warranty.png',
|
'icon' => 'warranty.png',
|
||||||
// 'defaultOn' => null,
|
'defaultOn' => null,
|
||||||
// ],
|
],
|
||||||
|
[
|
||||||
|
'name' => 'مدیریت واردات کالا',
|
||||||
|
'code' => 'import-workflow',
|
||||||
|
'timestamp' => '32104000',
|
||||||
|
'timelabel' => 'یک سال',
|
||||||
|
'price' => '200000',
|
||||||
|
'icon' => 'import-workflow.png',
|
||||||
|
'defaultOn' => null,
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
$repo = $entityManager->getRepository(PluginProdect::class);
|
$repo = $entityManager->getRepository(PluginProdect::class);
|
||||||
|
|
|
||||||
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]));
|
||||||
|
}
|
||||||
|
|
@ -68,6 +68,41 @@ class SellController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/approve/{code}', name: 'app_sell_approve', methods: ['POST'])]
|
||||||
|
public function approveSellDoc(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) throw $this->createAccessDeniedException();
|
||||||
|
$doc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([
|
||||||
|
'bid' => $acc['bid'],
|
||||||
|
'code' => $code,
|
||||||
|
'money' => $acc['money']
|
||||||
|
]);
|
||||||
|
if (!$doc) throw $this->createNotFoundException('فاکتور یافت نشد');
|
||||||
|
$doc->setStatus('approved');
|
||||||
|
$entityManager->persist($doc);
|
||||||
|
$entityManager->flush();
|
||||||
|
return $this->json(['result' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/sell/payment/approve/{code}', name: 'app_sell_payment_approve', methods: ['POST'])]
|
||||||
|
public function approveSellPayment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('sell');
|
||||||
|
if (!$acc) throw $this->createAccessDeniedException();
|
||||||
|
$paymentDoc = $entityManager->getRepository(\App\Entity\HesabdariDoc::class)->findOneBy([
|
||||||
|
'bid' => $acc['bid'],
|
||||||
|
'code' => $code,
|
||||||
|
'money' => $acc['money'],
|
||||||
|
'type' => 'sell_receive'
|
||||||
|
]);
|
||||||
|
if (!$paymentDoc) throw $this->createNotFoundException('سند دریافت یافت نشد');
|
||||||
|
$paymentDoc->setStatus('approved');
|
||||||
|
$entityManager->persist($paymentDoc);
|
||||||
|
$entityManager->flush();
|
||||||
|
return $this->json(['result' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/api/sell/get/info/{code}', name: 'app_sell_get_info')]
|
#[Route('/api/sell/get/info/{code}', name: 'app_sell_get_info')]
|
||||||
public function app_sell_get_info(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, string $code): JsonResponse
|
public function app_sell_get_info(Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, string $code): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
@ -195,6 +230,16 @@ class SellController extends AbstractController
|
||||||
$doc->setSubmitter($this->getUser());
|
$doc->setSubmitter($this->getUser());
|
||||||
$doc->setMoney($acc['money']);
|
$doc->setMoney($acc['money']);
|
||||||
$doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting'));
|
$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) {
|
if ($params['transferCost'] != 0) {
|
||||||
$hesabdariRow = new HesabdariRow();
|
$hesabdariRow = new HesabdariRow();
|
||||||
|
|
@ -287,6 +332,18 @@ class SellController extends AbstractController
|
||||||
$hesabdariRow->setPerson($person);
|
$hesabdariRow->setPerson($person);
|
||||||
$entityManager->persist($hesabdariRow);
|
$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->persist($doc);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
if (!$doc->getShortlink()) {
|
if (!$doc->getShortlink()) {
|
||||||
|
|
@ -424,10 +481,13 @@ class SellController extends AbstractController
|
||||||
|
|
||||||
$queryBuilder = $entityManager->createQueryBuilder()
|
$queryBuilder = $entityManager->createQueryBuilder()
|
||||||
->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount')
|
->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('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')
|
->addSelect('l.code as labelCode, l.label as labelLabel')
|
||||||
->from(HesabdariDoc::class, 'd')
|
->from(HesabdariDoc::class, 'd')
|
||||||
->leftJoin('d.submitter', 'u')
|
->leftJoin('d.submitter', 'u')
|
||||||
|
->leftJoin('d.approvedBy', 'approver')
|
||||||
->leftJoin('d.InvoiceLabel', 'l')
|
->leftJoin('d.InvoiceLabel', 'l')
|
||||||
->leftJoin('d.hesabdariRows', 'r')
|
->leftJoin('d.hesabdariRows', 'r')
|
||||||
->where('d.bid = :bid')
|
->where('d.bid = :bid')
|
||||||
|
|
@ -489,7 +549,9 @@ class SellController extends AbstractController
|
||||||
'plugin' => 'd.plugin',
|
'plugin' => 'd.plugin',
|
||||||
'refData' => 'd.refData',
|
'refData' => 'd.refData',
|
||||||
'shortlink' => 'd.shortlink',
|
'shortlink' => 'd.shortlink',
|
||||||
'status' => 'd.status',
|
'isPreview' => 'd.isPreview',
|
||||||
|
'isApproved' => 'd.isApproved',
|
||||||
|
'approvedBy' => 'd.approvedBy',
|
||||||
'submitter' => 'u.fullName',
|
'submitter' => 'u.fullName',
|
||||||
'label' => 'l.label', // از InvoiceLabel
|
'label' => 'l.label', // از InvoiceLabel
|
||||||
];
|
];
|
||||||
|
|
@ -535,6 +597,13 @@ class SellController extends AbstractController
|
||||||
'code' => $doc['labelCode'],
|
'code' => $doc['labelCode'],
|
||||||
'label' => $doc['labelLabel']
|
'label' => $doc['labelLabel']
|
||||||
] : null,
|
] : 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)
|
$mainRow = $entityManager->getRepository(HesabdariRow::class)
|
||||||
|
|
@ -778,6 +847,10 @@ class SellController extends AbstractController
|
||||||
$accountStatus['label'] = 'بدهکار';
|
$accountStatus['label'] = 'بدهکار';
|
||||||
$accountStatus['value'] = $bd - $bs;
|
$accountStatus['value'] = $bd - $bs;
|
||||||
}
|
}
|
||||||
|
// فقط در صورت تایید نهایی مجاز به چاپ هستیم
|
||||||
|
if ($doc->getStatus() !== 'approved') {
|
||||||
|
return $this->json(['result' => -10, 'message' => 'فاکتور هنوز تایید نشده است'], 403);
|
||||||
|
}
|
||||||
if ($params['pdf'] == true || $params['printers'] == true) {
|
if ($params['pdf'] == true || $params['printers'] == true) {
|
||||||
$note = '';
|
$note = '';
|
||||||
if ($printSettings) {
|
if ($printSettings) {
|
||||||
|
|
@ -1187,6 +1260,19 @@ class SellController extends AbstractController
|
||||||
$hesabdariRow->setPerson($person);
|
$hesabdariRow->setPerson($person);
|
||||||
$entityManager->persist($hesabdariRow);
|
$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->persist($doc);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|
@ -1272,6 +1358,14 @@ class SellController extends AbstractController
|
||||||
$receiveRow->setPerson($person);
|
$receiveRow->setPerson($person);
|
||||||
$entityManager->persist($receiveRow);
|
$entityManager->persist($receiveRow);
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// $paymentDoc->setStatus('pending_approval');
|
||||||
|
// } else {
|
||||||
|
// $paymentDoc->setStatus('approved');
|
||||||
|
// }
|
||||||
$entityManager->persist($paymentDoc);
|
$entityManager->persist($paymentDoc);
|
||||||
}
|
}
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ use App\Service\Log;
|
||||||
use App\Service\PluginService;
|
use App\Service\PluginService;
|
||||||
use App\Service\registryMGR;
|
use App\Service\registryMGR;
|
||||||
use App\Service\SMS;
|
use App\Service\SMS;
|
||||||
|
use App\Entity\PlugWarrantySerial;
|
||||||
|
use App\Entity\ArchiveFile;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use ReflectionException;
|
use ReflectionException;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
@ -54,6 +56,85 @@ 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, \App\Service\FileStorage $storage): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('store');
|
||||||
|
if (!$acc) throw $this->createAccessDeniedException();
|
||||||
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
||||||
|
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
||||||
|
|
||||||
|
$file = $request->files->get('file');
|
||||||
|
if (!$file) {
|
||||||
|
return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400);
|
||||||
|
}
|
||||||
|
// 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($stored['relativePath']);
|
||||||
|
$archive->setCat('storeroom_ticket');
|
||||||
|
$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($stored['size'] !== null ? (string)$stored['size'] : null);
|
||||||
|
$entityManager->persist($archive);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
return $this->json(['result'=>0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/storeroom/ticket/attachments/{code}', name: 'app_storeroom_ticket_list_attachments', methods: ['GET'])]
|
||||||
|
public function listTicketAttachments(string $code, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('store');
|
||||||
|
if (!$acc) throw $this->createAccessDeniedException();
|
||||||
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy(['bid'=>$acc['bid'],'code'=>$code]);
|
||||||
|
if (!$ticket) throw $this->createNotFoundException('حواله یافت نشد');
|
||||||
|
$items = $entityManager->getRepository(ArchiveFile::class)->findBy([
|
||||||
|
'bid'=>$acc['bid'],
|
||||||
|
'relatedDocType'=>'storeroom_ticket',
|
||||||
|
'relatedDocCode'=>$ticket->getCode()
|
||||||
|
], ['id'=>'DESC']);
|
||||||
|
return $this->json(array_map(function(ArchiveFile $a){
|
||||||
|
return [
|
||||||
|
'id'=>$a->getId(),
|
||||||
|
'filename'=>$a->getFilename(),
|
||||||
|
'fileType'=>$a->getFileType(),
|
||||||
|
'fileSize'=>$a->getFileSize(),
|
||||||
|
'des'=>$a->getDes(),
|
||||||
|
'dateSubmit'=>$a->getDateSubmit(),
|
||||||
|
];
|
||||||
|
}, $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')]
|
#[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
|
public function app_storeroom_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
@ -134,8 +215,12 @@ class StoreroomController extends AbstractController
|
||||||
foreach ($buys as $buy) {
|
foreach ($buys as $buy) {
|
||||||
$temp = $provider->Entity2Array($buy, 0);
|
$temp = $provider->Entity2Array($buy, 0);
|
||||||
$person = $this->getPerson($buy);
|
$person = $this->getPerson($buy);
|
||||||
$temp['person'] = Explore::ExplorePerson($person);
|
if ($person) {
|
||||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
$temp['person'] = Explore::ExplorePerson($person);
|
||||||
|
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||||
|
} else {
|
||||||
|
$temp['person'] = null;
|
||||||
|
}
|
||||||
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
||||||
//check storeroom exist
|
//check storeroom exist
|
||||||
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
||||||
|
|
@ -155,8 +240,12 @@ class StoreroomController extends AbstractController
|
||||||
foreach ($sells as $sell) {
|
foreach ($sells as $sell) {
|
||||||
$temp = $provider->Entity2Array($sell, 0);
|
$temp = $provider->Entity2Array($sell, 0);
|
||||||
$person = $this->getPerson($sell);
|
$person = $this->getPerson($sell);
|
||||||
$temp['person'] = Explore::ExplorePerson($person);
|
if ($person) {
|
||||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
$temp['person'] = Explore::ExplorePerson($person);
|
||||||
|
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||||
|
} else {
|
||||||
|
$temp['person'] = null;
|
||||||
|
}
|
||||||
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
||||||
//check storeroom exist
|
//check storeroom exist
|
||||||
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
||||||
|
|
@ -176,8 +265,12 @@ class StoreroomController extends AbstractController
|
||||||
foreach ($rfsells as $sell) {
|
foreach ($rfsells as $sell) {
|
||||||
$temp = $provider->Entity2Array($sell, 0);
|
$temp = $provider->Entity2Array($sell, 0);
|
||||||
$person = $this->getPerson($sell);
|
$person = $this->getPerson($sell);
|
||||||
$temp['person'] = Explore::ExplorePerson($person);
|
if ($person) {
|
||||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
$temp['person'] = Explore::ExplorePerson($person);
|
||||||
|
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||||
|
} else {
|
||||||
|
$temp['person'] = null;
|
||||||
|
}
|
||||||
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
$temp['commodities'] = $this->getCommodities($sell, $provider);
|
||||||
//check storeroom exist
|
//check storeroom exist
|
||||||
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
$this->calcStoreRemaining($temp, $sell, $entityManager);
|
||||||
|
|
@ -197,8 +290,12 @@ class StoreroomController extends AbstractController
|
||||||
foreach ($rfbuys as $buy) {
|
foreach ($rfbuys as $buy) {
|
||||||
$temp = $provider->Entity2Array($buy, 0);
|
$temp = $provider->Entity2Array($buy, 0);
|
||||||
$person = $this->getPerson($buy);
|
$person = $this->getPerson($buy);
|
||||||
$temp['person'] = Explore::ExplorePerson($person);
|
if ($person) {
|
||||||
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
$temp['person'] = Explore::ExplorePerson($person);
|
||||||
|
$temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
||||||
|
} else {
|
||||||
|
$temp['person'] = null;
|
||||||
|
}
|
||||||
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
$temp['commodities'] = $this->getCommodities($buy, $provider);
|
||||||
//check storeroom exist
|
//check storeroom exist
|
||||||
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
$this->calcStoreRemaining($temp, $buy, $entityManager);
|
||||||
|
|
@ -281,8 +378,12 @@ class StoreroomController extends AbstractController
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$res = $provider->Entity2Array($doc, 0);
|
$res = $provider->Entity2Array($doc, 0);
|
||||||
$res['person'] = $provider->Entity2Array($person, 0);
|
if ($person) {
|
||||||
$res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename();
|
$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']);
|
$res['commodities'] = $provider->ArrayEntity2Array($commodities, 1, ['doc', 'bid', 'year']);
|
||||||
//calculate rows data
|
//calculate rows data
|
||||||
$this->calcStoreRemaining($res, $doc, $entityManager);
|
$this->calcStoreRemaining($res, $doc, $entityManager);
|
||||||
|
|
@ -403,6 +504,10 @@ class StoreroomController extends AbstractController
|
||||||
$ticket->setTransfer($params['ticket']['transfer']);
|
$ticket->setTransfer($params['ticket']['transfer']);
|
||||||
$ticket->setYear($acc['year']);
|
$ticket->setYear($acc['year']);
|
||||||
$ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom'));
|
$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->setReceiver($params['ticket']['receiver']);
|
||||||
$ticket->setTransferType($transferType);
|
$ticket->setTransferType($transferType);
|
||||||
$ticket->setReferral($params['ticket']['referral']);
|
$ticket->setReferral($params['ticket']['referral']);
|
||||||
|
|
@ -411,6 +516,9 @@ class StoreroomController extends AbstractController
|
||||||
$ticket->setType($params['ticket']['type']);
|
$ticket->setType($params['ticket']['type']);
|
||||||
$ticket->setTypeString($params['ticket']['typeString']);
|
$ticket->setTypeString($params['ticket']['typeString']);
|
||||||
$ticket->setDes($params['ticket']['des']);
|
$ticket->setDes($params['ticket']['des']);
|
||||||
|
if (array_key_exists('importWorkflowCode', $params['ticket'])) {
|
||||||
|
$ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']);
|
||||||
|
}
|
||||||
$entityManager->persist($ticket);
|
$entityManager->persist($ticket);
|
||||||
//$entityManager->flush();
|
//$entityManager->flush();
|
||||||
|
|
||||||
|
|
@ -418,6 +526,26 @@ class StoreroomController extends AbstractController
|
||||||
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
$docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([
|
||||||
'doc' => $doc
|
'doc' => $doc
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Determine if warranty serials are required based on flag or provided lines
|
||||||
|
$hasSerialLines = false;
|
||||||
|
foreach (($params['items'] ?? []) as $it) {
|
||||||
|
if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; }
|
||||||
|
}
|
||||||
|
$requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines;
|
||||||
|
if ($requireWarrantySerial) {
|
||||||
|
if (!$pluginService->isActive('warranty', $acc['bid'])) {
|
||||||
|
return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403);
|
||||||
|
}
|
||||||
|
// Validate counts up-front
|
||||||
|
foreach ($params['items'] as $item) {
|
||||||
|
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||||
|
if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) {
|
||||||
|
return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($params['items'] as $item) {
|
foreach ($params['items'] as $item) {
|
||||||
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
$row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([
|
||||||
'bid' => $acc['bid'],
|
'bid' => $acc['bid'],
|
||||||
|
|
@ -441,8 +569,58 @@ class StoreroomController extends AbstractController
|
||||||
$ticketItem->setCommodity($row->getCommodity());
|
$ticketItem->setCommodity($row->getCommodity());
|
||||||
$ticketItem->setType($item['type']);
|
$ticketItem->setType($item['type']);
|
||||||
$entityManager->persist($ticketItem);
|
$entityManager->persist($ticketItem);
|
||||||
|
|
||||||
|
// Bind warranty serials per item if provided
|
||||||
|
if ($requireWarrantySerial) {
|
||||||
|
$lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : [];
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$entityManager->flush();
|
$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
|
//save logs
|
||||||
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
$log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']);
|
||||||
if ($pluginService->isActive('accpro', $acc['bid'])) {
|
if ($pluginService->isActive('accpro', $acc['bid'])) {
|
||||||
|
|
@ -503,6 +681,58 @@ class StoreroomController extends AbstractController
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[Route('/api/storeroom/ticket/status/{code}', name: 'app_storeroom_ticket_status_update', methods: ['POST'])]
|
||||||
|
public function app_storeroom_ticket_status_update(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('store');
|
||||||
|
if (!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
$params = json_decode($request->getContent() ?: '{}', true);
|
||||||
|
$status = $params['status'] ?? null; // in_progress|done|rejected|approved|pending_approval
|
||||||
|
if (!in_array($status, ['in_progress','done','rejected','approved','pending_approval'])) {
|
||||||
|
return $this->json(['result' => -1, 'message' => 'وضعیت نامعتبر'], 400);
|
||||||
|
}
|
||||||
|
$ticket = $entityManager->getRepository(StoreroomTicket::class)->findOneBy([
|
||||||
|
'bid' => $acc['bid'],
|
||||||
|
'code' => $code
|
||||||
|
]);
|
||||||
|
if (!$ticket) {
|
||||||
|
throw $this->createNotFoundException('حواله یافت نشد.');
|
||||||
|
}
|
||||||
|
// $ticket->setStatus($status);
|
||||||
|
$entityManager->persist($ticket);
|
||||||
|
$entityManager->flush();
|
||||||
|
return $this->json(['result' => 0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/api/storeroom/tickets', name: 'app_storeroom_tickets_by_status', methods: ['GET'])]
|
||||||
|
public function app_storeroom_tickets_by_status(Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse
|
||||||
|
{
|
||||||
|
$acc = $access->hasRole('store');
|
||||||
|
if (!$acc)
|
||||||
|
throw $this->createAccessDeniedException();
|
||||||
|
$status = $request->query->get('status');
|
||||||
|
$criteria = [
|
||||||
|
'bid' => $acc['bid'],
|
||||||
|
'year' => $acc['year'],
|
||||||
|
];
|
||||||
|
if ($status) {
|
||||||
|
$criteria['status'] = $status;
|
||||||
|
}
|
||||||
|
$tickets = $entityManager->getRepository(StoreroomTicket::class)->findBy($criteria, ['date' => 'DESC']);
|
||||||
|
return $this->json(array_map(function(StoreroomTicket $t){
|
||||||
|
return [
|
||||||
|
'code' => $t->getCode(),
|
||||||
|
'date' => $t->getDate(),
|
||||||
|
'type' => $t->getType(),
|
||||||
|
'typeString' => $t->getTypeString(),
|
||||||
|
'importWorkflowCode' => $t->getImportWorkflowCode(),
|
||||||
|
'person' => $t->getPerson() ? $t->getPerson()->getNikename() : null,
|
||||||
|
'storeroom' => $t->getStoreroom() ? $t->getStoreroom()->getName() : null,
|
||||||
|
];
|
||||||
|
}, $tickets));
|
||||||
|
}
|
||||||
|
|
||||||
#[Route('/api/storeroom/tickets/list/{type}', name: 'app_storeroom_tickets_list')]
|
#[Route('/api/storeroom/tickets/list/{type}', name: 'app_storeroom_tickets_list')]
|
||||||
public function app_storeroom_tickets_list(string $type, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
|
public function app_storeroom_tickets_list(string $type, Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager): JsonResponse
|
||||||
{
|
{
|
||||||
|
|
@ -516,15 +746,33 @@ class StoreroomController extends AbstractController
|
||||||
], [
|
], [
|
||||||
'date' => 'DESC'
|
'date' => 'DESC'
|
||||||
]);
|
]);
|
||||||
return $this->json($provider->ArrayEntity2ArrayJustIncludes($tickets, [
|
$result = $provider->ArrayEntity2ArrayJustIncludes($tickets, [
|
||||||
'getDes',
|
'getDes',
|
||||||
'getCode',
|
'getCode',
|
||||||
'getDate',
|
'getDate',
|
||||||
'getPerson',
|
'getPerson',
|
||||||
'getNikename',
|
'getNikename',
|
||||||
'getDoc',
|
'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')]
|
#[Route('/api/storeroom/tickets/info/{code}', name: 'app_storeroom_ticket_view')]
|
||||||
|
|
@ -542,7 +790,7 @@ class StoreroomController extends AbstractController
|
||||||
//get items
|
//get items
|
||||||
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
$items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]);
|
||||||
$res = [];
|
$res = [];
|
||||||
$res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString'], 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['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0);
|
||||||
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
$res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0);
|
||||||
//get rows
|
//get rows
|
||||||
|
|
@ -659,6 +907,15 @@ class StoreroomController extends AbstractController
|
||||||
} else {
|
} else {
|
||||||
$title = 'حواله خروج از انبار';
|
$title = 'حواله خروج از انبار';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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);
|
||||||
|
}
|
||||||
|
}
|
||||||
$pdfPid = 0;
|
$pdfPid = 0;
|
||||||
$pdfPid = $provider->createPrint(
|
$pdfPid = $provider->createPrint(
|
||||||
$acc['bid'],
|
$acc['bid'],
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,9 @@ class Business
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
private ?string $cashdeskCode = '1000';
|
private ?string $cashdeskCode = '1000';
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::BIGINT, nullable: true)]
|
||||||
|
private ?string $importWorkflowCode = null;
|
||||||
|
|
||||||
#[ORM\OneToMany(mappedBy: 'bid', targetEntity: Salary::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'bid', targetEntity: Salary::class, orphanRemoval: true)]
|
||||||
private Collection $salaries;
|
private Collection $salaries;
|
||||||
|
|
||||||
|
|
@ -307,6 +310,21 @@ class Business
|
||||||
#[Ignore]
|
#[Ignore]
|
||||||
private Collection $plugWarrantySerials;
|
private Collection $plugWarrantySerials;
|
||||||
|
|
||||||
|
#[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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->logs = new ArrayCollection();
|
$this->logs = new ArrayCollection();
|
||||||
|
|
@ -352,6 +370,7 @@ class Business
|
||||||
$this->plugHrmDocs = new ArrayCollection();
|
$this->plugHrmDocs = new ArrayCollection();
|
||||||
$this->aiConversations = new ArrayCollection();
|
$this->aiConversations = new ArrayCollection();
|
||||||
$this->plugWarrantySerials = new ArrayCollection();
|
$this->plugWarrantySerials = new ArrayCollection();
|
||||||
|
$this->importWorkflows = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|
@ -879,6 +898,18 @@ class Business
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflowCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->importWorkflowCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflowCode(?string $importWorkflowCode): self
|
||||||
|
{
|
||||||
|
$this->importWorkflowCode = $importWorkflowCode;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return Collection<int, Salary>
|
* @return Collection<int, Salary>
|
||||||
*/
|
*/
|
||||||
|
|
@ -2155,4 +2186,72 @@ class Business
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflows(): Collection
|
||||||
|
{
|
||||||
|
return $this->importWorkflows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addImportWorkflow(ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
if (!$this->importWorkflows->contains($importWorkflow)) {
|
||||||
|
$this->importWorkflows->add($importWorkflow);
|
||||||
|
$importWorkflow->setBusiness($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeImportWorkflow(ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
if ($this->importWorkflows->removeElement($importWorkflow)) {
|
||||||
|
if ($importWorkflow->getBusiness() === $this) {
|
||||||
|
$importWorkflow->setBusiness(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
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;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
441
hesabixCore/src/Entity/ImportWorkflow.php
Normal file
441
hesabixCore/src/Entity/ImportWorkflow.php
Normal file
|
|
@ -0,0 +1,441 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowRepository;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowRepository::class)]
|
||||||
|
class ImportWorkflow
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, unique: true)]
|
||||||
|
private ?string $code = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $title = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'importWorkflows')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?Business $business = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'importWorkflows')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?User $submitter = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $dateMod = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $supplierName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $supplierCountry = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $supplierAddress = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $supplierPhone = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $supplierEmail = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $totalAmount = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $currency = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $exchangeRate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $totalAmountIRR = null;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowItem::class, orphanRemoval: true)]
|
||||||
|
private Collection $items;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowPayment::class, orphanRemoval: true)]
|
||||||
|
private Collection $payments;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowDocument::class, orphanRemoval: true)]
|
||||||
|
private Collection $documents;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowStage::class, orphanRemoval: true)]
|
||||||
|
private Collection $stages;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowShipping::class, orphanRemoval: true)]
|
||||||
|
private Collection $shipping;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'importWorkflow', targetEntity: ImportWorkflowCustoms::class, orphanRemoval: true)]
|
||||||
|
private Collection $customs;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->items = new ArrayCollection();
|
||||||
|
$this->payments = new ArrayCollection();
|
||||||
|
$this->documents = new ArrayCollection();
|
||||||
|
$this->stages = new ArrayCollection();
|
||||||
|
$this->shipping = new ArrayCollection();
|
||||||
|
$this->customs = new ArrayCollection();
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'draft';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCode(string $code): static
|
||||||
|
{
|
||||||
|
$this->code = $code;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): static
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBusiness(): ?Business
|
||||||
|
{
|
||||||
|
return $this->business;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBusiness(?Business $business): static
|
||||||
|
{
|
||||||
|
$this->business = $business;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSubmitter(): ?User
|
||||||
|
{
|
||||||
|
return $this->submitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSubmitter(?User $submitter): static
|
||||||
|
{
|
||||||
|
$this->submitter = $submitter;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateMod(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateMod(?string $dateMod): static
|
||||||
|
{
|
||||||
|
$this->dateMod = $dateMod;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierName(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierName(?string $supplierName): static
|
||||||
|
{
|
||||||
|
$this->supplierName = $supplierName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierCountry(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierCountry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierCountry(?string $supplierCountry): static
|
||||||
|
{
|
||||||
|
$this->supplierCountry = $supplierCountry;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierAddress(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierAddress(?string $supplierAddress): static
|
||||||
|
{
|
||||||
|
$this->supplierAddress = $supplierAddress;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierPhone(?string $supplierPhone): static
|
||||||
|
{
|
||||||
|
$this->supplierPhone = $supplierPhone;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSupplierEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->supplierEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSupplierEmail(?string $supplierEmail): static
|
||||||
|
{
|
||||||
|
$this->supplierEmail = $supplierEmail;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAmount(): ?string
|
||||||
|
{
|
||||||
|
return $this->totalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotalAmount(?string $totalAmount): static
|
||||||
|
{
|
||||||
|
$this->totalAmount = $totalAmount;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrency(): ?string
|
||||||
|
{
|
||||||
|
return $this->currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCurrency(?string $currency): static
|
||||||
|
{
|
||||||
|
$this->currency = $currency;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExchangeRate(): ?string
|
||||||
|
{
|
||||||
|
return $this->exchangeRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExchangeRate(?string $exchangeRate): static
|
||||||
|
{
|
||||||
|
$this->exchangeRate = $exchangeRate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalAmountIRR(): ?string
|
||||||
|
{
|
||||||
|
return $this->totalAmountIRR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotalAmountIRR(?string $totalAmountIRR): static
|
||||||
|
{
|
||||||
|
$this->totalAmountIRR = $totalAmountIRR;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getItems(): Collection
|
||||||
|
{
|
||||||
|
return $this->items;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addItem(ImportWorkflowItem $item): static
|
||||||
|
{
|
||||||
|
if (!$this->items->contains($item)) {
|
||||||
|
$this->items->add($item);
|
||||||
|
$item->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeItem(ImportWorkflowItem $item): static
|
||||||
|
{
|
||||||
|
if ($this->items->removeElement($item)) {
|
||||||
|
if ($item->getImportWorkflow() === $this) {
|
||||||
|
$item->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPayments(): Collection
|
||||||
|
{
|
||||||
|
return $this->payments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addPayment(ImportWorkflowPayment $payment): static
|
||||||
|
{
|
||||||
|
if (!$this->payments->contains($payment)) {
|
||||||
|
$this->payments->add($payment);
|
||||||
|
$payment->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removePayment(ImportWorkflowPayment $payment): static
|
||||||
|
{
|
||||||
|
if ($this->payments->removeElement($payment)) {
|
||||||
|
if ($payment->getImportWorkflow() === $this) {
|
||||||
|
$payment->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDocuments(): Collection
|
||||||
|
{
|
||||||
|
return $this->documents;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addDocument(ImportWorkflowDocument $document): static
|
||||||
|
{
|
||||||
|
if (!$this->documents->contains($document)) {
|
||||||
|
$this->documents->add($document);
|
||||||
|
$document->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeDocument(ImportWorkflowDocument $document): static
|
||||||
|
{
|
||||||
|
if ($this->documents->removeElement($document)) {
|
||||||
|
if ($document->getImportWorkflow() === $this) {
|
||||||
|
$document->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStages(): Collection
|
||||||
|
{
|
||||||
|
return $this->stages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addStage(ImportWorkflowStage $stage): static
|
||||||
|
{
|
||||||
|
if (!$this->stages->contains($stage)) {
|
||||||
|
$this->stages->add($stage);
|
||||||
|
$stage->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeStage(ImportWorkflowStage $stage): static
|
||||||
|
{
|
||||||
|
if ($this->stages->removeElement($stage)) {
|
||||||
|
if ($stage->getImportWorkflow() === $this) {
|
||||||
|
$stage->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShipping(): Collection
|
||||||
|
{
|
||||||
|
return $this->shipping;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addShipping(ImportWorkflowShipping $shipping): static
|
||||||
|
{
|
||||||
|
if (!$this->shipping->contains($shipping)) {
|
||||||
|
$this->shipping->add($shipping);
|
||||||
|
$shipping->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeShipping(ImportWorkflowShipping $shipping): static
|
||||||
|
{
|
||||||
|
if ($this->shipping->removeElement($shipping)) {
|
||||||
|
if ($shipping->getImportWorkflow() === $this) {
|
||||||
|
$shipping->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustoms(): Collection
|
||||||
|
{
|
||||||
|
return $this->customs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addCustom(ImportWorkflowCustoms $custom): static
|
||||||
|
{
|
||||||
|
if (!$this->customs->contains($custom)) {
|
||||||
|
$this->customs->add($custom);
|
||||||
|
$custom->setImportWorkflow($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeCustom(ImportWorkflowCustoms $custom): static
|
||||||
|
{
|
||||||
|
if ($this->customs->removeElement($custom)) {
|
||||||
|
if ($custom->getImportWorkflow() === $this) {
|
||||||
|
$custom->setImportWorkflow(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
254
hesabixCore/src/Entity/ImportWorkflowCustoms.php
Normal file
254
hesabixCore/src/Entity/ImportWorkflowCustoms.php
Normal file
|
|
@ -0,0 +1,254 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowCustomsRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowCustomsRepository::class)]
|
||||||
|
class ImportWorkflowCustoms
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'customs')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $declarationNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $customsCode = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $clearanceDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $customsDuty = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $valueAddedTax = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $otherCharges = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $totalCustomsCharges = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $customsBroker = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $customsBrokerPhone = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $customsBrokerEmail = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $warehouseNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $warehouseLocation = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDeclarationNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->declarationNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDeclarationNumber(?string $declarationNumber): static
|
||||||
|
{
|
||||||
|
$this->declarationNumber = $declarationNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomsCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->customsCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomsCode(?string $customsCode): static
|
||||||
|
{
|
||||||
|
$this->customsCode = $customsCode;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClearanceDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->clearanceDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setClearanceDate(?string $clearanceDate): static
|
||||||
|
{
|
||||||
|
$this->clearanceDate = $clearanceDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomsDuty(): ?string
|
||||||
|
{
|
||||||
|
return $this->customsDuty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomsDuty(?string $customsDuty): static
|
||||||
|
{
|
||||||
|
$this->customsDuty = $customsDuty;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getValueAddedTax(): ?string
|
||||||
|
{
|
||||||
|
return $this->valueAddedTax;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setValueAddedTax(?string $valueAddedTax): static
|
||||||
|
{
|
||||||
|
$this->valueAddedTax = $valueAddedTax;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOtherCharges(): ?string
|
||||||
|
{
|
||||||
|
return $this->otherCharges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOtherCharges(?string $otherCharges): static
|
||||||
|
{
|
||||||
|
$this->otherCharges = $otherCharges;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalCustomsCharges(): ?string
|
||||||
|
{
|
||||||
|
return $this->totalCustomsCharges;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotalCustomsCharges(?string $totalCustomsCharges): static
|
||||||
|
{
|
||||||
|
$this->totalCustomsCharges = $totalCustomsCharges;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomsBroker(): ?string
|
||||||
|
{
|
||||||
|
return $this->customsBroker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomsBroker(?string $customsBroker): static
|
||||||
|
{
|
||||||
|
$this->customsBroker = $customsBroker;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomsBrokerPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->customsBrokerPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomsBrokerPhone(?string $customsBrokerPhone): static
|
||||||
|
{
|
||||||
|
$this->customsBrokerPhone = $customsBrokerPhone;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCustomsBrokerEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->customsBrokerEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCustomsBrokerEmail(?string $customsBrokerEmail): static
|
||||||
|
{
|
||||||
|
$this->customsBrokerEmail = $customsBrokerEmail;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarehouseNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->warehouseNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarehouseNumber(?string $warehouseNumber): static
|
||||||
|
{
|
||||||
|
$this->warehouseNumber = $warehouseNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWarehouseLocation(): ?string
|
||||||
|
{
|
||||||
|
return $this->warehouseLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarehouseLocation(?string $warehouseLocation): static
|
||||||
|
{
|
||||||
|
$this->warehouseLocation = $warehouseLocation;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
213
hesabixCore/src/Entity/ImportWorkflowDocument.php
Normal file
213
hesabixCore/src/Entity/ImportWorkflowDocument.php
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowDocumentRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowDocumentRepository::class)]
|
||||||
|
class ImportWorkflowDocument
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'documents')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $type = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $title = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $filePath = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $fileName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $fileSize = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $fileType = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $documentNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $issueDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $expiryDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTitle(): ?string
|
||||||
|
{
|
||||||
|
return $this->title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTitle(string $title): static
|
||||||
|
{
|
||||||
|
$this->title = $title;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFilePath(): ?string
|
||||||
|
{
|
||||||
|
return $this->filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFilePath(?string $filePath): static
|
||||||
|
{
|
||||||
|
$this->filePath = $filePath;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFileName(): ?string
|
||||||
|
{
|
||||||
|
return $this->fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFileName(?string $fileName): static
|
||||||
|
{
|
||||||
|
$this->fileName = $fileName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFileSize(): ?string
|
||||||
|
{
|
||||||
|
return $this->fileSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFileSize(?string $fileSize): static
|
||||||
|
{
|
||||||
|
$this->fileSize = $fileSize;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFileType(): ?string
|
||||||
|
{
|
||||||
|
return $this->fileType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFileType(?string $fileType): static
|
||||||
|
{
|
||||||
|
$this->fileType = $fileType;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDocumentNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->documentNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDocumentNumber(?string $documentNumber): static
|
||||||
|
{
|
||||||
|
$this->documentNumber = $documentNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIssueDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->issueDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setIssueDate(?string $issueDate): static
|
||||||
|
{
|
||||||
|
$this->issueDate = $issueDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getExpiryDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->expiryDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setExpiryDate(?string $expiryDate): static
|
||||||
|
{
|
||||||
|
$this->expiryDate = $expiryDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
270
hesabixCore/src/Entity/ImportWorkflowItem.php
Normal file
270
hesabixCore/src/Entity/ImportWorkflowItem.php
Normal file
|
|
@ -0,0 +1,270 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'items')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Commodity $commodity = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $name = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $productCode = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $brand = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $model = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $originCountry = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $quantity = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $unitPrice = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $unitPriceIRR = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $totalPrice = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $totalPriceIRR = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $weight = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $volume = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $specifications = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setName(string $name): static
|
||||||
|
{
|
||||||
|
$this->name = $name;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getProductCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->productCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProductCode(?string $productCode): static
|
||||||
|
{
|
||||||
|
$this->productCode = $productCode;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBrand(): ?string
|
||||||
|
{
|
||||||
|
return $this->brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBrand(?string $brand): static
|
||||||
|
{
|
||||||
|
$this->brand = $brand;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getModel(): ?string
|
||||||
|
{
|
||||||
|
return $this->model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setModel(?string $model): static
|
||||||
|
{
|
||||||
|
$this->model = $model;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginCountry(): ?string
|
||||||
|
{
|
||||||
|
return $this->originCountry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOriginCountry(?string $originCountry): static
|
||||||
|
{
|
||||||
|
$this->originCountry = $originCountry;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQuantity(): ?string
|
||||||
|
{
|
||||||
|
return $this->quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setQuantity(string $quantity): static
|
||||||
|
{
|
||||||
|
$this->quantity = $quantity;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnitPrice(): ?string
|
||||||
|
{
|
||||||
|
return $this->unitPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUnitPrice(?string $unitPrice): static
|
||||||
|
{
|
||||||
|
$this->unitPrice = $unitPrice;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnitPriceIRR(): ?string
|
||||||
|
{
|
||||||
|
return $this->unitPriceIRR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUnitPriceIRR(?string $unitPriceIRR): static
|
||||||
|
{
|
||||||
|
$this->unitPriceIRR = $unitPriceIRR;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalPrice(): ?string
|
||||||
|
{
|
||||||
|
return $this->totalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotalPrice(?string $totalPrice): static
|
||||||
|
{
|
||||||
|
$this->totalPrice = $totalPrice;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTotalPriceIRR(): ?string
|
||||||
|
{
|
||||||
|
return $this->totalPriceIRR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setTotalPriceIRR(?string $totalPriceIRR): static
|
||||||
|
{
|
||||||
|
$this->totalPriceIRR = $totalPriceIRR;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getWeight(): ?string
|
||||||
|
{
|
||||||
|
return $this->weight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWeight(?string $weight): static
|
||||||
|
{
|
||||||
|
$this->weight = $weight;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVolume(): ?string
|
||||||
|
{
|
||||||
|
return $this->volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVolume(?string $volume): static
|
||||||
|
{
|
||||||
|
$this->volume = $volume;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSpecifications(): ?string
|
||||||
|
{
|
||||||
|
return $this->specifications;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSpecifications(?string $specifications): static
|
||||||
|
{
|
||||||
|
$this->specifications = $specifications;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
227
hesabixCore/src/Entity/ImportWorkflowPayment.php
Normal file
227
hesabixCore/src/Entity/ImportWorkflowPayment.php
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowPaymentRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowPaymentRepository::class)]
|
||||||
|
class ImportWorkflowPayment
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'payments')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $type = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $amount = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $currency = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $amountIRR = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $paymentDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $referenceNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $bankName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $accountNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $recipientName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $receiptNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAmount(): ?string
|
||||||
|
{
|
||||||
|
return $this->amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAmount(string $amount): static
|
||||||
|
{
|
||||||
|
$this->amount = $amount;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrency(): ?string
|
||||||
|
{
|
||||||
|
return $this->currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setCurrency(?string $currency): static
|
||||||
|
{
|
||||||
|
$this->currency = $currency;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAmountIRR(): ?string
|
||||||
|
{
|
||||||
|
return $this->amountIRR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAmountIRR(?string $amountIRR): static
|
||||||
|
{
|
||||||
|
$this->amountIRR = $amountIRR;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPaymentDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->paymentDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setPaymentDate(string $paymentDate): static
|
||||||
|
{
|
||||||
|
$this->paymentDate = $paymentDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReferenceNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->referenceNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReferenceNumber(?string $referenceNumber): static
|
||||||
|
{
|
||||||
|
$this->referenceNumber = $referenceNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBankName(): ?string
|
||||||
|
{
|
||||||
|
return $this->bankName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBankName(?string $bankName): static
|
||||||
|
{
|
||||||
|
$this->bankName = $bankName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAccountNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->accountNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAccountNumber(?string $accountNumber): static
|
||||||
|
{
|
||||||
|
$this->accountNumber = $accountNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getRecipientName(): ?string
|
||||||
|
{
|
||||||
|
return $this->recipientName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setRecipientName(?string $recipientName): static
|
||||||
|
{
|
||||||
|
$this->recipientName = $recipientName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getReceiptNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->receiptNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setReceiptNumber(?string $receiptNumber): static
|
||||||
|
{
|
||||||
|
$this->receiptNumber = $receiptNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
269
hesabixCore/src/Entity/ImportWorkflowShipping.php
Normal file
269
hesabixCore/src/Entity/ImportWorkflowShipping.php
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowShippingRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowShippingRepository::class)]
|
||||||
|
class ImportWorkflowShipping
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'shipping')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $type = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $containerNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $billOfLading = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $shippingDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $arrivalDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $unloadingDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $shippingCompany = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $shippingCompanyPhone = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $shippingCompanyEmail = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $originPort = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $destinationPort = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $vesselName = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $voyageNumber = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setType(string $type): static
|
||||||
|
{
|
||||||
|
$this->type = $type;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getContainerNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->containerNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setContainerNumber(?string $containerNumber): static
|
||||||
|
{
|
||||||
|
$this->containerNumber = $containerNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBillOfLading(): ?string
|
||||||
|
{
|
||||||
|
return $this->billOfLading;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setBillOfLading(?string $billOfLading): static
|
||||||
|
{
|
||||||
|
$this->billOfLading = $billOfLading;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShippingDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->shippingDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShippingDate(?string $shippingDate): static
|
||||||
|
{
|
||||||
|
$this->shippingDate = $shippingDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getArrivalDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->arrivalDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setArrivalDate(?string $arrivalDate): static
|
||||||
|
{
|
||||||
|
$this->arrivalDate = $arrivalDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnloadingDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->unloadingDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setUnloadingDate(?string $unloadingDate): static
|
||||||
|
{
|
||||||
|
$this->unloadingDate = $unloadingDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShippingCompany(): ?string
|
||||||
|
{
|
||||||
|
return $this->shippingCompany;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShippingCompany(?string $shippingCompany): static
|
||||||
|
{
|
||||||
|
$this->shippingCompany = $shippingCompany;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShippingCompanyPhone(): ?string
|
||||||
|
{
|
||||||
|
return $this->shippingCompanyPhone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShippingCompanyPhone(?string $shippingCompanyPhone): static
|
||||||
|
{
|
||||||
|
$this->shippingCompanyPhone = $shippingCompanyPhone;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getShippingCompanyEmail(): ?string
|
||||||
|
{
|
||||||
|
return $this->shippingCompanyEmail;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setShippingCompanyEmail(?string $shippingCompanyEmail): static
|
||||||
|
{
|
||||||
|
$this->shippingCompanyEmail = $shippingCompanyEmail;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getOriginPort(): ?string
|
||||||
|
{
|
||||||
|
return $this->originPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setOriginPort(?string $originPort): static
|
||||||
|
{
|
||||||
|
$this->originPort = $originPort;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDestinationPort(): ?string
|
||||||
|
{
|
||||||
|
return $this->destinationPort;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDestinationPort(?string $destinationPort): static
|
||||||
|
{
|
||||||
|
$this->destinationPort = $destinationPort;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVesselName(): ?string
|
||||||
|
{
|
||||||
|
return $this->vesselName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVesselName(?string $vesselName): static
|
||||||
|
{
|
||||||
|
$this->vesselName = $vesselName;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVoyageNumber(): ?string
|
||||||
|
{
|
||||||
|
return $this->voyageNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVoyageNumber(?string $voyageNumber): static
|
||||||
|
{
|
||||||
|
$this->voyageNumber = $voyageNumber;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(?string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
157
hesabixCore/src/Entity/ImportWorkflowStage.php
Normal file
157
hesabixCore/src/Entity/ImportWorkflowStage.php
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\ImportWorkflowStageRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Annotation\Ignore;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: ImportWorkflowStageRepository::class)]
|
||||||
|
class ImportWorkflowStage
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(inversedBy: 'stages')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
#[Ignore]
|
||||||
|
private ?ImportWorkflow $importWorkflow = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $stage = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $status = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $startDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $endDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
private ?string $description = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $assignedTo = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
private ?string $dateSubmit = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255, nullable: true)]
|
||||||
|
private ?string $notes = null;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->dateSubmit = date('Y-m-d H:i:s');
|
||||||
|
$this->status = 'pending';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflow(): ?ImportWorkflow
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStage(): ?string
|
||||||
|
{
|
||||||
|
return $this->stage;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStage(string $stage): static
|
||||||
|
{
|
||||||
|
$this->stage = $stage;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStatus(): ?string
|
||||||
|
{
|
||||||
|
return $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStatus(string $status): static
|
||||||
|
{
|
||||||
|
$this->status = $status;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStartDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->startDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStartDate(?string $startDate): static
|
||||||
|
{
|
||||||
|
$this->startDate = $startDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getEndDate(): ?string
|
||||||
|
{
|
||||||
|
return $this->endDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setEndDate(?string $endDate): static
|
||||||
|
{
|
||||||
|
$this->endDate = $endDate;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDescription(): ?string
|
||||||
|
{
|
||||||
|
return $this->description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDescription(?string $description): static
|
||||||
|
{
|
||||||
|
$this->description = $description;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getAssignedTo(): ?string
|
||||||
|
{
|
||||||
|
return $this->assignedTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setAssignedTo(?string $assignedTo): static
|
||||||
|
{
|
||||||
|
$this->assignedTo = $assignedTo;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateSubmit(): ?string
|
||||||
|
{
|
||||||
|
return $this->dateSubmit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setDateSubmit(string $dateSubmit): static
|
||||||
|
{
|
||||||
|
$this->dateSubmit = $dateSubmit;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getNotes(): ?string
|
||||||
|
{
|
||||||
|
return $this->notes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setNotes(?string $notes): static
|
||||||
|
{
|
||||||
|
$this->notes = $notes;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -141,6 +141,12 @@ class Permission
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $ai = null;
|
private ?bool $ai = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $warehouseManager = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?bool $importWorkflow = null;
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
|
@ -649,4 +655,28 @@ class Permission
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public function isWarehouseManager(): ?bool
|
||||||
|
{
|
||||||
|
return $this->warehouseManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setWarehouseManager(?bool $warehouseManager): static
|
||||||
|
{
|
||||||
|
$this->warehouseManager = $warehouseManager;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isImportWorkflow(): ?bool
|
||||||
|
{
|
||||||
|
return $this->importWorkflow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflow(?bool $importWorkflow): static
|
||||||
|
{
|
||||||
|
$this->importWorkflow = $importWorkflow;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -161,6 +161,8 @@ class Person
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
private ?string $tags = null;
|
private ?string $tags = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->hesabdariRows = new ArrayCollection();
|
$this->hesabdariRows = new ArrayCollection();
|
||||||
|
|
@ -913,4 +915,6 @@ class Person
|
||||||
$this->tags = $tags;
|
$this->tags = $tags;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,170 +2,177 @@
|
||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
use App\Repository\PlugWarrantySerialRepository;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
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
|
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\Id]
|
||||||
#[ORM\GeneratedValue]
|
#[ORM\GeneratedValue]
|
||||||
#[ORM\Column]
|
#[ORM\Column(type: 'integer')]
|
||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
#[Ignore]
|
private ?Business $business = null;
|
||||||
private ?Business $bid = null;
|
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
||||||
#[ORM\JoinColumn(nullable: false)]
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
#[Ignore]
|
|
||||||
private ?Commodity $commodity = null;
|
private ?Commodity $commodity = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, unique: true)]
|
#[ORM\Column(name: 'serial_number', length: 255, unique: true)]
|
||||||
private ?string $serialNumber = null;
|
private ?string $serialNumber = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 25)]
|
#[ORM\Column(type: 'datetime_immutable')]
|
||||||
private ?string $dateSubmit = null;
|
private \DateTimeImmutable $dateSubmit;
|
||||||
|
|
||||||
#[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')]
|
#[ORM\ManyToOne]
|
||||||
#[Ignore]
|
|
||||||
private ?User $submitter = null;
|
private ?User $submitter = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 255, nullable: true)]
|
#[ORM\Column(type: 'text', nullable: true)]
|
||||||
private ?string $description = null;
|
private ?string $description = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 25, nullable: true)]
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||||
private ?string $warrantyStartDate = null;
|
private ?\DateTimeImmutable $warrantyStartDate = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 25, nullable: true)]
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||||
private ?string $warrantyEndDate = null;
|
private ?\DateTimeImmutable $warrantyEndDate = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 50, nullable: true)]
|
#[ORM\Column(length: 20)]
|
||||||
private ?string $status = 'active';
|
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;
|
private ?string $notes = null;
|
||||||
|
|
||||||
|
#[ORM\Column(name: 'allocated_to_document_id', type: 'integer', nullable: true)]
|
||||||
|
private ?int $allocatedToDocumentId = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: 'datetime_immutable', nullable: true)]
|
||||||
|
private ?\DateTimeImmutable $allocatedAt = 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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->dateSubmit = date('Y-m-d H:i:s');
|
$this->dateSubmit = new \DateTimeImmutable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int { return $this->id; }
|
||||||
{
|
|
||||||
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
|
public function setUsed(bool $used): self {
|
||||||
{
|
$this->status = $used ? self::STATUS_CONSUMED : self::STATUS_AVAILABLE;
|
||||||
return $this->bid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setBid(?Business $bid): static
|
|
||||||
{
|
|
||||||
$this->bid = $bid;
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getCommodity(): ?Commodity
|
public function setUsedAt(string $usedAt): self {
|
||||||
{
|
$this->usedAt = new \DateTimeImmutable($usedAt);
|
||||||
return $this->commodity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setCommodity(?Commodity $commodity): static
|
|
||||||
{
|
|
||||||
$this->commodity = $commodity;
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSerialNumber(): ?string
|
public function setUsedTicketCode(string $ticketCode): self {
|
||||||
{
|
$this->usedTicketCode = $ticketCode;
|
||||||
return $this->serialNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setSerialNumber(string $serialNumber): static
|
|
||||||
{
|
|
||||||
$this->serialNumber = $serialNumber;
|
|
||||||
return $this;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -77,6 +77,25 @@ class StoreroomTicket
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
private ?bool $canShare = null;
|
private ?bool $canShare = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#[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()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->storeroomItems = new ArrayCollection();
|
$this->storeroomItems = new ArrayCollection();
|
||||||
|
|
@ -332,4 +351,62 @@ class StoreroomTicket
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function getImportWorkflowCode(): ?string
|
||||||
|
{
|
||||||
|
return $this->importWorkflowCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setImportWorkflowCode(?string $importWorkflowCode): static
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
#[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)]
|
#[ORM\OneToMany(mappedBy: 'user', targetEntity: AIConversation::class, orphanRemoval: true)]
|
||||||
private Collection $aiConversations;
|
private Collection $aiConversations;
|
||||||
|
|
||||||
|
#[ORM\OneToMany(mappedBy: 'submitter', targetEntity: ImportWorkflow::class, orphanRemoval: true)]
|
||||||
|
private Collection $importWorkflows;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->userTokens = new ArrayCollection();
|
$this->userTokens = new ArrayCollection();
|
||||||
|
|
@ -171,6 +174,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
$this->backBuiltModules = new ArrayCollection();
|
$this->backBuiltModules = new ArrayCollection();
|
||||||
$this->PlugGhestaDocs = new ArrayCollection();
|
$this->PlugGhestaDocs = new ArrayCollection();
|
||||||
$this->aiConversations = new ArrayCollection();
|
$this->aiConversations = new ArrayCollection();
|
||||||
|
$this->importWorkflows = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
|
|
@ -1066,4 +1070,28 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getImportWorkflows(): Collection
|
||||||
|
{
|
||||||
|
return $this->importWorkflows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addImportWorkflow(ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
if (!$this->importWorkflows->contains($importWorkflow)) {
|
||||||
|
$this->importWorkflows->add($importWorkflow);
|
||||||
|
$importWorkflow->setSubmitter($this);
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeImportWorkflow(ImportWorkflow $importWorkflow): static
|
||||||
|
{
|
||||||
|
if ($this->importWorkflows->removeElement($importWorkflow)) {
|
||||||
|
if ($importWorkflow->getSubmitter() === $this) {
|
||||||
|
$importWorkflow->setSubmitter(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -142,8 +142,11 @@ class CommodityRepository extends ServiceEntityRepository
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->setMaxResults($params['Take'])
|
if (isset($params['Take']) && $params['Take'] !== -1) {
|
||||||
->orderBy('p.id', 'ASC');
|
$query->setMaxResults($params['Take']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->orderBy('p.id', 'ASC');
|
||||||
|
|
||||||
return $query->getQuery()->getResult();
|
return $query->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowCustoms;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowCustomsRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowCustoms::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwc')
|
||||||
|
->andWhere('iwc.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwc.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByDeclarationNumber($declarationNumber, $importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwc')
|
||||||
|
->andWhere('iwc.declarationNumber = :declarationNumber')
|
||||||
|
->andWhere('iwc.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('declarationNumber', $declarationNumber)
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowDocument;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowDocumentRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowDocument::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwd')
|
||||||
|
->andWhere('iwd.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwd.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByType($type, $importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwd')
|
||||||
|
->andWhere('iwd.type = :type')
|
||||||
|
->andWhere('iwd.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('type', $type)
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwd.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
25
hesabixCore/src/Repository/ImportWorkflowItemRepository.php
Normal file
25
hesabixCore/src/Repository/ImportWorkflowItemRepository.php
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowItem;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowItemRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowItem::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwi')
|
||||||
|
->andWhere('iwi.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwi.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowPayment;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowPaymentRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowPayment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwp')
|
||||||
|
->andWhere('iwp.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwp.paymentDate', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByType($type, $importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iwp')
|
||||||
|
->andWhere('iwp.type = :type')
|
||||||
|
->andWhere('iwp.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('type', $type)
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iwp.paymentDate', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
66
hesabixCore/src/Repository/ImportWorkflowRepository.php
Normal file
66
hesabixCore/src/Repository/ImportWorkflowRepository.php
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflow;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflow::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByBusiness($businessId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iw')
|
||||||
|
->andWhere('iw.business = :businessId')
|
||||||
|
->setParameter('businessId', $businessId)
|
||||||
|
->orderBy('iw.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByStatus($status, $businessId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iw')
|
||||||
|
->andWhere('iw.status = :status')
|
||||||
|
->andWhere('iw.business = :businessId')
|
||||||
|
->setParameter('status', $status)
|
||||||
|
->setParameter('businessId', $businessId)
|
||||||
|
->orderBy('iw.dateSubmit', 'DESC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByCode($code, $businessId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iw')
|
||||||
|
->andWhere('iw.code = :code')
|
||||||
|
->andWhere('iw.business = :businessId')
|
||||||
|
->setParameter('code', $code)
|
||||||
|
->setParameter('businessId', $businessId)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findWithDetails($id, $businessId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iw')
|
||||||
|
->leftJoin('iw.items', 'items')
|
||||||
|
->leftJoin('iw.payments', 'payments')
|
||||||
|
->leftJoin('iw.documents', 'documents')
|
||||||
|
->leftJoin('iw.stages', 'stages')
|
||||||
|
->leftJoin('iw.shipping', 'shipping')
|
||||||
|
->leftJoin('iw.customs', 'customs')
|
||||||
|
->addSelect('items', 'payments', 'documents', 'stages', 'shipping', 'customs')
|
||||||
|
->andWhere('iw.id = :id')
|
||||||
|
->andWhere('iw.business = :businessId')
|
||||||
|
->setParameter('id', $id)
|
||||||
|
->setParameter('businessId', $businessId)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowShipping;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowShippingRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowShipping::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iws')
|
||||||
|
->andWhere('iws.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iws.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByType($type, $importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iws')
|
||||||
|
->andWhere('iws.type = :type')
|
||||||
|
->andWhere('iws.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('type', $type)
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
36
hesabixCore/src/Repository/ImportWorkflowStageRepository.php
Normal file
36
hesabixCore/src/Repository/ImportWorkflowStageRepository.php
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\ImportWorkflowStage;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class ImportWorkflowStageRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, ImportWorkflowStage::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByImportWorkflow($importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iws')
|
||||||
|
->andWhere('iws.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->orderBy('iws.dateSubmit', 'ASC')
|
||||||
|
->getQuery()
|
||||||
|
->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findByStage($stage, $importWorkflowId)
|
||||||
|
{
|
||||||
|
return $this->createQueryBuilder('iws')
|
||||||
|
->andWhere('iws.stage = :stage')
|
||||||
|
->andWhere('iws.importWorkflow = :importWorkflowId')
|
||||||
|
->setParameter('stage', $stage)
|
||||||
|
->setParameter('importWorkflowId', $importWorkflowId)
|
||||||
|
->getQuery()
|
||||||
|
->getOneOrNullResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,8 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
public function findByBusiness($bid): array
|
public function findByBusiness($bid): array
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('p')
|
return $this->createQueryBuilder('p')
|
||||||
->andWhere('p.bid = :val')
|
->join('p.business', 'b')
|
||||||
|
->andWhere('b.id = :val')
|
||||||
->setParameter('val', $bid)
|
->setParameter('val', $bid)
|
||||||
->orderBy('p.dateSubmit', 'DESC')
|
->orderBy('p.dateSubmit', 'DESC')
|
||||||
->getQuery()
|
->getQuery()
|
||||||
|
|
@ -36,7 +37,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
public function findByCommodity($bid, $commodityId): array
|
public function findByCommodity($bid, $commodityId): array
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('p')
|
return $this->createQueryBuilder('p')
|
||||||
->andWhere('p.bid = :bid')
|
->andWhere('p.business = :bid')
|
||||||
->andWhere('p.commodity = :commodityId')
|
->andWhere('p.commodity = :commodityId')
|
||||||
->setParameter('bid', $bid)
|
->setParameter('bid', $bid)
|
||||||
->setParameter('commodityId', $commodityId)
|
->setParameter('commodityId', $commodityId)
|
||||||
|
|
@ -52,7 +53,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
public function findByStatus($bid, $status): array
|
public function findByStatus($bid, $status): array
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('p')
|
return $this->createQueryBuilder('p')
|
||||||
->andWhere('p.bid = :bid')
|
->andWhere('p.business = :bid')
|
||||||
->andWhere('p.status = :status')
|
->andWhere('p.status = :status')
|
||||||
->setParameter('bid', $bid)
|
->setParameter('bid', $bid)
|
||||||
->setParameter('status', $status)
|
->setParameter('status', $status)
|
||||||
|
|
@ -72,7 +73,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
->setParameter('serialNumber', $serialNumber);
|
->setParameter('serialNumber', $serialNumber);
|
||||||
|
|
||||||
if ($bid) {
|
if ($bid) {
|
||||||
$qb->andWhere('p.bid = :bid')
|
$qb->andWhere('p.business = :bid')
|
||||||
->setParameter('bid', $bid);
|
->setParameter('bid', $bid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +87,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('p')
|
return $this->createQueryBuilder('p')
|
||||||
->leftJoin('p.commodity', 'c')
|
->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')
|
->andWhere('p.serialNumber LIKE :keyword OR c.name LIKE :keyword OR p.description LIKE :keyword')
|
||||||
->setParameter('bid', $bid)
|
->setParameter('bid', $bid)
|
||||||
->setParameter('keyword', '%' . $keyword . '%')
|
->setParameter('keyword', '%' . $keyword . '%')
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,10 @@ class Access
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
elseif($this->request->headers->get('api-key')){
|
elseif($this->request->headers->get('api-key') || $this->request->headers->get('X-AUTH-TOKEN')){
|
||||||
|
$rawToken = $this->request->headers->get('api-key') ?: $this->request->headers->get('X-AUTH-TOKEN');
|
||||||
$token = $this->em->getRepository(APIToken::class)->findOneBy([
|
$token = $this->em->getRepository(APIToken::class)->findOneBy([
|
||||||
'token'=>$this->request->headers->get('api-key')
|
'token'=> $rawToken
|
||||||
]);
|
]);
|
||||||
if(!$token) { return false; }
|
if(!$token) { return false; }
|
||||||
|
|
||||||
|
|
@ -68,6 +69,10 @@ class Access
|
||||||
|
|
||||||
$bid = $token->getBid();
|
$bid = $token->getBid();
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
// بدون BID فعال یا توکن معتبر
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if ($this->request->headers->get('activeYear')) {
|
if ($this->request->headers->get('activeYear')) {
|
||||||
$year = $this->em->getRepository(Year::class)->findOneBy([
|
$year = $this->em->getRepository(Year::class)->findOneBy([
|
||||||
'id' => $this->request->headers->get('activeYear'),
|
'id' => $this->request->headers->get('activeYear'),
|
||||||
|
|
@ -75,7 +80,7 @@ class Access
|
||||||
]);
|
]);
|
||||||
if (!$year) { return false; }
|
if (!$year) { return false; }
|
||||||
}
|
}
|
||||||
elseif($this->request->headers->get('api-key')){
|
elseif($this->request->headers->get('api-key') || $this->request->headers->get('X-AUTH-TOKEN')){
|
||||||
$year = $this->em->getRepository(Year::class)->findOneBy([
|
$year = $this->em->getRepository(Year::class)->findOneBy([
|
||||||
'head' => true,
|
'head' => true,
|
||||||
'bid'=>$bid
|
'bid'=>$bid
|
||||||
|
|
@ -126,6 +131,19 @@ class Access
|
||||||
elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){
|
elseif ($this->user && $roll == 'join' && count($this->em->getRepository(Permission::class)->findBy(['user'=>$this->user,'bid'=>$bid]))){
|
||||||
return $accessArray;
|
return $accessArray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$warehousePermission = $this->em->getRepository(Permission::class)->findOneBy([
|
||||||
|
'bid'=>$bid,
|
||||||
|
'user'=>$this->user
|
||||||
|
]);
|
||||||
|
|
||||||
|
if($warehousePermission && $warehousePermission->isWarehouseManager()){
|
||||||
|
$warehouseRoles = ['commodity', 'store', 'plugWarrantyManager'];
|
||||||
|
if(in_array($roll, $warehouseRoles)){
|
||||||
|
return $accessArray;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$methodName = 'is' . ucfirst($roll);
|
$methodName = 'is' . ucfirst($roll);
|
||||||
$permission = $this->em->getRepository(Permission::class)->findOneBy([
|
$permission = $this->em->getRepository(Permission::class)->findOneBy([
|
||||||
'bid'=>$bid,
|
'bid'=>$bid,
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,10 @@ class Explore
|
||||||
'amount' => $doc->getAmount(),
|
'amount' => $doc->getAmount(),
|
||||||
'mdate' => '',
|
'mdate' => '',
|
||||||
'plugin' => $doc->getPlugin(),
|
'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);
|
$row->setDiscount(0);
|
||||||
$temp['tax'] = $row->getTax();
|
$temp['tax'] = $row->getTax();
|
||||||
$temp['discount'] = $row->getDiscount();
|
$temp['discount'] = $row->getDiscount();
|
||||||
|
|
||||||
|
// Approval fields - از سند اصلی گرفته میشود
|
||||||
|
|
||||||
return $temp;
|
return $temp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -347,6 +354,7 @@ class Explore
|
||||||
'address' => $person->getAddress(),
|
'address' => $person->getAddress(),
|
||||||
'prelabel' => null,
|
'prelabel' => null,
|
||||||
'tags' => $person->getTags(),
|
'tags' => $person->getTags(),
|
||||||
|
'requireTwoStep' => $person->getBid() ? $person->getBid()->isRequireTwoStepApproval() : false,
|
||||||
];
|
];
|
||||||
if ($person->getPrelabel()) {
|
if ($person->getPrelabel()) {
|
||||||
$res['prelabel'] = $person->getPrelabel()->getLabel();
|
$res['prelabel'] = $person->getPrelabel()->getLabel();
|
||||||
|
|
@ -570,6 +578,10 @@ class Explore
|
||||||
'shortlinks' => $item->isShortlinks(),
|
'shortlinks' => $item->isShortlinks(),
|
||||||
'walletEnabled' => $item->isWalletEnable(),
|
'walletEnabled' => $item->isWalletEnable(),
|
||||||
'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null,
|
'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null,
|
||||||
|
'requireTwoStepApproval' => $item->isRequireTwoStepApproval(),
|
||||||
|
'invoiceApprover' => $item->getInvoiceApprover(),
|
||||||
|
'warehouseApprover' => $item->getWarehouseApprover(),
|
||||||
|
'financialApprover' => $item->getFinancialApprover(),
|
||||||
'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(),
|
'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(),
|
||||||
'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(),
|
'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",
|
"maz-ui": "^3.50.1",
|
||||||
"monaco-editor": "^0.52.2",
|
"monaco-editor": "^0.52.2",
|
||||||
"pinia": "^3.0.2",
|
"pinia": "^3.0.2",
|
||||||
|
"qr-scanner": "^1.4.2",
|
||||||
"sweetalert2": "^11.4.8",
|
"sweetalert2": "^11.4.8",
|
||||||
"v-money3": "^3.24.1",
|
"v-money3": "^3.24.1",
|
||||||
"vue": "^3.5.13",
|
"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>
|
||||||
|
|
@ -0,0 +1,306 @@
|
||||||
|
<template>
|
||||||
|
<v-dialog v-model="dialog" max-width="800" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="d-flex align-center">
|
||||||
|
<v-icon class="ml-2">mdi-plus</v-icon>
|
||||||
|
پرونده واردات جدید
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="create" validate-on="input">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.title"
|
||||||
|
label="عنوان پرونده"
|
||||||
|
:rules="[rules.required, rules.minLength]"
|
||||||
|
required
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.supplierName"
|
||||||
|
label="نام تامین کننده"
|
||||||
|
:rules="[rules.required, rules.minLength]"
|
||||||
|
required
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.supplierCountry"
|
||||||
|
label="کشور تامین کننده"
|
||||||
|
:rules="[rules.minLength]"
|
||||||
|
counter="50"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.supplierPhone"
|
||||||
|
label="تلفن تامین کننده"
|
||||||
|
:rules="[rules.phone]"
|
||||||
|
counter="20"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.supplierEmail"
|
||||||
|
label="ایمیل تامین کننده"
|
||||||
|
type="email"
|
||||||
|
:rules="[rules.email]"
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.supplierAddress"
|
||||||
|
label="آدرس تامین کننده"
|
||||||
|
rows="2"
|
||||||
|
:rules="[rules.maxLength]"
|
||||||
|
counter="500"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.totalAmount)"
|
||||||
|
label="مبلغ کل"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney, rules.maxAmount]"
|
||||||
|
@update:modelValue="onMoneyInput('totalAmount', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.currency"
|
||||||
|
:items="currencyOptions"
|
||||||
|
label="واحد پول"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.exchangeRate)"
|
||||||
|
label="نرخ تبدیل"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney, rules.maxExchangeRate]"
|
||||||
|
@update:modelValue="onMoneyInput('exchangeRate', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="formatMoney(formData.totalAmountIRR)"
|
||||||
|
label="مبلغ کل (ریال)"
|
||||||
|
readonly
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="3"
|
||||||
|
:rules="[rules.maxLength]"
|
||||||
|
counter="1000"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="cancel">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
ایجاد
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
|
||||||
|
// Props & Emits
|
||||||
|
const props = defineProps({
|
||||||
|
modelValue: Boolean
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue', 'created'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
title: '',
|
||||||
|
supplierName: '',
|
||||||
|
supplierCountry: '',
|
||||||
|
supplierPhone: '',
|
||||||
|
supplierEmail: '',
|
||||||
|
supplierAddress: '',
|
||||||
|
totalAmount: '',
|
||||||
|
currency: 'USD',
|
||||||
|
exchangeRate: '',
|
||||||
|
totalAmountIRR: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const dialog = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => emit('update:modelValue', value)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Currency options
|
||||||
|
const currencyOptions = [
|
||||||
|
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||||
|
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||||
|
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||||
|
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||||
|
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||||
|
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
minLength: (value) => !value || value.length >= 2 || 'حداقل 2 کاراکتر الزامی است',
|
||||||
|
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
||||||
|
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
||||||
|
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||||
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
|
positiveMoney: (value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||||
|
},
|
||||||
|
maxAmount: (value) => !value || parseFloat(value) <= 999999999 || 'مبلغ نباید بیشتر از 999,999,999 باشد',
|
||||||
|
maxExchangeRate: (value) => !value || parseFloat(value) <= 999999 || 'نرخ تبدیل نباید بیشتر از 999,999 باشد'
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMoneyInput = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0
|
||||||
|
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||||
|
const num = Number(cleaned)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMoneyInput = (field, value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
formData.value[field] = numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (value) => {
|
||||||
|
const numericValue = Number(value) || 0
|
||||||
|
return numericValue
|
||||||
|
.toFixed(0)
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
watch([
|
||||||
|
() => formData.value.totalAmount,
|
||||||
|
() => formData.value.exchangeRate,
|
||||||
|
() => formData.value.currency
|
||||||
|
], ([newTotalAmount, newExchangeRate, currency]) => {
|
||||||
|
const total = parseMoneyInput(newTotalAmount)
|
||||||
|
const rate = currency === 'IRR' ? 1 : parseMoneyInput(newExchangeRate)
|
||||||
|
const result = Math.round(total * rate)
|
||||||
|
formData.value.totalAmountIRR = isNaN(result) ? 0 : result
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const create = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/import-workflow/create', formData.value)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: 'پرونده واردات با موفقیت ایجاد شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
resetForm()
|
||||||
|
emit('created')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating workflow:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ایجاد پرونده واردات خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancel = () => {
|
||||||
|
resetForm()
|
||||||
|
dialog.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
title: '',
|
||||||
|
supplierName: '',
|
||||||
|
supplierCountry: '',
|
||||||
|
supplierPhone: '',
|
||||||
|
supplierEmail: '',
|
||||||
|
supplierAddress: '',
|
||||||
|
totalAmount: '',
|
||||||
|
currency: 'USD',
|
||||||
|
exchangeRate: '',
|
||||||
|
totalAmountIRR: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.ltr-input :deep(input) {
|
||||||
|
direction: ltr !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-customs">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>اطلاعات ترخیص گمرکی</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن اطلاعات ترخیص
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="customs"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="اطلاعات ترخیص گمرکی ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.totalCustomsCharges="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.totalCustomsCharges) }}
|
||||||
|
<small class="text-medium-emphasis">ریال</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editCustoms(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deleteCustoms(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingCustoms ? 'ویرایش اطلاعات ترخیص' : 'افزودن اطلاعات ترخیص جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveCustoms">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.declarationNumber"
|
||||||
|
label="شماره اظهارنامه"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.customsCode"
|
||||||
|
label="کد گمرک"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.clearanceDate"
|
||||||
|
label="تاریخ ترخیص"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.customsBroker"
|
||||||
|
label="ترخیصکار"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.customsDuty)"
|
||||||
|
label="حقوق گمرکی"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney]"
|
||||||
|
@update:modelValue="onMoneyInput('customsDuty', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.valueAddedTax)"
|
||||||
|
label="مالیات بر ارزش افزوده"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney]"
|
||||||
|
@update:modelValue="onMoneyInput('valueAddedTax', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.otherCharges)"
|
||||||
|
label="سایر عوارض"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney]"
|
||||||
|
@update:modelValue="onMoneyInput('otherCharges', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="formatMoney(totalChargesNumeric)"
|
||||||
|
label="کل هزینههای گمرکی"
|
||||||
|
readonly
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.warehouseNumber"
|
||||||
|
label="شماره انبار"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.customsBrokerPhone"
|
||||||
|
label="تلفن ترخیصکار"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.customsBrokerEmail"
|
||||||
|
label="ایمیل ترخیصکار"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.warehouseLocation"
|
||||||
|
label="محل انبار"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="saveLoading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingCustoms ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, watch } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
customs: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const editingCustoms = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
declarationNumber: '',
|
||||||
|
customsCode: '',
|
||||||
|
clearanceDate: '',
|
||||||
|
customsDuty: '',
|
||||||
|
valueAddedTax: '',
|
||||||
|
otherCharges: '',
|
||||||
|
totalCustomsCharges: '',
|
||||||
|
customsBroker: '',
|
||||||
|
customsBrokerPhone: '',
|
||||||
|
customsBrokerEmail: '',
|
||||||
|
warehouseNumber: '',
|
||||||
|
warehouseLocation: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'شماره اظهارنامه', key: 'declarationNumber', sortable: false },
|
||||||
|
{ title: 'کد گمرک', key: 'customsCode', sortable: false },
|
||||||
|
{ title: 'تاریخ ترخیص', key: 'clearanceDate', sortable: false },
|
||||||
|
{ title: 'ترخیصکار', key: 'customsBroker', sortable: false },
|
||||||
|
{ title: 'کل هزینهها', key: 'totalCustomsCharges', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
|
positiveMoney: (value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
return numeric >= 0 || 'مقدار باید مثبت باشد'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers for money formatting/parse and LTR input
|
||||||
|
const parseMoneyInput = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0
|
||||||
|
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||||
|
const num = Number(cleaned)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMoneyInput = (field, value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
formData.value[field] = numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (value) => {
|
||||||
|
const numericValue = Number(value) || 0
|
||||||
|
return numericValue
|
||||||
|
.toFixed(0)
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 total
|
||||||
|
})
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const editCustoms = (customs) => {
|
||||||
|
editingCustoms.value = customs
|
||||||
|
formData.value = { ...customs }
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteCustoms = async (customs) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'حذف اطلاعات ترخیص',
|
||||||
|
text: 'آیا از حذف این اطلاعات ترخیص گمرکی اطمینان دارید؟',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'حذف',
|
||||||
|
cancelButtonText: 'لغو'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/customs/${customs.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire('موفق', 'اطلاعات ترخیص با موفقیت حذف شد', 'success')
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('خطا', 'در حذف اطلاعات ترخیص خطایی رخ داد', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveCustoms = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = editingCustoms.value
|
||||||
|
? `/api/import-workflow/${props.workflowId}/customs/${editingCustoms.value.id}/update`
|
||||||
|
: `/api/import-workflow/${props.workflowId}/customs/create`
|
||||||
|
|
||||||
|
const method = editingCustoms.value ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: formData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingCustoms.value ? 'اطلاعات ترخیص با موفقیت ویرایش شد' : 'اطلاعات ترخیص با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
closeDialog()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving customs:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره اطلاعات ترخیص خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
editingCustoms.value = null
|
||||||
|
formData.value = {
|
||||||
|
declarationNumber: '',
|
||||||
|
customsCode: '',
|
||||||
|
clearanceDate: '',
|
||||||
|
customsDuty: '',
|
||||||
|
valueAddedTax: '',
|
||||||
|
otherCharges: '',
|
||||||
|
totalCustomsCharges: '',
|
||||||
|
customsBroker: '',
|
||||||
|
customsBrokerPhone: '',
|
||||||
|
customsBrokerEmail: '',
|
||||||
|
warehouseNumber: '',
|
||||||
|
warehouseLocation: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
showAddDialog.value = false
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const formatNumber = (number) => {
|
||||||
|
if (!number) return '0'
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-customs {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -0,0 +1,460 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-documents">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>مدیریت اسناد</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن سند
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="documents"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="سندی ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.type="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getTypeColor(item.type)"
|
||||||
|
size="small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ getTypeText(item.type) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.fileName="{ item }">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="ml-1">{{ getFileIcon(item.fileType) }}</v-icon>
|
||||||
|
<span>{{ item.fileName || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.fileSize="{ item }">
|
||||||
|
{{ formatFileSize(item.fileSize) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
v-if="item.id"
|
||||||
|
icon="mdi-download"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="downloadFile(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editDocument(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deleteDocument(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingDocument ? 'ویرایش سند' : 'افزودن سند جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveDocument">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.type"
|
||||||
|
:items="documentTypes"
|
||||||
|
label="نوع سند"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.title"
|
||||||
|
label="عنوان سند"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.documentNumber"
|
||||||
|
label="شماره سند"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.issueDate"
|
||||||
|
label="تاریخ صدور"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-file-input
|
||||||
|
: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>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="saveLoading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingDocument ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
documents: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const editingDocument = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
const selectedFile = ref(null)
|
||||||
|
const fileInputValue = ref([])
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
documentNumber: '',
|
||||||
|
issueDate: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'نوع سند', key: 'type', sortable: false },
|
||||||
|
{ title: 'عنوان', key: 'title', sortable: false },
|
||||||
|
{ title: 'فایل', key: 'fileName', sortable: false },
|
||||||
|
{ title: 'حجم', key: 'fileSize', sortable: false },
|
||||||
|
{ title: 'شماره سند', key: 'documentNumber', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Document types
|
||||||
|
const documentTypes = [
|
||||||
|
{ title: 'فاکتور تجاری', value: 'commercial_invoice' },
|
||||||
|
{ title: 'پیش فاکتور', value: 'proforma_invoice' },
|
||||||
|
{ title: 'بارنامه', value: 'bill_of_lading' },
|
||||||
|
{ title: 'لیست بستهبندی', value: 'packing_list' },
|
||||||
|
{ title: 'گواهی مبدا', value: 'certificate_of_origin' },
|
||||||
|
{ title: 'گواهی کیفیت', value: 'quality_certificate' },
|
||||||
|
{ title: 'مجوز واردات', value: 'import_permit' },
|
||||||
|
{ title: 'اظهارنامه گمرکی', value: 'customs_declaration' },
|
||||||
|
{ title: 'رسید پرداخت', value: 'payment_receipt' },
|
||||||
|
{ title: 'سایر اسناد', value: 'other' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
fileRequired: (value) => !!selectedFile.value || 'انتخاب فایل الزامی است'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const editDocument = (document) => {
|
||||||
|
editingDocument.value = document
|
||||||
|
formData.value = { ...document }
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteDocument = async (document) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'حذف سند',
|
||||||
|
text: `آیا از حذف سند "${document.title}" اطمینان دارید؟`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'حذف',
|
||||||
|
cancelButtonText: 'لغو'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/documents/${document.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire('موفق', 'سند با موفقیت حذف شد', 'success')
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('خطا', 'در حذف سند خطایی رخ داد', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDocument = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const formDataToSend = new FormData()
|
||||||
|
|
||||||
|
// Add form fields
|
||||||
|
Object.keys(formData.value).forEach(key => {
|
||||||
|
formDataToSend.append(key, formData.value[key] || '')
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add file if selected
|
||||||
|
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 = 'POST'
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: formDataToSend,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingDocument.value ? 'سند با موفقیت ویرایش شد' : 'سند با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
closeDialog()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving document:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره سند خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = null
|
||||||
|
fileInputValue.value = []
|
||||||
|
formData.value = {
|
||||||
|
type: '',
|
||||||
|
title: '',
|
||||||
|
documentNumber: '',
|
||||||
|
issueDate: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
showAddDialog.value = false
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
commercial_invoice: 'blue',
|
||||||
|
proforma_invoice: 'light-blue',
|
||||||
|
bill_of_lading: 'purple',
|
||||||
|
packing_list: 'teal',
|
||||||
|
certificate_of_origin: 'green',
|
||||||
|
quality_certificate: 'lime',
|
||||||
|
import_permit: 'orange',
|
||||||
|
customs_declaration: 'red',
|
||||||
|
payment_receipt: 'pink',
|
||||||
|
other: 'grey'
|
||||||
|
}
|
||||||
|
return colors[type] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type) => {
|
||||||
|
const texts = {
|
||||||
|
commercial_invoice: 'فاکتور تجاری',
|
||||||
|
proforma_invoice: 'پیش فاکتور',
|
||||||
|
bill_of_lading: 'بارنامه',
|
||||||
|
packing_list: 'لیست بستهبندی',
|
||||||
|
certificate_of_origin: 'گواهی مبدا',
|
||||||
|
quality_certificate: 'گواهی کیفیت',
|
||||||
|
import_permit: 'مجوز واردات',
|
||||||
|
customs_declaration: 'اظهارنامه گمرکی',
|
||||||
|
payment_receipt: 'رسید پرداخت',
|
||||||
|
other: 'سایر'
|
||||||
|
}
|
||||||
|
return texts[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const getFileIcon = (fileType) => {
|
||||||
|
if (!fileType) return 'mdi-file'
|
||||||
|
|
||||||
|
if (fileType.includes('pdf')) return 'mdi-file-pdf-box'
|
||||||
|
if (fileType.includes('image')) return 'mdi-file-image'
|
||||||
|
if (fileType.includes('word')) return 'mdi-file-word'
|
||||||
|
if (fileType.includes('excel')) return 'mdi-file-excel'
|
||||||
|
|
||||||
|
return 'mdi-file'
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatFileSize = (size) => {
|
||||||
|
if (!size) return '-'
|
||||||
|
|
||||||
|
const bytes = parseInt(size)
|
||||||
|
if (bytes === 0) return '0 Bytes'
|
||||||
|
|
||||||
|
const k = 1024
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-documents {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,523 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-items">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>آیتمهای وارداتی</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن آیتم
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="items"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="آیتمی ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.unitPrice="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.unitPrice) }}
|
||||||
|
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.totalPrice="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.totalPrice) }}
|
||||||
|
<small class="text-medium-emphasis">{{ getCurrency(item) }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editItem(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deleteItem(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingItem ? 'ویرایش آیتم' : 'افزودن آیتم جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveItem">
|
||||||
|
<v-card-text>
|
||||||
|
<!-- Commodity selector / viewer -->
|
||||||
|
<v-row>
|
||||||
|
<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
|
||||||
|
v-model="formData.brand"
|
||||||
|
label="برند"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.model"
|
||||||
|
label="مدل"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.originCountry"
|
||||||
|
label="کشور مبدا"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.quantity"
|
||||||
|
label="تعداد"
|
||||||
|
type="number"
|
||||||
|
:rules="[rules.required, rules.positive]"
|
||||||
|
required
|
||||||
|
min="1"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.unitPrice)"
|
||||||
|
label="قیمت واحد (ارزی)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.required, rules.positiveMoney]"
|
||||||
|
required
|
||||||
|
@update:modelValue="onMoneyInput('unitPrice', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.unitPriceIRR)"
|
||||||
|
label="قیمت واحد (ریال)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.required, rules.positiveMoney]"
|
||||||
|
required
|
||||||
|
@update:modelValue="onMoneyInput('unitPriceIRR', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="(formData.quantity && formData.unitPrice) ? formatMoney(totalPrice) : ''"
|
||||||
|
label="قیمت کل"
|
||||||
|
readonly
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.weight"
|
||||||
|
label="وزن (کیلوگرم)"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
:rules="[rules.positive]"
|
||||||
|
min="0"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.volume"
|
||||||
|
label="حجم (متر مکعب)"
|
||||||
|
type="number"
|
||||||
|
step="0.01"
|
||||||
|
:rules="[rules.positive]"
|
||||||
|
min="0"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.specifications"
|
||||||
|
label="ویژگیها"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingItem ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>حذف آیتم</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
آیا از حذف آیتم "{{ selectedItem?.name }}" اطمینان دارید؟
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||||
|
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
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({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
currency: {
|
||||||
|
type: String,
|
||||||
|
default: 'USD'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const editingItem = ref(null)
|
||||||
|
const selectedItem = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
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: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
originCountry: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
unitPriceIRR: '',
|
||||||
|
totalPrice: '',
|
||||||
|
totalPriceIRR: '',
|
||||||
|
weight: '',
|
||||||
|
volume: '',
|
||||||
|
specifications: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'نام کالا', key: 'name', sortable: false },
|
||||||
|
{ title: 'برند', key: 'brand', sortable: false },
|
||||||
|
{ title: 'مدل', key: 'model', sortable: false },
|
||||||
|
{ title: 'کشور مبدا', key: 'originCountry', sortable: false },
|
||||||
|
{ title: 'تعداد', key: 'quantity', sortable: false },
|
||||||
|
{ title: 'قیمت واحد', key: 'unitPrice', sortable: false },
|
||||||
|
{ title: 'قیمت کل', key: 'totalPrice', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
|
positiveMoney: (value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers for money formatting/parse and LTR input
|
||||||
|
const parseMoneyInput = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0
|
||||||
|
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||||
|
const num = Number(cleaned)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMoneyInput = (field, value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
formData.value[field] = numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (value) => {
|
||||||
|
const numericValue = Number(value) || 0
|
||||||
|
return numericValue
|
||||||
|
.toFixed(0)
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
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 total
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Watch for unit price IRR and quantity changes
|
||||||
|
watch([() => formData.value.quantity, () => formData.value.unitPriceIRR], () => {
|
||||||
|
if (formData.value.quantity && formData.value.unitPriceIRR) {
|
||||||
|
const total = parseFloat(formData.value.quantity) * parseFloat(formData.value.unitPriceIRR)
|
||||||
|
formData.value.totalPriceIRR = total.toString()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
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) => {
|
||||||
|
selectedItem.value = item
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveItem = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
if (!selectedCommodity.value) {
|
||||||
|
Swal.fire({ title: 'هشدار', text: 'انتخاب کالا الزامی است', icon: 'warning' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = editingItem.value
|
||||||
|
? `/api/import-workflow/${props.workflowId}/items/${editingItem.value.id}/update`
|
||||||
|
: `/api/import-workflow/${props.workflowId}/items/create`
|
||||||
|
|
||||||
|
const method = editingItem.value ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
...formData.value,
|
||||||
|
commodity_id: selectedCommodity.value.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: payload
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingItem.value ? 'آیتم با موفقیت ویرایش شد' : 'آیتم با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
cancelEdit()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving item:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره آیتم خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
deleteLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/items/${selectedItem.value.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: 'آیتم با موفقیت حذف شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting item:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در حذف آیتم خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
editingItem.value = null
|
||||||
|
formData.value = {
|
||||||
|
name: '',
|
||||||
|
productCode: '',
|
||||||
|
brand: '',
|
||||||
|
model: '',
|
||||||
|
originCountry: '',
|
||||||
|
quantity: '',
|
||||||
|
unitPrice: '',
|
||||||
|
unitPriceIRR: '',
|
||||||
|
totalPrice: '',
|
||||||
|
totalPriceIRR: '',
|
||||||
|
weight: '',
|
||||||
|
volume: '',
|
||||||
|
specifications: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
showAddDialog.value = false
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = () => props.currency || 'USD'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-items {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,547 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-payments">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>پرداختها</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن پرداخت
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="payments"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="پرداختی ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.type="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getTypeColor(item.type)"
|
||||||
|
size="small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ getTypeText(item.type) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.amount="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.amount) }}
|
||||||
|
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.amountIRR="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.amountIRR) }}
|
||||||
|
<small class="text-medium-emphasis">ریال</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(item.status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.paymentDate="{ item }">
|
||||||
|
{{ formatDate(item.paymentDate) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editPayment(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deletePayment(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingPayment ? 'ویرایش پرداخت' : 'افزودن پرداخت جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="savePayment">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.type"
|
||||||
|
:items="paymentTypes"
|
||||||
|
label="نوع پرداخت"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.status"
|
||||||
|
:items="statusOptions"
|
||||||
|
label="وضعیت"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.amount)"
|
||||||
|
label="مبلغ"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.required, rules.positiveMoney]"
|
||||||
|
required
|
||||||
|
@update:modelValue="onMoneyInput('amount', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.currency"
|
||||||
|
:items="currencyOptions"
|
||||||
|
label="واحد پول"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(formData.amountIRR)"
|
||||||
|
label="مبلغ (ریال)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney]"
|
||||||
|
@update:modelValue="onMoneyInput('amountIRR', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.paymentDate"
|
||||||
|
label="تاریخ پرداخت"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.referenceNumber"
|
||||||
|
label="شماره مرجع"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.bankName"
|
||||||
|
label="نام بانک"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.accountNumber"
|
||||||
|
label="شماره حساب"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.recipientName"
|
||||||
|
label="نام دریافت کننده"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.receiptNumber"
|
||||||
|
label="شماره رسید"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="loading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingPayment ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>حذف پرداخت</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
آیا از حذف این پرداخت اطمینان دارید؟
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||||
|
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const editingPayment = ref(null)
|
||||||
|
const selectedPayment = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
const deleteLoading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
type: '',
|
||||||
|
amount: '',
|
||||||
|
currency: 'USD',
|
||||||
|
amountIRR: '',
|
||||||
|
paymentDate: '',
|
||||||
|
referenceNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
accountNumber: '',
|
||||||
|
recipientName: '',
|
||||||
|
status: 'pending',
|
||||||
|
description: '',
|
||||||
|
receiptNumber: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'نوع پرداخت', key: 'type', sortable: false },
|
||||||
|
{ title: 'مبلغ', key: 'amount', sortable: false },
|
||||||
|
{ title: 'مبلغ (ریال)', key: 'amountIRR', sortable: false },
|
||||||
|
{ title: 'تاریخ پرداخت', key: 'paymentDate', sortable: false },
|
||||||
|
{ title: 'دریافت کننده', key: 'recipientName', sortable: false },
|
||||||
|
{ title: 'وضعیت', key: 'status', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Options
|
||||||
|
const paymentTypes = [
|
||||||
|
{ title: 'پرداخت به تامین کننده', value: 'supplier' },
|
||||||
|
{ title: 'پرداخت ترخیصکار', value: 'customs_broker' },
|
||||||
|
{ title: 'پرداخت حمل و نقل', value: 'shipping' },
|
||||||
|
{ title: 'پرداخت صرافی', value: 'exchange' },
|
||||||
|
{ title: 'پرداخت بیمه', value: 'insurance' },
|
||||||
|
{ title: 'پرداخت انبار موقت', value: 'temporary_storage' },
|
||||||
|
{ title: 'سایر هزینهها', value: 'other' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'در انتظار پرداخت', value: 'pending' },
|
||||||
|
{ title: 'پرداخت شده', value: 'paid' },
|
||||||
|
{ title: 'تایید شده', value: 'confirmed' },
|
||||||
|
{ title: 'لغو شده', value: 'cancelled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const currencyOptions = [
|
||||||
|
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||||
|
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||||
|
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||||
|
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||||
|
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||||
|
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
|
positiveMoney: (value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helpers for money formatting/parse and LTR input
|
||||||
|
const parseMoneyInput = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0
|
||||||
|
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||||
|
const num = Number(cleaned)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMoneyInput = (field, value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
formData.value[field] = numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatMoney = (value) => {
|
||||||
|
const numericValue = Number(value) || 0
|
||||||
|
return numericValue
|
||||||
|
.toFixed(0)
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const editPayment = (payment) => {
|
||||||
|
editingPayment.value = payment
|
||||||
|
formData.value = { ...payment }
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deletePayment = (payment) => {
|
||||||
|
selectedPayment.value = payment
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const savePayment = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = editingPayment.value
|
||||||
|
? `/api/import-workflow/${props.workflowId}/payments/${editingPayment.value.id}/update`
|
||||||
|
: `/api/import-workflow/${props.workflowId}/payments/create`
|
||||||
|
|
||||||
|
const method = editingPayment.value ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: formData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingPayment.value ? 'پرداخت با موفقیت ویرایش شد' : 'پرداخت با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
cancelEdit()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving payment:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره پرداخت خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
deleteLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/payments/${selectedPayment.value.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: 'پرداخت با موفقیت حذف شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting payment:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در حذف پرداخت خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
editingPayment.value = null
|
||||||
|
formData.value = {
|
||||||
|
type: '',
|
||||||
|
amount: '',
|
||||||
|
currency: 'USD',
|
||||||
|
amountIRR: '',
|
||||||
|
paymentDate: '',
|
||||||
|
referenceNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
accountNumber: '',
|
||||||
|
recipientName: '',
|
||||||
|
status: 'pending',
|
||||||
|
description: '',
|
||||||
|
receiptNumber: ''
|
||||||
|
}
|
||||||
|
showAddDialog.value = false
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
supplier: 'blue',
|
||||||
|
customs_broker: 'orange',
|
||||||
|
shipping: 'purple',
|
||||||
|
exchange: 'teal',
|
||||||
|
insurance: 'green',
|
||||||
|
temporary_storage: 'brown',
|
||||||
|
other: 'grey'
|
||||||
|
}
|
||||||
|
return colors[type] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type) => {
|
||||||
|
const texts = {
|
||||||
|
supplier: 'تامین کننده',
|
||||||
|
customs_broker: 'ترخیصکار',
|
||||||
|
shipping: 'حمل و نقل',
|
||||||
|
exchange: 'صرافی',
|
||||||
|
insurance: 'بیمه',
|
||||||
|
temporary_storage: 'انبار موقت',
|
||||||
|
other: 'سایر'
|
||||||
|
}
|
||||||
|
return texts[type] || type
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'orange',
|
||||||
|
paid: 'blue',
|
||||||
|
confirmed: 'green',
|
||||||
|
cancelled: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
pending: 'در انتظار',
|
||||||
|
paid: 'پرداخت شده',
|
||||||
|
confirmed: 'تایید شده',
|
||||||
|
cancelled: 'لغو شده'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (number) => {
|
||||||
|
if (!number) return '0'
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showAddDialog.value = false
|
||||||
|
cancelEdit()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-payments {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,394 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-shipping">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>اطلاعات حمل و نقل</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن اطلاعات حمل
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="shipping"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="اطلاعات حمل و نقلی ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.type="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getTypeColor(item.type)"
|
||||||
|
size="small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ getTypeText(item.type) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editShipping(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deleteShipping(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="800" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingShipping ? 'ویرایش اطلاعات حمل' : 'افزودن اطلاعات حمل جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveShipping">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.type"
|
||||||
|
:items="shippingTypes"
|
||||||
|
label="نوع حمل"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.shippingCompany"
|
||||||
|
label="شرکت حمل و نقل"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.containerNumber"
|
||||||
|
label="شماره کانتینر"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.billOfLading"
|
||||||
|
label="شماره بارنامه"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.shippingDate"
|
||||||
|
label="تاریخ حمل"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.arrivalDate"
|
||||||
|
label="تاریخ رسیدن"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.unloadingDate"
|
||||||
|
label="تاریخ تخلیه"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.originPort"
|
||||||
|
label="محل مبدا حمل"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.destinationPort"
|
||||||
|
label="محل مقصد حمل"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.vesselName"
|
||||||
|
label="نام وسیله/حامل"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.voyageNumber"
|
||||||
|
label="شماره سفر/پرواز"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="saveLoading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingShipping ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
shipping: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const editingShipping = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
type: '',
|
||||||
|
containerNumber: '',
|
||||||
|
billOfLading: '',
|
||||||
|
shippingDate: '',
|
||||||
|
arrivalDate: '',
|
||||||
|
unloadingDate: '',
|
||||||
|
shippingCompany: '',
|
||||||
|
originPort: '',
|
||||||
|
destinationPort: '',
|
||||||
|
vesselName: '',
|
||||||
|
voyageNumber: '',
|
||||||
|
description: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'نوع حمل', key: 'type', sortable: false },
|
||||||
|
{ title: 'شرکت حمل', key: 'shippingCompany', sortable: false },
|
||||||
|
{ title: 'شماره کانتینر', key: 'containerNumber', sortable: false },
|
||||||
|
{ title: 'بندر مبدا', key: 'originPort', sortable: false },
|
||||||
|
{ title: 'بندر مقصد', key: 'destinationPort', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Shipping types
|
||||||
|
const shippingTypes = [
|
||||||
|
{ title: 'دریایی', value: 'sea' },
|
||||||
|
{ title: 'هوایی', value: 'air' },
|
||||||
|
{ title: 'زمینی', value: 'land' },
|
||||||
|
{ title: 'ترکیبی', value: 'multimodal' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
maxLength: (value) => !value || value.length <= 500 || 'حداکثر 500 کاراکتر مجاز است',
|
||||||
|
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||||
|
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const editShipping = (shipping) => {
|
||||||
|
editingShipping.value = shipping
|
||||||
|
formData.value = { ...shipping }
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteShipping = async (shipping) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'حذف اطلاعات حمل',
|
||||||
|
text: 'آیا از حذف این اطلاعات حمل و نقل اطمینان دارید؟',
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'حذف',
|
||||||
|
cancelButtonText: 'لغو'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/shipping/${shipping.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire('موفق', 'اطلاعات حمل با موفقیت حذف شد', 'success')
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('خطا', 'در حذف اطلاعات حمل خطایی رخ داد', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveShipping = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = editingShipping.value
|
||||||
|
? `/api/import-workflow/${props.workflowId}/shipping/${editingShipping.value.id}/update`
|
||||||
|
: `/api/import-workflow/${props.workflowId}/shipping/create`
|
||||||
|
|
||||||
|
const method = editingShipping.value ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: formData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingShipping.value ? 'اطلاعات حمل با موفقیت ویرایش شد' : 'اطلاعات حمل با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
closeDialog()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving shipping:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره اطلاعات حمل خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
editingShipping.value = null
|
||||||
|
formData.value = {
|
||||||
|
type: '',
|
||||||
|
containerNumber: '',
|
||||||
|
billOfLading: '',
|
||||||
|
shippingDate: '',
|
||||||
|
arrivalDate: '',
|
||||||
|
unloadingDate: '',
|
||||||
|
shippingCompany: '',
|
||||||
|
originPort: '',
|
||||||
|
destinationPort: '',
|
||||||
|
vesselName: '',
|
||||||
|
voyageNumber: '',
|
||||||
|
description: ''
|
||||||
|
}
|
||||||
|
showAddDialog.value = false
|
||||||
|
if (form.value) {
|
||||||
|
form.value.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const getTypeColor = (type) => {
|
||||||
|
const colors = {
|
||||||
|
sea: 'blue',
|
||||||
|
air: 'purple',
|
||||||
|
land: 'green',
|
||||||
|
multimodal: 'orange'
|
||||||
|
}
|
||||||
|
return colors[type] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getTypeText = (type) => {
|
||||||
|
const texts = {
|
||||||
|
sea: 'دریایی',
|
||||||
|
air: 'هوایی',
|
||||||
|
land: 'زمینی',
|
||||||
|
multimodal: 'ترکیبی'
|
||||||
|
}
|
||||||
|
return texts[type] || type
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-shipping {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-stages">
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex justify-space-between align-center mb-4">
|
||||||
|
<h3>مراحل واردات</h3>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showAddDialog = true"
|
||||||
|
>
|
||||||
|
افزودن مرحله
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<v-timeline :direction="$vuetify.display.smAndDown ? 'vertical' : 'horizontal'" class="mb-4">
|
||||||
|
<v-timeline-item
|
||||||
|
v-for="stage in stages"
|
||||||
|
:key="stage.id"
|
||||||
|
:dot-color="getStatusColor(stage.status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template v-slot:icon>
|
||||||
|
<v-icon size="small">{{ getStageIcon(stage.stage) }}</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-card class="elevation-2">
|
||||||
|
<v-card-title class="text-h6">
|
||||||
|
{{ getStageText(stage.stage) }}
|
||||||
|
</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(stage.status)"
|
||||||
|
size="small"
|
||||||
|
class="mb-2"
|
||||||
|
>
|
||||||
|
{{ getStatusText(stage.status) }}
|
||||||
|
</v-chip>
|
||||||
|
<div v-if="stage.startDate">
|
||||||
|
<small>تاریخ شروع: {{ formatDate(stage.startDate) }}</small>
|
||||||
|
</div>
|
||||||
|
<div v-if="stage.endDate">
|
||||||
|
<small>تاریخ پایان: {{ formatDate(stage.endDate) }}</small>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-btn
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editStage(stage)"
|
||||||
|
>
|
||||||
|
ویرایش
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-timeline-item>
|
||||||
|
</v-timeline>
|
||||||
|
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="stages"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
no-data-text="مرحلهای ثبت نشده است"
|
||||||
|
>
|
||||||
|
<template v-slot:item.stage="{ item }">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon class="ml-2">{{ getStageIcon(item.stage) }}</v-icon>
|
||||||
|
{{ getStageText(item.stage) }}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(item.status)"
|
||||||
|
size="small"
|
||||||
|
variant="flat"
|
||||||
|
>
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-pencil"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
@click="editStage(item)"
|
||||||
|
></v-btn>
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-delete"
|
||||||
|
size="small"
|
||||||
|
variant="text"
|
||||||
|
color="error"
|
||||||
|
@click="deleteStage(item)"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<!-- Add/Edit Dialog -->
|
||||||
|
<v-dialog v-model="showAddDialog" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-card-title class="pa-4">
|
||||||
|
{{ editingStage ? 'ویرایش مرحله' : 'افزودن مرحله جدید' }}
|
||||||
|
</v-card-title>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-form ref="form" v-model="valid" @submit.prevent="saveStage">
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.stage"
|
||||||
|
:items="stageTypes"
|
||||||
|
label="نوع مرحله"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-select
|
||||||
|
v-model="formData.status"
|
||||||
|
:items="statusOptions"
|
||||||
|
label="وضعیت"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
required
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.startDate"
|
||||||
|
label="تاریخ شروع"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<h-date-picker
|
||||||
|
v-model="formData.endDate"
|
||||||
|
label="تاریخ پایان"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="formData.assignedTo"
|
||||||
|
label="مسئول"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="formData.notes"
|
||||||
|
label="یادداشتها"
|
||||||
|
rows="2"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
|
||||||
|
<v-divider></v-divider>
|
||||||
|
|
||||||
|
<v-card-actions class="pa-4">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="closeDialog">لغو</v-btn>
|
||||||
|
<v-btn
|
||||||
|
type="submit"
|
||||||
|
color="primary"
|
||||||
|
:loading="saveLoading"
|
||||||
|
:disabled="!valid"
|
||||||
|
>
|
||||||
|
{{ editingStage ? 'ویرایش' : 'افزودن' }}
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-form>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import HDatepicker from '@/components/forms/Hdatepicker.vue'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
workflowId: {
|
||||||
|
type: [String, Number],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
stages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['updated'])
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const loading = ref(false)
|
||||||
|
const showAddDialog = ref(false)
|
||||||
|
const editingStage = ref(null)
|
||||||
|
const form = ref()
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
stage: '',
|
||||||
|
status: 'pending',
|
||||||
|
startDate: '',
|
||||||
|
endDate: '',
|
||||||
|
assignedTo: '',
|
||||||
|
description: '',
|
||||||
|
notes: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'مرحله', key: 'stage', sortable: false },
|
||||||
|
{ title: 'وضعیت', key: 'status', sortable: false },
|
||||||
|
{ title: 'تاریخ شروع', key: 'startDate', sortable: false },
|
||||||
|
{ title: 'تاریخ پایان', key: 'endDate', sortable: false },
|
||||||
|
{ title: 'مسئول', key: 'assignedTo', sortable: false },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false, align: 'center' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Stage types
|
||||||
|
const stageTypes = [
|
||||||
|
{ title: 'صدور پیشفاکتور', value: 'proforma_issue' },
|
||||||
|
{ title: 'تایید سفارش', value: 'order_confirmation' },
|
||||||
|
{ title: 'پرداخت', value: 'payment' },
|
||||||
|
{ title: 'آمادهسازی کالا', value: 'goods_preparation' },
|
||||||
|
{ title: 'حمل و نقل', value: 'shipping' },
|
||||||
|
{ title: 'رسیدن کالا', value: 'arrival' },
|
||||||
|
{ title: 'ترخیص گمرکی', value: 'customs_clearance' },
|
||||||
|
{ title: 'انتقال به انبار', value: 'warehouse_transfer' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'در انتظار', value: 'pending' },
|
||||||
|
{ title: 'در حال انجام', value: 'in_progress' },
|
||||||
|
{ title: 'تکمیل شده', value: 'completed' },
|
||||||
|
{ title: 'لغو شده', value: 'cancelled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const editStage = (stage) => {
|
||||||
|
editingStage.value = stage
|
||||||
|
formData.value = { ...stage }
|
||||||
|
showAddDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteStage = async (stage) => {
|
||||||
|
const result = await Swal.fire({
|
||||||
|
title: 'حذف مرحله',
|
||||||
|
text: `آیا از حذف مرحله "${getStageText(stage.stage)}" اطمینان دارید؟`,
|
||||||
|
icon: 'warning',
|
||||||
|
showCancelButton: true,
|
||||||
|
confirmButtonText: 'حذف',
|
||||||
|
cancelButtonText: 'لغو'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${props.workflowId}/stages/${stage.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire('موفق', 'مرحله با موفقیت حذف شد', 'success')
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
Swal.fire('خطا', 'در حذف مرحله خطایی رخ داد', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveStage = async () => {
|
||||||
|
if (!valid.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const url = editingStage.value
|
||||||
|
? `/api/import-workflow/${props.workflowId}/stages/${editingStage.value.id}/update`
|
||||||
|
: `/api/import-workflow/${props.workflowId}/stages/create`
|
||||||
|
|
||||||
|
const method = editingStage.value ? 'PUT' : 'POST'
|
||||||
|
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data: formData.value
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: editingStage.value ? 'مرحله با موفقیت ویرایش شد' : 'مرحله با موفقیت افزوده شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
cancelEdit()
|
||||||
|
emit('updated')
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving stage:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره مرحله خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeDialog = () => {
|
||||||
|
showAddDialog.value = false
|
||||||
|
editingStage.value = null
|
||||||
|
resetForm()
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = {
|
||||||
|
proforma_issue: 'mdi-file-document-outline',
|
||||||
|
order_confirmation: 'mdi-check-circle-outline',
|
||||||
|
payment: 'mdi-credit-card-outline',
|
||||||
|
goods_preparation: 'mdi-package-variant',
|
||||||
|
shipping: 'mdi-truck-delivery-outline',
|
||||||
|
arrival: 'mdi-map-marker-check',
|
||||||
|
customs_clearance: 'mdi-gavel',
|
||||||
|
warehouse_transfer: 'mdi-warehouse'
|
||||||
|
}
|
||||||
|
return icons[stage] || 'mdi-circle-outline'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStageText = (stage) => {
|
||||||
|
const texts = {
|
||||||
|
proforma_issue: 'صدور پیشفاکتور',
|
||||||
|
order_confirmation: 'تایید سفارش',
|
||||||
|
payment: 'پرداخت',
|
||||||
|
goods_preparation: 'آمادهسازی کالا',
|
||||||
|
shipping: 'حمل و نقل',
|
||||||
|
arrival: 'رسیدن کالا',
|
||||||
|
customs_clearance: 'ترخیص گمرکی',
|
||||||
|
warehouse_transfer: 'انتقال به انبار'
|
||||||
|
}
|
||||||
|
return texts[stage] || stage
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
pending: 'orange',
|
||||||
|
in_progress: 'blue',
|
||||||
|
completed: 'green',
|
||||||
|
cancelled: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
pending: 'در انتظار',
|
||||||
|
in_progress: 'در حال انجام',
|
||||||
|
completed: 'تکمیل شده',
|
||||||
|
cancelled: 'لغو شده'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-stages {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -137,16 +137,55 @@
|
||||||
<v-window-item value="file">
|
<v-window-item value="file">
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<v-card variant="outlined" class="pa-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-file-input
|
||||||
v-model="uploadedFile"
|
v-model="uploadedFile"
|
||||||
label="انتخاب فایل"
|
label="انتخاب فایل"
|
||||||
accept=".xlsx,.xls"
|
accept=".xlsx,.xls,.csv"
|
||||||
prepend-icon="mdi-file-excel"
|
prepend-icon="mdi-file-upload"
|
||||||
show-size
|
show-size
|
||||||
counter
|
counter
|
||||||
@change="handleFileUpload"
|
@update:model-value="handleFileUpload"
|
||||||
></v-file-input>
|
></v-file-input>
|
||||||
|
|
||||||
<v-alert
|
<v-alert
|
||||||
|
|
@ -213,7 +252,7 @@ const emit = defineEmits<{
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const activeTab = ref('manual')
|
const activeTab = ref('manual')
|
||||||
const uploadedFile = ref(null)
|
const uploadedFile = ref<File | null>(null)
|
||||||
const filePreview = ref<any[]>([])
|
const filePreview = ref<any[]>([])
|
||||||
const fileErrors = ref<string[]>([])
|
const fileErrors = ref<string[]>([])
|
||||||
|
|
||||||
|
|
@ -285,7 +324,7 @@ const tableHeaders = [
|
||||||
|
|
||||||
const previewHeaders = [
|
const previewHeaders = [
|
||||||
{ title: 'شماره سریال', key: 'serialNumber' },
|
{ title: 'شماره سریال', key: 'serialNumber' },
|
||||||
{ title: 'محصول', key: 'commodity' },
|
{ title: 'کد کالا', key: 'commodity_code' },
|
||||||
{ title: 'توضیحات', key: 'description' },
|
{ title: 'توضیحات', key: 'description' },
|
||||||
{ title: 'شروع گارانتی', key: 'warrantyStartDate' },
|
{ title: 'شروع گارانتی', key: 'warrantyStartDate' },
|
||||||
{ title: 'پایان گارانتی', key: 'warrantyEndDate' },
|
{ title: 'پایان گارانتی', key: 'warrantyEndDate' },
|
||||||
|
|
@ -336,28 +375,61 @@ const removeManualRow = (index: number) => {
|
||||||
manualSerials.value.splice(index, 1)
|
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) {
|
if (!file) {
|
||||||
filePreview.value = []
|
filePreview.value = []
|
||||||
fileErrors.value = []
|
fileErrors.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!(file instanceof File)) {
|
||||||
|
fileErrors.value = ['فایل انتخاب شده معتبر نیست']
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', file)
|
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: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'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 || []
|
fileErrors.value = response.data.errors || []
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
fileErrors.value = ['خطا در خواندن فایل']
|
if (error.response?.data?.error) {
|
||||||
console.error(error)
|
fileErrors.value = [error.response.data.error]
|
||||||
|
} else {
|
||||||
|
fileErrors.value = ['خطا در خواندن فایل']
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -393,7 +465,14 @@ const importData = async () => {
|
||||||
return
|
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 {
|
} else {
|
||||||
if (!uploadedFile.value) {
|
if (!uploadedFile.value) {
|
||||||
await Swal.fire({
|
await Swal.fire({
|
||||||
|
|
@ -407,13 +486,29 @@ const importData = async () => {
|
||||||
const formData = new FormData()
|
const formData = new FormData()
|
||||||
formData.append('file', uploadedFile.value)
|
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: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'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) {
|
if (data.length === 0) {
|
||||||
|
|
@ -438,7 +533,6 @@ const importData = async () => {
|
||||||
icon: 'error',
|
icon: 'error',
|
||||||
confirmButtonText: 'قبول'
|
confirmButtonText: 'قبول'
|
||||||
})
|
})
|
||||||
console.error(error)
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
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 = () => {
|
const resetData = () => {
|
||||||
manualSerials.value = [{
|
manualSerials.value = [{
|
||||||
serialNumber: '',
|
serialNumber: '',
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<v-dialog v-model="dialog" max-width="600px" persistent>
|
<v-dialog v-model="dialog" max-width="640px" persistent>
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-title class="d-flex align-center p-3 gap-2">
|
<v-card-title class="d-flex align-center p-3 gap-2">
|
||||||
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||||
|
|
@ -10,104 +10,109 @@
|
||||||
<v-form ref="form" v-model="valid">
|
<v-form ref="form" v-model="valid">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-text-field
|
<v-text-field v-model="formData.serialNumber" label="شماره سریال *" :rules="[rules.serialNumber]" required
|
||||||
v-model="formData.serialNumber"
|
:disabled="isEdit" variant="outlined" density="comfortable" hide-details="auto" maxlength="50" counter>
|
||||||
label="شماره سریال *"
|
<template #append>
|
||||||
:rules="[rules.serialNumber]"
|
<v-btn icon small @click="openScanner" :disabled="isEdit" color="primary" variant="text">
|
||||||
required
|
<v-icon size="20">mdi-qrcode-scan</v-icon>
|
||||||
:disabled="isEdit"
|
</v-btn>
|
||||||
variant="outlined"
|
</template>
|
||||||
density="comfortable"
|
</v-text-field>
|
||||||
hide-details="auto"
|
|
||||||
maxlength="50"
|
|
||||||
counter
|
|
||||||
></v-text-field>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-autocomplete
|
<Hcommoditysearch
|
||||||
v-model="formData.commodity_id"
|
: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="محصول *"
|
label="محصول *"
|
||||||
:items="commodities"
|
|
||||||
item-title="name"
|
|
||||||
item-value="id"
|
|
||||||
:rules="[rules.commodity]"
|
:rules="[rules.commodity]"
|
||||||
required
|
required
|
||||||
:filter="customFilter"
|
class="serial-commodity"
|
||||||
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]"
|
|
||||||
/>
|
/>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<h-date-picker
|
<h-date-picker v-model="formData.warrantyStartDate" label="تاریخ شروع گارانتی" :rules="[rules.date]" dense
|
||||||
v-model="formData.warrantyEndDate"
|
outlined hide-details="auto" />
|
||||||
label="تاریخ پایان گارانتی"
|
|
||||||
:rules="[rules.date, (value: any) => rules.endDate(value, formData.warrantyStartDate)]"
|
|
||||||
/>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-select
|
<h-date-picker v-model="formData.warrantyEndDate" label="تاریخ پایان گارانتی"
|
||||||
v-model="formData.status"
|
:rules="[(v: any) => rules.endDate(v, formData.warrantyStartDate)]" dense outlined
|
||||||
label="وضعیت"
|
hide-details="auto" />
|
||||||
:items="statusOptions"
|
|
||||||
item-title="title"
|
|
||||||
item-value="value"
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
></v-select>
|
|
||||||
</v-col>
|
</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-col cols="12">
|
||||||
<v-textarea
|
<v-textarea v-model="formData.description" label="توضیحات" rows="3" auto-grow variant="outlined"
|
||||||
v-model="formData.description"
|
density="comfortable" hide-details="auto" />
|
||||||
label="توضیحات"
|
|
||||||
rows="3"
|
|
||||||
auto-grow
|
|
||||||
variant="outlined"
|
|
||||||
density="comfortable"
|
|
||||||
hide-details="auto"
|
|
||||||
></v-textarea>
|
|
||||||
</v-col>
|
</v-col>
|
||||||
<!-- <v-col cols="12">
|
|
||||||
<v-textarea
|
|
||||||
v-model="formData.notes"
|
|
||||||
label="یادداشتها"
|
|
||||||
rows="2"
|
|
||||||
auto-grow
|
|
||||||
></v-textarea>
|
|
||||||
</v-col> -->
|
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-form>
|
</v-form>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
|
|
||||||
<v-card-actions>
|
<v-card-actions>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer />
|
||||||
<v-btn color="grey" @click="close">انصراف</v-btn>
|
<v-btn color="grey" @click="close">انصراف</v-btn>
|
||||||
<v-btn
|
<v-btn color="primary" @click="save" :loading="loading" :disabled="!valid">
|
||||||
color="primary"
|
|
||||||
@click="save"
|
|
||||||
:loading="loading"
|
|
||||||
:disabled="!valid"
|
|
||||||
>
|
|
||||||
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
{{ isEdit ? 'ویرایش' : 'ذخیره' }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</v-card-actions>
|
</v-card-actions>
|
||||||
</v-card>
|
</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>
|
</v-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
|
|
@ -124,165 +129,331 @@ const emit = defineEmits<{
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const valid = ref(false)
|
const valid = ref(false)
|
||||||
const form = ref()
|
const form = ref()
|
||||||
|
const commodityModel = ref<any>(null)
|
||||||
|
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
serialNumber: '',
|
serialNumber: '',
|
||||||
commodity_id: '',
|
commodity_id: null as number | null,
|
||||||
description: '',
|
description: '',
|
||||||
warrantyStartDate: '',
|
warrantyStartDate: '',
|
||||||
warrantyEndDate: '',
|
warrantyEndDate: '',
|
||||||
status: 'active',
|
status: 'available',
|
||||||
|
activation: 'deactive',
|
||||||
notes: ''
|
notes: ''
|
||||||
})
|
})
|
||||||
|
|
||||||
const rules = {
|
const rules = {
|
||||||
required: (value: any) => !!value || 'این فیلد الزامی است',
|
required: (v: any) => !!v || 'این فیلد الزامی است',
|
||||||
serialNumber: (value: any) => {
|
serialNumber: (v: any) => {
|
||||||
if (!value) return 'شماره سریال الزامی است'
|
if (!v) return 'شماره سریال الزامی است'
|
||||||
if (!/^[A-Za-z0-9]+$/.test(value)) {
|
if (!/^[A-Za-z0-9\-_.]+$/.test(v)) return 'شماره سریال نامعتبر است'
|
||||||
return 'شماره سریال فقط میتواند شامل حروف انگلیسی و اعداد باشد'
|
if (v.length < 3) return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
||||||
}
|
if (v.length > 50) return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
||||||
if (value.length < 3) {
|
|
||||||
return 'شماره سریال باید حداقل ۳ کاراکتر باشد'
|
|
||||||
}
|
|
||||||
if (value.length > 50) {
|
|
||||||
return 'شماره سریال نمیتواند بیشتر از ۵۰ کاراکتر باشد'
|
|
||||||
}
|
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
commodity: (value: any) => {
|
commodity: (v: any) => !!v || 'انتخاب محصول الزامی است',
|
||||||
if (!value) return 'انتخاب محصول الزامی است'
|
date: (v: any) => {
|
||||||
return true
|
if (!v) return true
|
||||||
|
const d = new Date(v)
|
||||||
|
return !isNaN(d.getTime()) || 'تاریخ نامعتبر است'
|
||||||
},
|
},
|
||||||
date: (value: any) => {
|
endDate: (v: any, s: any) => {
|
||||||
if (!value) return true
|
if (!v || !s) return true
|
||||||
const date = new Date(value)
|
const end = new Date(v); const start = new Date(s)
|
||||||
if (isNaN(date.getTime())) {
|
return end > start || 'تاریخ پایان باید بعد از تاریخ شروع باشد'
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ title: 'فعال', value: 'active' },
|
{ title: 'آزاد', value: 'available' },
|
||||||
{ title: 'غیرفعال', value: 'inactive' },
|
{ title: 'تخصیص یافته', value: 'allocated' },
|
||||||
{ title: 'منقضی شده', value: 'expired' },
|
{ title: 'تأیید شده', value: 'verified' },
|
||||||
// { title: 'استفاده شده', value: 'used' }
|
{ title: 'متصل', value: 'bound' },
|
||||||
|
{ title: 'مصرف شده', value: 'consumed' },
|
||||||
|
{ title: 'باطل', value: 'void' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const activationOptions = [
|
||||||
|
{ title: 'غیرفعال', value: 'deactive' },
|
||||||
|
{ title: 'فعال', value: 'active' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const dialog = computed({
|
const dialog = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => emit('update:modelValue', value)
|
set: (v) => emit('update:modelValue', v)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isEdit = computed(() => !!props.serial)
|
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 save = async () => {
|
||||||
const isValid = await form.value.validate()
|
const res = await form.value?.validate()
|
||||||
if (!isValid) {
|
const ok = typeof res === 'object' ? res.valid : !!res
|
||||||
console.log('فرم دارای خطا است')
|
if (!ok) return
|
||||||
return
|
if (!formData.value.serialNumber || !formData.value.commodity_id) return
|
||||||
}
|
|
||||||
|
|
||||||
if (!formData.value.serialNumber || !formData.value.commodity_id) {
|
|
||||||
console.log('فیلدهای الزامی پر نشدهاند')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
const data = { ...formData.value }
|
emit('save', { ...formData.value })
|
||||||
emit('save', data)
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false
|
loading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
clearValidationErrors()
|
closeScanner()
|
||||||
emit('close')
|
emit('close')
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
serialNumber: '',
|
serialNumber: '',
|
||||||
commodity_id: '',
|
commodity_id: null,
|
||||||
description: '',
|
description: '',
|
||||||
warrantyStartDate: '',
|
warrantyStartDate: '',
|
||||||
warrantyEndDate: '',
|
warrantyEndDate: '',
|
||||||
status: 'active',
|
status: 'available',
|
||||||
|
activation: 'deactive',
|
||||||
notes: ''
|
notes: ''
|
||||||
}
|
}
|
||||||
if (form.value) {
|
commodityModel.value = null
|
||||||
form.value.resetValidation()
|
form.value?.resetValidation()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearValidationErrors = () => {
|
|
||||||
if (form.value) {
|
|
||||||
form.value.resetValidation()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadSerialData = () => {
|
const loadSerialData = () => {
|
||||||
if (props.serial) {
|
if (props.serial) {
|
||||||
formData.value = {
|
formData.value = {
|
||||||
serialNumber: props.serial.serialNumber || '',
|
serialNumber: props.serial.serialNumber || '',
|
||||||
commodity_id: props.serial.commodity?.id || '',
|
commodity_id: Number(props.serial.commodity?.id) || null,
|
||||||
description: props.serial.description || '',
|
description: props.serial.description || '',
|
||||||
warrantyStartDate: props.serial.warrantyStartDate || '',
|
warrantyStartDate: props.serial.warrantyStartDate || '',
|
||||||
warrantyEndDate: props.serial.warrantyEndDate || '',
|
warrantyEndDate: props.serial.warrantyEndDate || '',
|
||||||
status: props.serial.status || 'active',
|
status: props.serial.status || 'available',
|
||||||
|
activation: props.serial.activation || 'deactive',
|
||||||
notes: props.serial.notes || ''
|
notes: props.serial.notes || ''
|
||||||
}
|
}
|
||||||
|
commodityModel.value = props.commodities.find(c => c.id === formData.value.commodity_id) || null
|
||||||
} else {
|
} else {
|
||||||
resetForm()
|
resetForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const customFilter = (item: any, queryText: string) => {
|
const customFilter = (item: any, q: string) => {
|
||||||
const text = item.name.toLowerCase()
|
const t = String(item?.name || '').toLowerCase()
|
||||||
const searchText = queryText.toLowerCase()
|
const s = String(q || '').toLowerCase()
|
||||||
return text.indexOf(searchText) > -1
|
return t.indexOf(s) > -1
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCommoditySelect = (selectedCommodity: any) => {
|
const handleCommoditySelect = (c: any) => {
|
||||||
if (selectedCommodity && selectedCommodity.id) {
|
formData.value.commodity_id = c?.id ? Number(c.id) : null
|
||||||
formData.value.commodity_id = selectedCommodity.id
|
|
||||||
} else {
|
|
||||||
formData.value.commodity_id = ''
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(() => props.serial, () => {
|
watch(() => props.serial, () => nextTick(loadSerialData), { immediate: true })
|
||||||
nextTick(() => {
|
watch(() => props.modelValue, v => { if (v) nextTick(loadSerialData) })
|
||||||
loadSerialData()
|
onBeforeUnmount(() => { closeScanner() })
|
||||||
})
|
|
||||||
}, { immediate: true })
|
|
||||||
|
|
||||||
watch(() => props.modelValue, (newVal) => {
|
|
||||||
if (newVal) {
|
|
||||||
nextTick(() => {
|
|
||||||
loadSerialData()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.v-dialog {
|
/* normalize Hcommoditysearch height with other inputs */
|
||||||
direction: rtl;
|
.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>
|
<template>
|
||||||
<v-dialog v-model="dialog" max-width="700px">
|
<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-card-title class="d-flex align-center p-3 gap-2">
|
||||||
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
<v-icon class="mr-3" color="primary">mdi-shield-check</v-icon>
|
||||||
<span>جزئیات سریال گارانتی</span>
|
<span>جزئیات سریال گارانتی</span>
|
||||||
<v-spacer></v-spacer>
|
<v-spacer></v-spacer>
|
||||||
<v-chip
|
<v-chip :color="getStatusColor(serial?.status)" size="small">
|
||||||
:color="getStatusColor(serial?.status)"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{{ getStatusText(serial?.status) }}
|
{{ getStatusText(serial?.status) }}
|
||||||
</v-chip>
|
</v-chip>
|
||||||
|
<v-chip v-if="serial?.expired" color="error" size="small" class="opacity-100">
|
||||||
|
منقضی شده
|
||||||
|
</v-chip>
|
||||||
</v-card-title>
|
</v-card-title>
|
||||||
|
|
||||||
<v-card-text v-if="serial">
|
<v-card-text v-if="serial">
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-card variant="outlined" class="pa-4">
|
<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>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="primary">mdi-barcode</v-icon>
|
<v-icon color="primary">mdi-barcode</v-icon>
|
||||||
</template>
|
</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">
|
<v-list-item-subtitle class="font-weight-bold">
|
||||||
{{ serial.serialNumber }}
|
{{ serial.serialNumber }}
|
||||||
</v-list-item-subtitle>
|
</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</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>
|
<v-list-item>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="primary">mdi-package-variant</v-icon>
|
<v-icon color="primary">mdi-package-variant</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>محصول</v-list-item-title>
|
<v-list-item-title>کالا</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<router-link :to="`/acc/commodity/mod/${serial.commodity?.code}`">
|
||||||
<div class="font-weight-bold">{{ serial.commodity?.name }}</div>
|
<v-list-item-subtitle>
|
||||||
<div class="text-caption">کد: {{ serial.commodity?.code }}</div>
|
<div class="font-weight-bold">{{ serial.commodity?.name }}</div>
|
||||||
</v-list-item-subtitle>
|
<div class="text-caption">کد: {{ serial.commodity?.code }}</div>
|
||||||
|
</v-list-item-subtitle>
|
||||||
|
</router-link>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
|
|
@ -47,13 +59,24 @@
|
||||||
<v-list-item-title>تاریخ ثبت</v-list-item-title>
|
<v-list-item-title>تاریخ ثبت</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ formatDate(serial.dateSubmit) }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ formatDate(serial.dateSubmit) }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" md="6">
|
<v-col cols="12" md="6">
|
||||||
<v-card variant="outlined" class="pa-4">
|
<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>
|
||||||
<v-list-item>
|
<v-list-item>
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
|
|
@ -71,7 +94,9 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>پایان گارانتی</v-list-item-title>
|
<v-list-item-title>پایان گارانتی</v-list-item-title>
|
||||||
<v-list-item-subtitle>
|
<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-subtitle>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
|
||||||
|
|
@ -82,20 +107,54 @@
|
||||||
<v-list-item-title>ثبت کننده</v-list-item-title>
|
<v-list-item-title>ثبت کننده</v-list-item-title>
|
||||||
<v-list-item-subtitle>{{ serial.submitter?.name }}</v-list-item-subtitle>
|
<v-list-item-subtitle>{{ serial.submitter?.name }}</v-list-item-subtitle>
|
||||||
</v-list-item>
|
</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-list>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" v-if="serial.description">
|
<v-col cols="12" v-if="serial.description">
|
||||||
<v-card variant="outlined" class="pa-4">
|
<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>
|
<p class="text-body-1">{{ serial.description }}</p>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
<v-col cols="12" v-if="serial.notes">
|
<v-col cols="12" v-if="serial.notes">
|
||||||
<v-card variant="outlined" class="pa-4">
|
<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>
|
<p class="text-body-1">{{ serial.notes }}</p>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
@ -135,40 +194,44 @@ const close = () => {
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active': return 'success'
|
case 'available': return 'success'
|
||||||
case 'inactive': return 'grey'
|
case 'allocated': return 'info'
|
||||||
case 'expired': return 'warning'
|
case 'verified': return 'primary'
|
||||||
case 'used': return 'info'
|
case 'bound': return 'warning'
|
||||||
|
case 'consumed': return 'teal'
|
||||||
|
case 'void': return 'grey'
|
||||||
default: return 'grey'
|
default: return 'grey'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'active': return 'فعال'
|
case 'available': return 'آزاد'
|
||||||
case 'inactive': return 'غیرفعال'
|
case 'allocated': return 'تخصیص یافته'
|
||||||
case 'expired': return 'منقضی شده'
|
case 'verified': return 'تأیید شده'
|
||||||
// case 'used': return 'استفاده شده'
|
case 'bound': return 'متصل'
|
||||||
default: return status
|
case 'consumed': return 'مصرف شده'
|
||||||
|
case 'void': return 'باطل'
|
||||||
|
default: return 'نامشخص'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatDate = (date: string) => {
|
const formatDate = (date: string) => {
|
||||||
if (!date) return '-'
|
if (!date) return '-'
|
||||||
|
|
||||||
if (date.includes('/') && date.split('/')[0].length === 4) {
|
if (date.includes('/') && date.split('/')[0].length === 4) {
|
||||||
return date
|
return date
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dateObj = new Date(date)
|
const dateObj = new Date(date)
|
||||||
if (isNaN(dateObj.getTime())) return '-'
|
if (isNaN(dateObj.getTime())) return '-'
|
||||||
|
|
||||||
const jMoment = moment(dateObj)
|
const jMoment = moment(dateObj)
|
||||||
const persianYear = jMoment.jYear()
|
const persianYear = jMoment.jYear()
|
||||||
const persianMonth = jMoment.jMonth() + 1
|
const persianMonth = jMoment.jMonth() + 1
|
||||||
const persianDay = jMoment.jDate()
|
const persianDay = jMoment.jDate()
|
||||||
|
|
||||||
return `${persianYear}/${persianMonth.toString().padStart(2, '0')}/${persianDay.toString().padStart(2, '0')}`
|
return `${persianYear}/${persianMonth.toString().padStart(2, '0')}/${persianDay.toString().padStart(2, '0')}`
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return date
|
return date
|
||||||
|
|
@ -176,8 +239,45 @@ const formatDate = (date: string) => {
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
.v-dialog {
|
.v-dialog {
|
||||||
direction: rtl;
|
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>
|
||||||
|
|
@ -1060,6 +1060,26 @@ const router = createRouter({
|
||||||
component: () =>
|
component: () =>
|
||||||
import('../views/acc/inquiry/panel.vue'),
|
import('../views/acc/inquiry/panel.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'plugins/import-workflow/list',
|
||||||
|
name: 'import_workflow_list',
|
||||||
|
component: () =>
|
||||||
|
import('../views/acc/plugins/import-workflow/list.vue'),
|
||||||
|
meta: {
|
||||||
|
'title': 'مدیریت واردات کالا',
|
||||||
|
'login': true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'plugins/import-workflow/:id',
|
||||||
|
name: 'import_workflow_detail',
|
||||||
|
component: () =>
|
||||||
|
import('../views/acc/plugins/import-workflow/view.vue'),
|
||||||
|
meta: {
|
||||||
|
'title': 'جزئیات پرونده واردات',
|
||||||
|
'login': true,
|
||||||
|
}
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -1121,6 +1141,23 @@ const router = createRouter({
|
||||||
'title': 'نصب وب اپلیکیشن ',
|
'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',
|
path: '/oauth/authorize',
|
||||||
name: 'oauth_authorize',
|
name: 'oauth_authorize',
|
||||||
|
|
@ -1147,8 +1184,8 @@ router.beforeEach(async (to, from, next) => {
|
||||||
} else {
|
} else {
|
||||||
document.title = <string>to.meta.title;
|
document.title = <string>to.meta.title;
|
||||||
}
|
}
|
||||||
//check user is login
|
//check user is login (skip for public routes)
|
||||||
if (to.meta.login) {
|
if (to.meta.login && !to.meta.public) {
|
||||||
let result = await axios.post('/api/user/check/login');
|
let result = await axios.post('/api/user/check/login');
|
||||||
if (result.status == 200 && result.data.Success == true) {
|
if (result.status == 200 && result.data.Success == true) {
|
||||||
//check user has role
|
//check user has role
|
||||||
|
|
@ -1172,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();
|
next();
|
||||||
return
|
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';
|
||||||
|
}
|
||||||
|
|
@ -216,6 +216,7 @@ export default {
|
||||||
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
{ path: '/acc/plugins/tax/invoices/list', key: 'L', label: this.$t('drawer.tax_invoices'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||||
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
{ path: '/acc/plugins/tax/settings', key: 'T', label: this.$t('drawer.tax_settings'), ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('taxsettings') },
|
||||||
{ path: '/acc/plugins/custominvoice/templates', key: 'I', label: 'قالبهای فاکتور', ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('custominvoice') },
|
{ path: '/acc/plugins/custominvoice/templates', key: 'I', label: 'قالبهای فاکتور', ctrl: true, shift: true, permission: () => this.permissions.settings && this.isPluginActive('custominvoice') },
|
||||||
|
{ path: '/acc/plugins/import-workflow', key: 'I', label: 'مدیریت واردات کالا', ctrl: true, shift: true, permission: () => this.permissions.importWorkflow },
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
restorePermissions(shortcuts) {
|
restorePermissions(shortcuts) {
|
||||||
|
|
@ -806,6 +807,26 @@ export default {
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list-group>
|
</v-list-group>
|
||||||
<v-list-subheader color="primary">{{ $t('drawer.services') }}</v-list-subheader>
|
<v-list-subheader color="primary">{{ $t('drawer.services') }}</v-list-subheader>
|
||||||
|
<v-list-group v-show="isPluginActive('import-workflow') && permissions.importWorkflow">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-list-item class="text-dark" v-bind="props" title="مدیریت واردات کالا">
|
||||||
|
<template v-slot:prepend><v-icon icon="mdi-import" color="primary"></v-icon></template>
|
||||||
|
</v-list-item>
|
||||||
|
</template>
|
||||||
|
<v-list-item v-if="permissions.importWorkflow" to="/acc/plugins/import-workflow/list">
|
||||||
|
<v-list-item-title>
|
||||||
|
لیست پروندههای واردات
|
||||||
|
<span v-if="isCtrlShiftPressed" class="shortcut-key">{{ getShortcutKey('/acc/plugins/import-workflow/list') }}</span>
|
||||||
|
</v-list-item-title>
|
||||||
|
<template v-slot:append>
|
||||||
|
<v-tooltip text="پرونده واردات جدید" location="end">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon="mdi-plus-box" variant="plain" @click="$router.push('/acc/plugins/import-workflow/list')" />
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list-group>
|
||||||
<v-list-group v-show="permissions.plugRepservice && isPluginActive('repservice')">
|
<v-list-group v-show="permissions.plugRepservice && isPluginActive('repservice')">
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.repservice')">
|
<v-list-item class="text-dark" v-bind="props" :title="$t('drawer.repservice')">
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,9 @@
|
||||||
<v-btn color="primary" size="small" @click="dialog = true" :loading="loading" prepend-icon="mdi-bank">
|
<v-btn color="primary" size="small" @click="dialog = true" :loading="loading" prepend-icon="mdi-bank">
|
||||||
{{ $t('dialog.banks_accounts') }}
|
{{ $t('dialog.banks_accounts') }}
|
||||||
</v-btn>
|
</v-btn>
|
||||||
|
<v-btn color="warning" size="small" @click="editPersonDialog = true" :loading="loading" prepend-icon="mdi-account-edit" class="ml-2">
|
||||||
|
ویرایش شخص
|
||||||
|
</v-btn>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn v-bind="props" icon="" color="red">
|
<v-btn v-bind="props" icon="" color="red">
|
||||||
|
|
@ -91,6 +94,81 @@
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-dialog>
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- دیالوگ ویرایش شخص -->
|
||||||
|
<v-dialog v-model="editPersonDialog" max-width="600" persistent>
|
||||||
|
<v-card>
|
||||||
|
<v-toolbar color="primary-dark" dense flat>
|
||||||
|
<v-toolbar-title class="text-white">ویرایش شخص</v-toolbar-title>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn icon @click="editPersonDialog = false">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
<v-card-text class="pa-4">
|
||||||
|
<v-form ref="editPersonForm" v-model="editPersonFormValid">
|
||||||
|
<v-row dense>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.nikename"
|
||||||
|
label="نام مستعار"
|
||||||
|
dense
|
||||||
|
required
|
||||||
|
:rules="[v => !!v || 'نام مستعار الزامی است']"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.name"
|
||||||
|
label="نام کامل"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.mobile"
|
||||||
|
label="موبایل"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.tel"
|
||||||
|
label="تلفن"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.address"
|
||||||
|
label="آدرس"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editPersonData.des"
|
||||||
|
label="توضیحات"
|
||||||
|
dense
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn color="secondary" text @click="editPersonDialog = false">
|
||||||
|
انصراف
|
||||||
|
</v-btn>
|
||||||
|
<v-btn color="primary" @click="savePersonChanges" :loading="saveLoading" :disabled="!editPersonFormValid">
|
||||||
|
ذخیره
|
||||||
|
</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
<!-- محتوای اصلی -->
|
<!-- محتوای اصلی -->
|
||||||
<v-container fluid class="pa-4">
|
<v-container fluid class="pa-4">
|
||||||
<v-row dense>
|
<v-row dense>
|
||||||
|
|
@ -179,6 +257,7 @@
|
||||||
$filters.formatNumber(selectedPerson.bd) || '-' }}</span></div>
|
$filters.formatNumber(selectedPerson.bd) || '-' }}</span></div>
|
||||||
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_balance') }}: <span class="text-primary">{{
|
<div class="text-subtitle-2">{{ $t('pages.person_card.accounting_balance') }}: <span class="text-primary">{{
|
||||||
$filters.formatNumber(selectedPerson.balance) || '-' }}</span></div>
|
$filters.formatNumber(selectedPerson.balance) || '-' }}</span></div>
|
||||||
|
<v-divider class="my-2" />
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
@ -263,6 +342,18 @@ export default {
|
||||||
items: [],
|
items: [],
|
||||||
loading: ref(false),
|
loading: ref(false),
|
||||||
dialog: false,
|
dialog: false,
|
||||||
|
editPersonDialog: false,
|
||||||
|
editPersonFormValid: false,
|
||||||
|
saveLoading: false,
|
||||||
|
editPersonData: {
|
||||||
|
nikename: '',
|
||||||
|
name: '',
|
||||||
|
mobile: '',
|
||||||
|
tel: '',
|
||||||
|
address: '',
|
||||||
|
des: '',
|
||||||
|
|
||||||
|
},
|
||||||
debounceTimeout: null, // برای مدیریت debounce
|
debounceTimeout: null, // برای مدیریت debounce
|
||||||
headers: [
|
headers: [
|
||||||
{ title: this.$t('dialog.operation'), key: "operation", align: "center", sortable: false },
|
{ title: this.$t('dialog.operation'), key: "operation", align: "center", sortable: false },
|
||||||
|
|
@ -338,6 +429,16 @@ export default {
|
||||||
try {
|
try {
|
||||||
const personResponse = await axios.post('/api/person/info/' + id);
|
const personResponse = await axios.post('/api/person/info/' + id);
|
||||||
this.selectedPerson = personResponse.data;
|
this.selectedPerson = personResponse.data;
|
||||||
|
// پر کردن فرم ویرایش با اطلاعات فعلی شخص
|
||||||
|
this.editPersonData = {
|
||||||
|
nikename: this.selectedPerson.nikename || '',
|
||||||
|
name: this.selectedPerson.name || '',
|
||||||
|
mobile: this.selectedPerson.mobile || '',
|
||||||
|
tel: this.selectedPerson.tel || '',
|
||||||
|
address: this.selectedPerson.address || '',
|
||||||
|
des: this.selectedPerson.des || '',
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'person', id });
|
const rowsResponse = await axios.post('/api/accounting/rows/search', { type: 'person', id });
|
||||||
this.items = rowsResponse.data;
|
this.items = rowsResponse.data;
|
||||||
|
|
@ -349,6 +450,29 @@ export default {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async savePersonChanges() {
|
||||||
|
if (!this.selectedPerson || !this.selectedPerson.code) {
|
||||||
|
this.snackbar = { show: true, text: 'شخص انتخاب نشده است', color: 'error' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.saveLoading = true;
|
||||||
|
try {
|
||||||
|
const response = await axios.post('/api/person/mod/' + this.selectedPerson.code, this.editPersonData);
|
||||||
|
if (response.data.Success) {
|
||||||
|
this.snackbar = { show: true, text: 'اطلاعات شخص با موفقیت بروزرسانی شد', color: 'success' };
|
||||||
|
this.editPersonDialog = false;
|
||||||
|
// بروزرسانی اطلاعات شخص
|
||||||
|
await this.loadPerson(this.selectedPerson.code);
|
||||||
|
} else {
|
||||||
|
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Save person error:', error);
|
||||||
|
this.snackbar = { show: true, text: 'خطا در بروزرسانی اطلاعات شخص', color: 'error' };
|
||||||
|
} finally {
|
||||||
|
this.saveLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
async excellOutput(allItems = true) {
|
async excellOutput(allItems = true) {
|
||||||
if (!allItems && this.itemsSelected.length === 0) {
|
if (!allItems && this.itemsSelected.length === 0) {
|
||||||
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
|
Swal.fire({ text: this.$t('pages.person_card.no_items_selected'), icon: 'info', confirmButtonText: this.$t('dialog.confirm') });
|
||||||
|
|
@ -429,6 +553,16 @@ export default {
|
||||||
return labels[type] || type;
|
return labels[type] || type;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
snackbar: {
|
||||||
|
get() {
|
||||||
|
return this.$store.state.snackbar;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$store.commit('setSnackbar', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@
|
||||||
<v-text-field v-model="person.des" :label="$t('pages.person.description')" dense
|
<v-text-field v-model="person.des" :label="$t('pages.person.description')" dense
|
||||||
prepend-inner-icon="mdi-text" hide-details />
|
prepend-inner-icon="mdi-text" hide-details />
|
||||||
</v-col>
|
</v-col>
|
||||||
|
|
||||||
</v-row>
|
</v-row>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
@ -287,7 +288,8 @@ export default {
|
||||||
types: [],
|
types: [],
|
||||||
accounts: [],
|
accounts: [],
|
||||||
prelabel: ref(null),
|
prelabel: ref(null),
|
||||||
speedAccess: false
|
speedAccess: false,
|
||||||
|
|
||||||
},
|
},
|
||||||
snackbar: {
|
snackbar: {
|
||||||
show: false,
|
show: false,
|
||||||
|
|
@ -346,6 +348,8 @@ export default {
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
axios.post('/api/person/info/' + id).then((response) => {
|
axios.post('/api/person/info/' + id).then((response) => {
|
||||||
|
console.log('Loaded person data:', response.data);
|
||||||
|
|
||||||
this.person = response.data;
|
this.person = response.data;
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
|
|
@ -404,6 +408,8 @@ export default {
|
||||||
if (canSubmit) {
|
if (canSubmit) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
try {
|
try {
|
||||||
|
console.log('Saving person data:', this.person);
|
||||||
|
|
||||||
const response = await axios.post('/api/person/mod/' + this.person.code, this.person);
|
const response = await axios.post('/api/person/mod/' + this.person.code, this.person);
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (response.data && response.data.result === 2) {
|
if (response.data && response.data.result === 2) {
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,13 @@
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
|
<v-tooltip text="تایید پرداختهای انتخابی" location="bottom">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon color="success" @click="approveSelectedPayments">
|
||||||
|
<v-icon>mdi-check-decagram</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
</v-tooltip>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template v-slot:activator="{ props }">
|
<template v-slot:activator="{ props }">
|
||||||
<v-btn v-bind="props" icon color="success">
|
<v-btn v-bind="props" icon color="success">
|
||||||
|
|
@ -434,6 +441,28 @@ const updateSelectedSum = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const approveSelectedPayments = async () => {
|
||||||
|
if (selectedItems.value.length === 0) {
|
||||||
|
Swal.fire({ text: 'هیچ آیتمی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const res = await Swal.fire({ title: 'تایید پرداختها', text: 'پرداختهای انتخابی تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' });
|
||||||
|
if (!res.isConfirmed) return;
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
for (const it of selectedItems.value) {
|
||||||
|
await axios.post(`/api/sell/payment/approve/${it.code}`);
|
||||||
|
}
|
||||||
|
Swal.fire({ text: 'پرداختها تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
|
||||||
|
selectedItems.value = [];
|
||||||
|
await loadData();
|
||||||
|
} catch (e) {
|
||||||
|
Swal.fire({ text: 'خطا در تایید پرداختها', icon: 'error', confirmButtonText: 'قبول' });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const print = async (allItems = true) => {
|
const print = async (allItems = true) => {
|
||||||
if (!allItems && selectedItems.value.length === 0) {
|
if (!allItems && selectedItems.value.length === 0) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
|
||||||
681
webUI/src/views/acc/plugins/import-workflow/list.vue
Normal file
681
webUI/src/views/acc/plugins/import-workflow/list.vue
Normal file
|
|
@ -0,0 +1,681 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-list">
|
||||||
|
<v-container fluid>
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<!-- <v-row>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div class="stats-card total-card">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-import</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-import</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.totalWorkflows || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">کل پروندهها</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('draft')" @click="filterByStatus('draft')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-file-document-outline</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-file-document-outline</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.draftWorkflows || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">پیشنویس</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('processing')" @click="filterByStatus('processing')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-progress-clock</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-progress-clock</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.processingWorkflows || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">در حال پردازش</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="6" sm="6" md="3">
|
||||||
|
<div :class="getCardClasses('completed')" @click="filterByStatus('completed')" role="button" tabindex="0">
|
||||||
|
<div class="stats-icon">
|
||||||
|
<v-icon size="24" color="white" class="d-sm-none">mdi-check-circle</v-icon>
|
||||||
|
<v-icon size="32" color="white" class="d-none d-sm-block">mdi-check-circle</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-number">
|
||||||
|
<v-progress-circular
|
||||||
|
v-if="statsLoading"
|
||||||
|
indeterminate
|
||||||
|
size="20"
|
||||||
|
color="white"
|
||||||
|
class="me-2"
|
||||||
|
></v-progress-circular>
|
||||||
|
{{ stats.completedWorkflows || 0 }}
|
||||||
|
</div>
|
||||||
|
<div class="stats-label">تکمیل شده</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row> -->
|
||||||
|
|
||||||
|
<!-- Data Table -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-data-table
|
||||||
|
:headers="headers"
|
||||||
|
:items="workflows"
|
||||||
|
:loading="loading"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template v-slot:top>
|
||||||
|
<!-- موبایل -->
|
||||||
|
<div class="d-block d-md-none pa-4">
|
||||||
|
<div class="d-flex gap-2 flex-column mb-3">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showCreateDialog = true"
|
||||||
|
size="small"
|
||||||
|
block
|
||||||
|
>
|
||||||
|
پرونده واردات جدید
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.search"
|
||||||
|
label="جستجو"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
@update:model-value="loadWorkflows"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
label="وضعیت"
|
||||||
|
:items="statusOptions"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
class="mb-3"
|
||||||
|
@update:model-value="loadWorkflows"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- دسکتاپ -->
|
||||||
|
<div class="d-none d-md-block">
|
||||||
|
<v-toolbar flat style="height: 70px !important; padding: 10px !important;">
|
||||||
|
<v-text-field
|
||||||
|
v-model="filters.search"
|
||||||
|
label="جستجو"
|
||||||
|
prepend-icon="mdi-magnify"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 250px;"
|
||||||
|
@update:model-value="loadWorkflows"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<v-select
|
||||||
|
v-model="filters.status"
|
||||||
|
label="وضعیت"
|
||||||
|
:items="statusOptions"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 200px;"
|
||||||
|
@update:model-value="loadWorkflows"
|
||||||
|
class="ml-2"
|
||||||
|
/>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
prepend-icon="mdi-plus"
|
||||||
|
@click="showCreateDialog = true"
|
||||||
|
>
|
||||||
|
پرونده واردات جدید
|
||||||
|
</v-btn>
|
||||||
|
</v-toolbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.status="{ item }">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(item.status)"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{{ getStatusText(item.status) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.totalAmount="{ item }">
|
||||||
|
<div>
|
||||||
|
{{ formatNumber(item.totalAmount) }}
|
||||||
|
<small class="text-medium-emphasis">{{ item.currency }}</small>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.dateSubmit="{ item }">
|
||||||
|
{{ formatDate(item.dateSubmit) }}
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-slot:item.actions="{ item }">
|
||||||
|
<v-menu>
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn
|
||||||
|
v-bind="props"
|
||||||
|
icon="mdi-menu"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
color="error"
|
||||||
|
></v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item @click="viewWorkflow(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="info">mdi-eye</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>مشاهده پرونده</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="editWorkflow(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="warning">mdi-pencil</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>ویرایش پرونده</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item @click="deleteWorkflow(item)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>حذف پرونده</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-container>
|
||||||
|
|
||||||
|
<!-- Create Dialog -->
|
||||||
|
<ImportWorkflowCreateDialog
|
||||||
|
v-model="showCreateDialog"
|
||||||
|
@created="onWorkflowCreated"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Dialog -->
|
||||||
|
<v-dialog v-model="showDeleteDialog" max-width="400">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>حذف پرونده واردات</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
آیا از حذف پرونده واردات "{{ selectedWorkflow?.title }}" اطمینان دارید؟
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn @click="showDeleteDialog = false">لغو</v-btn>
|
||||||
|
<v-btn color="error" @click="confirmDelete" :loading="deleteLoading">حذف</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
|
||||||
|
<!-- Snackbar for notifications -->
|
||||||
|
<v-snackbar
|
||||||
|
v-model="showSnackbar"
|
||||||
|
:color="snackbarColor"
|
||||||
|
:timeout="3000"
|
||||||
|
location="bottom"
|
||||||
|
class="rounded-lg"
|
||||||
|
elevation="2"
|
||||||
|
>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-icon :color="snackbarColor" class="me-2">
|
||||||
|
{{ snackbarColor === 'success' ? 'mdi-check-circle' : 'mdi-alert-circle' }}
|
||||||
|
</v-icon>
|
||||||
|
{{ snackbarText }}
|
||||||
|
</div>
|
||||||
|
<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, onMounted, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import ImportWorkflowCreateDialog from '../../../../components/plugins/import-workflow/ImportWorkflowCreateDialog.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const workflows = ref([])
|
||||||
|
const loading = ref(false)
|
||||||
|
const statsLoading = ref(false)
|
||||||
|
const showCreateDialog = ref(false)
|
||||||
|
const showDeleteDialog = ref(false)
|
||||||
|
const selectedWorkflow = ref(null)
|
||||||
|
const deleteLoading = ref(false)
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const stats = ref({
|
||||||
|
totalWorkflows: 0,
|
||||||
|
draftWorkflows: 0,
|
||||||
|
processingWorkflows: 0,
|
||||||
|
completedWorkflows: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Notifications
|
||||||
|
const showSnackbar = ref(false)
|
||||||
|
const snackbarText = ref('')
|
||||||
|
const snackbarColor = ref('success')
|
||||||
|
|
||||||
|
const showNotification = (text, color = 'success') => {
|
||||||
|
snackbarText.value = text
|
||||||
|
snackbarColor.value = color
|
||||||
|
showSnackbar.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
const filters = ref({
|
||||||
|
status: '',
|
||||||
|
search: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
limit: 20,
|
||||||
|
total: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
// Headers
|
||||||
|
const headers = [
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false },
|
||||||
|
{ title: 'کد', key: 'code', sortable: true },
|
||||||
|
{ title: 'عنوان', key: 'title', sortable: true },
|
||||||
|
{ title: 'تامین کننده', key: 'supplierName', sortable: true },
|
||||||
|
{ title: 'مبلغ کل', key: 'totalAmount', sortable: true },
|
||||||
|
{ title: 'وضعیت', key: 'status', sortable: true },
|
||||||
|
{ title: 'تاریخ ثبت', key: 'dateSubmit', sortable: true },
|
||||||
|
{ title: 'ثبت کننده', key: 'submitter', sortable: true }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Status options
|
||||||
|
const statusOptions = [
|
||||||
|
{ title: 'پیشنویس', value: 'draft' },
|
||||||
|
{ title: 'در حال پردازش', value: 'processing' },
|
||||||
|
{ title: 'ارسال شده', value: 'shipped' },
|
||||||
|
{ title: 'رسیده', value: 'arrived' },
|
||||||
|
{ title: 'ترخیص شده', value: 'cleared' },
|
||||||
|
{ title: 'تکمیل شده', value: 'completed' },
|
||||||
|
{ title: 'لغو شده', value: 'cancelled' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const loadWorkflows = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
page: pagination.value.page,
|
||||||
|
limit: pagination.value.limit
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.status) {
|
||||||
|
params.status = filters.value.status
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filters.value.search) {
|
||||||
|
params.search = filters.value.search
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await axios.get('/api/import-workflow/list', { params })
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
workflows.value = response.data.Result.data
|
||||||
|
pagination.value.total = response.data.Result.total
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading workflows:', error)
|
||||||
|
showNotification('در بارگذاری لیست پروندههای واردات خطایی رخ داد', 'error')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStats = async () => {
|
||||||
|
try {
|
||||||
|
statsLoading.value = true
|
||||||
|
const response = await axios.get('/api/import-workflow/stats')
|
||||||
|
if (response.data.Success) {
|
||||||
|
stats.value = response.data.Result
|
||||||
|
} else {
|
||||||
|
// Calculate stats from current data if API not available
|
||||||
|
const totalWorkflows = workflows.value.length
|
||||||
|
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
|
||||||
|
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
|
||||||
|
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
|
||||||
|
|
||||||
|
stats.value = {
|
||||||
|
totalWorkflows,
|
||||||
|
draftWorkflows,
|
||||||
|
processingWorkflows,
|
||||||
|
completedWorkflows
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('خطا در بارگذاری آمار:', error)
|
||||||
|
// Calculate stats from current data
|
||||||
|
const totalWorkflows = workflows.value.length
|
||||||
|
const draftWorkflows = workflows.value.filter(w => w.status === 'draft').length
|
||||||
|
const processingWorkflows = workflows.value.filter(w => w.status === 'processing').length
|
||||||
|
const completedWorkflows = workflows.value.filter(w => w.status === 'completed').length
|
||||||
|
|
||||||
|
stats.value = {
|
||||||
|
totalWorkflows,
|
||||||
|
draftWorkflows,
|
||||||
|
processingWorkflows,
|
||||||
|
completedWorkflows
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
statsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePagination = (options) => {
|
||||||
|
pagination.value.page = options.page
|
||||||
|
pagination.value.limit = options.itemsPerPage
|
||||||
|
loadWorkflows()
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterByStatus = (status) => {
|
||||||
|
filters.value.status = filters.value.status === status ? '' : status
|
||||||
|
loadWorkflows()
|
||||||
|
}
|
||||||
|
|
||||||
|
const viewWorkflow = (workflow) => {
|
||||||
|
router.push(`/acc/plugins/import-workflow/${workflow.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const editWorkflow = (workflow) => {
|
||||||
|
router.push(`/acc/plugins/import-workflow/${workflow.id}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteWorkflow = (workflow) => {
|
||||||
|
selectedWorkflow.value = workflow
|
||||||
|
showDeleteDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
deleteLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.delete(`/api/import-workflow/${selectedWorkflow.value.id}/delete`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
showNotification('پرونده واردات با موفقیت حذف شد')
|
||||||
|
loadWorkflows()
|
||||||
|
// loadStats()
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting workflow:', error)
|
||||||
|
showNotification('در حذف پرونده واردات خطایی رخ داد', 'error')
|
||||||
|
} finally {
|
||||||
|
deleteLoading.value = false
|
||||||
|
showDeleteDialog.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onWorkflowCreated = () => {
|
||||||
|
showCreateDialog.value = false
|
||||||
|
loadWorkflows()
|
||||||
|
// loadStats()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
draft: 'grey',
|
||||||
|
processing: 'blue',
|
||||||
|
shipped: 'orange',
|
||||||
|
arrived: 'purple',
|
||||||
|
cleared: 'teal',
|
||||||
|
completed: 'green',
|
||||||
|
cancelled: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
draft: 'پیشنویس',
|
||||||
|
processing: 'در حال پردازش',
|
||||||
|
shipped: 'ارسال شده',
|
||||||
|
arrived: 'رسیده',
|
||||||
|
cleared: 'ترخیص شده',
|
||||||
|
completed: 'تکمیل شده',
|
||||||
|
cancelled: 'لغو شده'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const getCardClasses = (status) => {
|
||||||
|
const baseClasses = 'stats-card'
|
||||||
|
const statusClasses = {
|
||||||
|
'draft': 'draft-card',
|
||||||
|
'processing': 'processing-card',
|
||||||
|
'completed': 'completed-card'
|
||||||
|
}
|
||||||
|
|
||||||
|
const classes = [baseClasses, statusClasses[status]]
|
||||||
|
|
||||||
|
if (filters.value.status === status) {
|
||||||
|
classes.push('active-filter')
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (number) => {
|
||||||
|
if (!number) return '0'
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
// loadStats(),
|
||||||
|
loadWorkflows()
|
||||||
|
])
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-list {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.v-data-table {
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table-header th) {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table td) {
|
||||||
|
padding: 12px 16px !important;
|
||||||
|
border-bottom: 1px solid #e0e0e0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-data-table__wrapper table tr:hover) {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.v-chip) {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-header {
|
||||||
|
background-color: #f5f5f5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
position: relative;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
color: white;
|
||||||
|
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 120px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:focus {
|
||||||
|
outline: 2px solid rgba(255, 255, 255, 0.5);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.active-filter {
|
||||||
|
transform: translateY(-4px) scale(1.02);
|
||||||
|
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.2);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card.active-filter::before {
|
||||||
|
background: linear-gradient(45deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(45deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
|
||||||
|
border-radius: 16px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card:hover {
|
||||||
|
transform: translateY(-8px) scale(1.01);
|
||||||
|
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-number {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
opacity: 0.9;
|
||||||
|
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card Variants - Professional Colors */
|
||||||
|
.total-card {
|
||||||
|
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draft-card {
|
||||||
|
background: linear-gradient(135deg, #95a5a6 0%, #7f8c8d 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.processing-card {
|
||||||
|
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.completed-card {
|
||||||
|
background: linear-gradient(135deg, #27ae60 0%, #2ecc71 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-card {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
821
webUI/src/views/acc/plugins/import-workflow/view.vue
Normal file
821
webUI/src/views/acc/plugins/import-workflow/view.vue
Normal file
|
|
@ -0,0 +1,821 @@
|
||||||
|
<template>
|
||||||
|
<div class="import-workflow-detail">
|
||||||
|
<v-container fluid>
|
||||||
|
<!-- Header -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title class="d-flex align-center justify-space-between">
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-btn
|
||||||
|
icon="mdi-arrow-right"
|
||||||
|
variant="text"
|
||||||
|
@click="$router.back()"
|
||||||
|
class="ml-2"
|
||||||
|
></v-btn>
|
||||||
|
<v-icon class="ml-2" color="primary">mdi-import</v-icon>
|
||||||
|
<span>{{ workflow?.title || 'جزئیات پرونده واردات' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-center">
|
||||||
|
<v-chip
|
||||||
|
:color="getStatusColor(workflow?.status)"
|
||||||
|
class="ml-2"
|
||||||
|
>
|
||||||
|
{{ getStatusText(workflow?.status) }}
|
||||||
|
</v-chip>
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
prepend-icon="mdi-pencil"
|
||||||
|
@click="editMode = !editMode"
|
||||||
|
>
|
||||||
|
{{ editMode ? 'لغو ویرایش' : 'ویرایش' }}
|
||||||
|
</v-btn>
|
||||||
|
<!-- <v-btn
|
||||||
|
class="mr-2"
|
||||||
|
color="success"
|
||||||
|
prepend-icon="mdi-warehouse"
|
||||||
|
@click="openCreateTicketDialog"
|
||||||
|
>
|
||||||
|
ایجاد حواله ورود از پرونده
|
||||||
|
</v-btn> -->
|
||||||
|
</div>
|
||||||
|
</v-card-title>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<v-row v-if="loading">
|
||||||
|
<v-col cols="12" class="text-center">
|
||||||
|
<v-progress-circular indeterminate color="primary"></v-progress-circular>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<template v-else-if="workflow">
|
||||||
|
<!-- Basic Info -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8">
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>اطلاعات کلی</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-form v-if="editMode" ref="form" v-model="valid" validate-on="input">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editData.title"
|
||||||
|
label="عنوان پرونده"
|
||||||
|
:rules="[rules.required, rules.minLength]"
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editData.supplierName"
|
||||||
|
label="نام تامین کننده"
|
||||||
|
:rules="[rules.required, rules.minLength]"
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editData.supplierCountry"
|
||||||
|
label="کشور تامین کننده"
|
||||||
|
:rules="[rules.maxLength]"
|
||||||
|
counter="50"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editData.supplierPhone"
|
||||||
|
label="تلفن تامین کننده"
|
||||||
|
:rules="[rules.phone]"
|
||||||
|
counter="20"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
v-model="editData.supplierEmail"
|
||||||
|
label="ایمیل تامین کننده"
|
||||||
|
:rules="[rules.email]"
|
||||||
|
counter="100"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="editData.supplierAddress"
|
||||||
|
label="آدرس تامین کننده"
|
||||||
|
rows="2"
|
||||||
|
:rules="[rules.maxLength]"
|
||||||
|
counter="500"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(editData.totalAmount)"
|
||||||
|
label="مبلغ کل (ارزی)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.positiveMoney]"
|
||||||
|
@update:modelValue="onMoneyInput('totalAmount', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="editData.currency"
|
||||||
|
:items="currencyOptions"
|
||||||
|
label="واحد پول"
|
||||||
|
:rules="[rules.required]"
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-text-field
|
||||||
|
class="ltr-input"
|
||||||
|
:model-value="formatMoney(editData.exchangeRate)"
|
||||||
|
label="نرخ تبدیل (ریال)"
|
||||||
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
:rules="[rules.exchangeRateRule]"
|
||||||
|
@update:modelValue="onMoneyInput('exchangeRate', $event)"
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-text-field
|
||||||
|
:model-value="formatMoney(editData.totalAmountIRR)"
|
||||||
|
label="مبلغ کل (ریال)"
|
||||||
|
readonly
|
||||||
|
></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-textarea
|
||||||
|
v-model="editData.description"
|
||||||
|
label="توضیحات"
|
||||||
|
rows="3"
|
||||||
|
></v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
@click="saveChanges"
|
||||||
|
:loading="saveLoading"
|
||||||
|
:disabled="!isFormValidForSave"
|
||||||
|
>
|
||||||
|
ذخیره تغییرات
|
||||||
|
</v-btn>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-form>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>نام تامین کننده:</strong>
|
||||||
|
<div>{{ workflow.supplierName || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>کشور تامین کننده:</strong>
|
||||||
|
<div>{{ workflow.supplierCountry || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>تلفن تامین کننده:</strong>
|
||||||
|
<div>{{ workflow.supplierPhone || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>ایمیل تامین کننده:</strong>
|
||||||
|
<div>{{ workflow.supplierEmail || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>آدرس تامین کننده:</strong>
|
||||||
|
<div>{{ workflow.supplierAddress || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>مبلغ کل:</strong>
|
||||||
|
<div>{{ formatMoney(workflow.totalAmount) }} {{ workflow.currency }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>نرخ تبدیل:</strong>
|
||||||
|
<div>{{ formatMoney(workflow.exchangeRate) }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>مبلغ کل (ریال):</strong>
|
||||||
|
<div>{{ formatMoney(workflow.totalAmountIRR) }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>توضیحات:</strong>
|
||||||
|
<div>{{ workflow.description || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
|
||||||
|
<v-col cols="12" md="4">
|
||||||
|
<v-card class="mb-4">
|
||||||
|
<v-card-title>اطلاعات سیستمی</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>کد پرونده:</strong>
|
||||||
|
<div>{{ workflow.code }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>تاریخ ثبت:</strong>
|
||||||
|
<div>{{ formatDate(workflow.dateSubmit) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>ثبت کننده:</strong>
|
||||||
|
<div>{{ workflow.submitter }}</div>
|
||||||
|
</div>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card>
|
||||||
|
<v-tabs v-model="activeTab" bg-color="primary">
|
||||||
|
<v-tab value="items">آیتمها <v-chip size="small" color="secondary" variant="tonal" class="ms-2" style="color: white !important;">{{ workflow.items?.length || 0 }}</v-chip></v-tab>
|
||||||
|
<v-tab value="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">
|
||||||
|
<v-tabs-window-item value="items">
|
||||||
|
<ImportWorkflowItems
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:items="workflow.items"
|
||||||
|
:currency="workflow.currency"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="payments">
|
||||||
|
<ImportWorkflowPayments
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:payments="workflow.payments"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="documents">
|
||||||
|
<ImportWorkflowDocuments
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:documents="workflow.documents"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="stages">
|
||||||
|
<ImportWorkflowStages
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:stages="workflow.stages"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="shipping">
|
||||||
|
<ImportWorkflowShipping
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:shipping="workflow.shipping"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="customs">
|
||||||
|
<ImportWorkflowCustoms
|
||||||
|
:workflow-id="workflowId"
|
||||||
|
:customs="workflow.customs"
|
||||||
|
@updated="loadWorkflow"
|
||||||
|
/>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
|
||||||
|
<v-tabs-window-item value="tickets">
|
||||||
|
<v-card flat>
|
||||||
|
<v-card-text>
|
||||||
|
<div class="d-flex align-center mb-4 gap-2">
|
||||||
|
<v-select
|
||||||
|
v-model="ticketsStatusFilter"
|
||||||
|
:items="ticketStatusOptions"
|
||||||
|
label="فیلتر وضعیت"
|
||||||
|
style="max-width: 260px"
|
||||||
|
clearable
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
@update:model-value="loadRelatedTickets"
|
||||||
|
/>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-btn color="primary" variant="text" icon="mdi-refresh" @click="loadRelatedTickets" :loading="loadingTickets" />
|
||||||
|
</div>
|
||||||
|
<v-data-table
|
||||||
|
:headers="ticketsHeaders"
|
||||||
|
:header-props="{ class: 'custom-header' }"
|
||||||
|
:items="relatedTickets"
|
||||||
|
:loading="loadingTickets"
|
||||||
|
density="comfortable"
|
||||||
|
class="elevation-1"
|
||||||
|
>
|
||||||
|
<template #item.code="{ item }">
|
||||||
|
<v-chip color="secondary" variant="tonal" size="small">{{ item.code }}</v-chip>
|
||||||
|
</template>
|
||||||
|
<template #item.status="{ item }">
|
||||||
|
<v-chip :color="ticketStatusColor(item.status)" size="small">
|
||||||
|
{{ ticketStatusLabel(item.status) }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
<template #item.actions="{ item }">
|
||||||
|
<v-btn
|
||||||
|
color="primary"
|
||||||
|
variant="text"
|
||||||
|
size="small"
|
||||||
|
@click="$router.push({ name: 'storeroom_ticket_view', params: { id: item.code } })"
|
||||||
|
>
|
||||||
|
مشاهده
|
||||||
|
</v-btn>
|
||||||
|
<v-menu>
|
||||||
|
<template #activator="{ props }">
|
||||||
|
<v-btn v-bind="props" icon variant="text" size="small">
|
||||||
|
<v-icon>mdi-dots-vertical</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item v-for="st in ticketStatusOptions" :key="st.value" @click="updateTicketStatus(item.code, st.value)" :disabled="!st.value">
|
||||||
|
<v-list-item-title>{{ st.title }}</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-menu>
|
||||||
|
</template>
|
||||||
|
</v-data-table>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-tabs-window-item>
|
||||||
|
</v-tabs-window>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Dialog: Create inbound storeroom ticket -->
|
||||||
|
<v-dialog v-model="showCreateTicketDialog" max-width="600">
|
||||||
|
<v-card>
|
||||||
|
<v-card-title>ایجاد حواله ورود به انبار</v-card-title>
|
||||||
|
<v-card-text>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-select
|
||||||
|
v-model="selectedStoreroomId"
|
||||||
|
:items="storerooms"
|
||||||
|
item-title="name"
|
||||||
|
item-value="id"
|
||||||
|
label="انبار"
|
||||||
|
:loading="loadingStorerooms"
|
||||||
|
:disabled="loadingStorerooms"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12">
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<v-text-field
|
||||||
|
v-model="personSearch"
|
||||||
|
label="جستجوی طرفحساب"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
/>
|
||||||
|
<v-btn color="primary" @click="searchPersons" :loading="loadingPersons">جستجو</v-btn>
|
||||||
|
</div>
|
||||||
|
<v-select
|
||||||
|
class="mt-3"
|
||||||
|
v-model="selectedPersonId"
|
||||||
|
:items="persons"
|
||||||
|
item-title="nikename"
|
||||||
|
item-value="id"
|
||||||
|
label="انتخاب طرفحساب"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
/>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-text>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn @click="showCreateTicketDialog = false">انصراف</v-btn>
|
||||||
|
<v-btn color="success" @click="createInboundTicket" :loading="creatingTicket" :disabled="!canCreateTicket">ایجاد حواله</v-btn>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-container>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, watch, nextTick, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import axios from 'axios'
|
||||||
|
import Swal from 'sweetalert2'
|
||||||
|
import ImportWorkflowItems from '../../../../components/plugins/import-workflow/ImportWorkflowItems.vue'
|
||||||
|
import ImportWorkflowPayments from '../../../../components/plugins/import-workflow/ImportWorkflowPayments.vue'
|
||||||
|
import ImportWorkflowDocuments from '../../../../components/plugins/import-workflow/ImportWorkflowDocuments.vue'
|
||||||
|
import ImportWorkflowStages from '../../../../components/plugins/import-workflow/ImportWorkflowStages.vue'
|
||||||
|
import ImportWorkflowShipping from '../../../../components/plugins/import-workflow/ImportWorkflowShipping.vue'
|
||||||
|
import ImportWorkflowCustoms from '../../../../components/plugins/import-workflow/ImportWorkflowCustoms.vue'
|
||||||
|
|
||||||
|
const route = useRoute()
|
||||||
|
const workflowId = route.params.id
|
||||||
|
|
||||||
|
// Data
|
||||||
|
const workflow = ref(null)
|
||||||
|
const form = ref(null)
|
||||||
|
const loading = ref(false)
|
||||||
|
const editMode = ref(false)
|
||||||
|
const valid = ref(false)
|
||||||
|
const saveLoading = ref(false)
|
||||||
|
const activeTab = ref('items')
|
||||||
|
// Create inbound ticket dialog
|
||||||
|
const showCreateTicketDialog = ref(false)
|
||||||
|
const storerooms = ref([])
|
||||||
|
const loadingStorerooms = ref(false)
|
||||||
|
const selectedStoreroomId = ref(null)
|
||||||
|
const personSearch = ref('')
|
||||||
|
const persons = ref([])
|
||||||
|
const loadingPersons = ref(false)
|
||||||
|
const selectedPersonId = ref(null)
|
||||||
|
const creatingTicket = ref(false)
|
||||||
|
|
||||||
|
const ticketsHeaders = [
|
||||||
|
{ title: 'کد حواله', key: 'code' },
|
||||||
|
{ title: 'تاریخ', key: 'date' },
|
||||||
|
{ title: 'نوع', key: 'typeString' },
|
||||||
|
{ title: 'وضعیت', key: 'status' },
|
||||||
|
{ title: 'انبار', key: 'storeroom' },
|
||||||
|
{ title: 'طرف حساب', key: 'person' },
|
||||||
|
{ title: 'عملیات', key: 'actions', sortable: false },
|
||||||
|
]
|
||||||
|
const ticketStatusOptions = [
|
||||||
|
{ title: 'همه', value: null },
|
||||||
|
{ title: 'در جریان', value: 'in_progress' },
|
||||||
|
{ title: 'تایید شده', value: 'approved' },
|
||||||
|
{ title: 'انجام شده', value: 'done' },
|
||||||
|
{ title: 'رد شده', value: 'rejected' },
|
||||||
|
{ title: 'در انتظار تایید', value: 'pending_approval' },
|
||||||
|
]
|
||||||
|
const ticketsStatusFilter = ref(null)
|
||||||
|
const relatedTickets = ref([])
|
||||||
|
const loadingTickets = ref(false)
|
||||||
|
|
||||||
|
const canCreateTicket = computed(() => !!selectedStoreroomId.value && !!selectedPersonId.value)
|
||||||
|
|
||||||
|
const openCreateTicketDialog = async () => {
|
||||||
|
showCreateTicketDialog.value = true
|
||||||
|
await loadStorerooms()
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadStorerooms = async () => {
|
||||||
|
try {
|
||||||
|
loadingStorerooms.value = true
|
||||||
|
const res = await axios.get('/api/storeroom/list/active')
|
||||||
|
// انتظار میرود سرورها آرایهای از انبارها بدهد
|
||||||
|
storerooms.value = Array.isArray(res.data?.data) ? res.data.data : (Array.isArray(res.data) ? res.data : [])
|
||||||
|
} catch (e) {
|
||||||
|
storerooms.value = []
|
||||||
|
} finally {
|
||||||
|
loadingStorerooms.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchPersons = async () => {
|
||||||
|
try {
|
||||||
|
loadingPersons.value = true
|
||||||
|
const res = await axios.post('/api/person/list/search', { search: personSearch.value || '' })
|
||||||
|
persons.value = Array.isArray(res.data) ? res.data : (Array.isArray(res.data?.Result) ? res.data.Result : [])
|
||||||
|
} catch (e) {
|
||||||
|
persons.value = []
|
||||||
|
} finally {
|
||||||
|
loadingPersons.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const createInboundTicket = async () => {
|
||||||
|
if (!canCreateTicket.value || !workflow.value) return
|
||||||
|
try {
|
||||||
|
creatingTicket.value = true
|
||||||
|
const res = await axios.post(`/api/import-workflow/${workflow.value.code}/create-inbound-ticket`, {
|
||||||
|
storeroom_id: selectedStoreroomId.value,
|
||||||
|
person_id: selectedPersonId.value,
|
||||||
|
})
|
||||||
|
if (res.data?.Success) {
|
||||||
|
Swal.fire({ title: 'موفق', text: `حواله با کد ${res.data.Result.ticketCode} ایجاد شد`, icon: 'success' })
|
||||||
|
showCreateTicketDialog.value = false
|
||||||
|
await loadRelatedTickets()
|
||||||
|
} else {
|
||||||
|
throw new Error(res.data?.ErrorMessage || 'خطا در ایجاد حواله')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Swal.fire({ title: 'خطا', text: 'ایجاد حواله ناموفق بود', icon: 'error' })
|
||||||
|
} finally {
|
||||||
|
creatingTicket.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadRelatedTickets = async () => {
|
||||||
|
if (!workflow.value) return
|
||||||
|
try {
|
||||||
|
loadingTickets.value = true
|
||||||
|
const qs = ticketsStatusFilter.value ? `?status=${ticketsStatusFilter.value}` : ''
|
||||||
|
const res = await axios.get(`/api/storeroom/tickets${qs}`)
|
||||||
|
const items = Array.isArray(res.data) ? res.data : []
|
||||||
|
relatedTickets.value = items.filter(t => t.importWorkflowCode === workflow.value.code)
|
||||||
|
} catch (e) {
|
||||||
|
relatedTickets.value = []
|
||||||
|
} finally {
|
||||||
|
loadingTickets.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTicketStatus = async (code, status) => {
|
||||||
|
try {
|
||||||
|
await axios.post(`/api/storeroom/ticket/status/${code}`, { status })
|
||||||
|
await loadRelatedTickets()
|
||||||
|
Swal.fire({ title: 'موفق', text: 'وضعیت حواله بهروزرسانی شد', icon: 'success' })
|
||||||
|
} catch (e) {
|
||||||
|
Swal.fire({ title: 'خطا', text: 'بهروزرسانی وضعیت ناموفق بود', icon: 'error' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editData = ref({})
|
||||||
|
|
||||||
|
// Currency options
|
||||||
|
const currencyOptions = [
|
||||||
|
{ title: 'دلار آمریکا (USD)', value: 'USD' },
|
||||||
|
{ title: 'یورو (EUR)', value: 'EUR' },
|
||||||
|
{ title: 'پوند انگلیس (GBP)', value: 'GBP' },
|
||||||
|
{ title: 'یوان چین (CNY)', value: 'CNY' },
|
||||||
|
{ title: 'درهم امارات (AED)', value: 'AED' },
|
||||||
|
{ title: 'ریال (IRR)', value: 'IRR' }
|
||||||
|
]
|
||||||
|
|
||||||
|
// Validation rules
|
||||||
|
const rules = {
|
||||||
|
required: (value) => !!value || 'این فیلد الزامی است',
|
||||||
|
positive: (value) => !value || parseFloat(value) > 0 || 'مقدار باید مثبت باشد',
|
||||||
|
minLength: (value) => !value || value.length >= 3 || 'حداقل 3 کاراکتر الزامی است',
|
||||||
|
maxLength: (value) => !value || value.length <= 1000 || 'حداکثر 1000 کاراکتر مجاز است',
|
||||||
|
phone: (value) => !value || /^[\d\-\+\(\)\s]+$/.test(value) || 'شماره تلفن معتبر وارد کنید',
|
||||||
|
email: (value) => !value || /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) || 'ایمیل معتبر وارد کنید',
|
||||||
|
positiveMoney: (value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
return numeric > 0 || 'مقدار باید مثبت باشد'
|
||||||
|
},
|
||||||
|
exchangeRateRule: (value) => {
|
||||||
|
// اگر واحد پول IRR باشد، خالی بودن یا 1 بودن نرخ تبدیل را مجاز کن تا فرم معتبر بماند
|
||||||
|
const currentCurrency = editData.value?.currency
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
if (currentCurrency === 'IRR') {
|
||||||
|
return numeric >= 0 || true
|
||||||
|
}
|
||||||
|
return numeric > 0 || 'نرخ تبدیل باید بزرگتر از صفر باشد'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for total amount and exchange rate changes
|
||||||
|
watch(
|
||||||
|
() => [editData.value.totalAmount, editData.value.exchangeRate, editData.value.currency],
|
||||||
|
([totalAmount, exchangeRate, currency]) => {
|
||||||
|
const total = parseFloat(totalAmount) || 0
|
||||||
|
const computedRate = currency === 'IRR' ? 1 : (parseFloat(exchangeRate) || 0)
|
||||||
|
const result = Math.round(total * computedRate)
|
||||||
|
editData.value.totalAmountIRR = isNaN(result) ? 0 : result
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const loadWorkflow = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`/api/import-workflow/${workflowId}`)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
workflow.value = response.data.Result
|
||||||
|
editData.value = { ...response.data.Result }
|
||||||
|
|
||||||
|
// Trigger the watch manually after setting editData
|
||||||
|
// مقدار ریالی بر اساس مبلغ و نرخ تبدیل/واحد پول محاسبه میشود
|
||||||
|
const total = parseFloat(editData.value.totalAmount) || 0
|
||||||
|
const rate = editData.value.currency === 'IRR' ? 1 : (parseFloat(editData.value.exchangeRate) || 0)
|
||||||
|
editData.value.totalAmountIRR = Math.round(total * rate)
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading workflow:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در بارگذاری جزئیات پرونده واردات خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveChanges = async () => {
|
||||||
|
if (!isFormValidForSave.value) return
|
||||||
|
|
||||||
|
saveLoading.value = true
|
||||||
|
try {
|
||||||
|
const response = await axios.put(`/api/import-workflow/${workflowId}/update`, editData.value)
|
||||||
|
|
||||||
|
if (response.data.Success) {
|
||||||
|
Swal.fire({
|
||||||
|
title: 'موفق',
|
||||||
|
text: 'تغییرات با موفقیت ذخیره شد',
|
||||||
|
icon: 'success'
|
||||||
|
})
|
||||||
|
editMode.value = false
|
||||||
|
loadWorkflow()
|
||||||
|
} else {
|
||||||
|
throw new Error(response.data.ErrorMessage)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving changes:', error)
|
||||||
|
Swal.fire({
|
||||||
|
title: 'خطا',
|
||||||
|
text: 'در ذخیره تغییرات خطایی رخ داد',
|
||||||
|
icon: 'error'
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
saveLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utilities
|
||||||
|
const parseMoneyInput = (val) => {
|
||||||
|
if (val === null || val === undefined) return 0
|
||||||
|
const cleaned = String(val).replace(/,/g, '').replace(/[^\d.-]/g, '')
|
||||||
|
const num = Number(cleaned)
|
||||||
|
return Number.isFinite(num) ? num : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// دکمه ذخیره تنها زمانی فعال شود که فیلدهای کلیدی معتبر باشند
|
||||||
|
const isFormValidForSave = computed(() => {
|
||||||
|
const titleOk = typeof editData.value.title === 'string' && editData.value.title.trim().length >= 3
|
||||||
|
const supplierOk = typeof editData.value.supplierName === 'string' && editData.value.supplierName.trim().length >= 3
|
||||||
|
const currencyOk = !!editData.value.currency
|
||||||
|
const total = parseMoneyInput(editData.value.totalAmount)
|
||||||
|
const rate = parseMoneyInput(editData.value.exchangeRate)
|
||||||
|
const totalOk = total >= 0
|
||||||
|
const rateOk = editData.value.currency === 'IRR' ? rate >= 0 : rate > 0
|
||||||
|
return titleOk && supplierOk && currencyOk && totalOk && rateOk && valid.value
|
||||||
|
})
|
||||||
|
|
||||||
|
const onMoneyInput = (field, value) => {
|
||||||
|
const numeric = parseMoneyInput(value)
|
||||||
|
editData.value[field] = numeric
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusColor = (status) => {
|
||||||
|
const colors = {
|
||||||
|
draft: 'grey',
|
||||||
|
processing: 'blue',
|
||||||
|
shipped: 'orange',
|
||||||
|
arrived: 'purple',
|
||||||
|
cleared: 'teal',
|
||||||
|
completed: 'green',
|
||||||
|
cancelled: 'red'
|
||||||
|
}
|
||||||
|
return colors[status] || 'grey'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStatusText = (status) => {
|
||||||
|
const texts = {
|
||||||
|
draft: 'پیشنویس',
|
||||||
|
processing: 'در حال پردازش',
|
||||||
|
shipped: 'ارسال شده',
|
||||||
|
arrived: 'رسیده',
|
||||||
|
cleared: 'ترخیص شده',
|
||||||
|
completed: 'تکمیل شده',
|
||||||
|
cancelled: 'لغو شده'
|
||||||
|
}
|
||||||
|
return texts[status] || status
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = (number) => {
|
||||||
|
if (!number) return '0'
|
||||||
|
return new Intl.NumberFormat('fa-IR').format(number)
|
||||||
|
}
|
||||||
|
|
||||||
|
// نمایش مبالغ بدون اعشار و با جداکننده ویرگول بین هر سه رقم
|
||||||
|
const formatMoney = (value) => {
|
||||||
|
const numericValue = Number(value) || 0
|
||||||
|
return numericValue
|
||||||
|
.toFixed(0)
|
||||||
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
if (!date) return '-'
|
||||||
|
return new Date(date).toLocaleDateString('fa-IR')
|
||||||
|
}
|
||||||
|
|
||||||
|
// استایل وضعیت حوالهها
|
||||||
|
const ticketStatusLabel = (status) => {
|
||||||
|
switch (status) {
|
||||||
|
case 'approved': return 'تایید شده'
|
||||||
|
case 'pending_approval': return 'در انتظار تایید'
|
||||||
|
case 'in_progress': return 'در حال انجام'
|
||||||
|
case 'done': return 'انجام شده'
|
||||||
|
case 'rejected': return 'رد شده'
|
||||||
|
default: return status || '-'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const ticketStatusColor = (status) => {
|
||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watch for edit mode changes
|
||||||
|
watch(editMode, async (newVal) => {
|
||||||
|
if (newVal && workflow.value) {
|
||||||
|
editData.value = { ...workflow.value }
|
||||||
|
await nextTick()
|
||||||
|
if (form.value && typeof form.value.validate === 'function') {
|
||||||
|
form.value.validate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadWorkflow()
|
||||||
|
await loadRelatedTickets()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.import-workflow-detail {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ltr-input :deep(input) {
|
||||||
|
direction: ltr !important;
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -39,6 +39,12 @@
|
||||||
</v-btn>
|
</v-btn>
|
||||||
</template>
|
</template>
|
||||||
<v-list>
|
<v-list>
|
||||||
|
<v-list-subheader color="primary">وضعیت تایید</v-list-subheader>
|
||||||
|
<v-list-item class="text-dark" title="تایید فاکتورهای انتخابی" @click="approveSelectedInvoices">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="green" icon="mdi-check-decagram"></v-icon>
|
||||||
|
</template>
|
||||||
|
</v-list-item>
|
||||||
<v-list-subheader color="primary">{{ $t('dialog.change_labels') }}</v-list-subheader>
|
<v-list-subheader color="primary">{{ $t('dialog.change_labels') }}</v-list-subheader>
|
||||||
<v-list-item v-for="item in types" class="text-dark" :title="$t('dialog.change_to') + ' ' + item.label" @click="changeLabel(item)">
|
<v-list-item v-for="item in types" class="text-dark" :title="$t('dialog.change_to') + ' ' + item.label" @click="changeLabel(item)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
|
|
@ -59,6 +65,13 @@
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
</v-toolbar>
|
</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>
|
<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>
|
<template v-slot:prepend-inner>
|
||||||
<v-tooltip location="bottom" :text="$t('dialog.search')">
|
<v-tooltip location="bottom" :text="$t('dialog.search')">
|
||||||
|
|
@ -99,8 +112,8 @@
|
||||||
v-model:items-per-page="serverOptions.rowsPerPage"
|
v-model:items-per-page="serverOptions.rowsPerPage"
|
||||||
v-model:page="serverOptions.page"
|
v-model:page="serverOptions.page"
|
||||||
:headers="visibleHeaders"
|
:headers="visibleHeaders"
|
||||||
:items="items"
|
:items="displayItems"
|
||||||
:items-length="total"
|
:items-length="displayTotal"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:no-data-text="$t('table.no_data')"
|
:no-data-text="$t('table.no_data')"
|
||||||
v-model="itemsSelected"
|
v-model="itemsSelected"
|
||||||
|
|
@ -148,6 +161,11 @@
|
||||||
<v-icon color="orange" icon="mdi-cloud-upload"></v-icon>
|
<v-icon color="orange" icon="mdi-cloud-upload"></v-icon>
|
||||||
</template>
|
</template>
|
||||||
</v-list-item>
|
</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)">
|
<v-list-item class="text-dark" :title="$t('dialog.edit')" @click="canEditItem(item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon icon="mdi-file-edit"></v-icon>
|
<v-icon icon="mdi-file-edit"></v-icon>
|
||||||
|
|
@ -206,6 +224,14 @@
|
||||||
</router-link>
|
</router-link>
|
||||||
<span v-else>-</span>
|
<span v-else>-</span>
|
||||||
</template>
|
</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 }">
|
<template v-slot:item.code="{ item }">
|
||||||
<router-link :to="'/acc/sell/view/' + item.code">
|
<router-link :to="'/acc/sell/view/' + item.code">
|
||||||
{{ item.code }}
|
{{ item.code }}
|
||||||
|
|
@ -350,6 +376,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
let self = this;
|
let self = this;
|
||||||
return {
|
return {
|
||||||
|
currentTab: 'approved',
|
||||||
paperSizes: [
|
paperSizes: [
|
||||||
{ title: self.$t('dialog.a4p'), value: 'A4' },
|
{ title: self.$t('dialog.a4p'), value: 'A4' },
|
||||||
{ title: self.$t('dialog.a4l'), value: 'A4-L' },
|
{ title: self.$t('dialog.a4l'), value: 'A4-L' },
|
||||||
|
|
@ -369,6 +396,8 @@ export default defineComponent({
|
||||||
invoiceIndex: true
|
invoiceIndex: true
|
||||||
},
|
},
|
||||||
plugins: {},
|
plugins: {},
|
||||||
|
business: { requireTwoStepApproval: false, invoiceApprover: null },
|
||||||
|
currentUser: { email: '', owner: false },
|
||||||
sumSelected: 0,
|
sumSelected: 0,
|
||||||
sumSelectedProfit: 0,
|
sumSelectedProfit: 0,
|
||||||
sumTotal: 0,
|
sumTotal: 0,
|
||||||
|
|
@ -378,7 +407,11 @@ export default defineComponent({
|
||||||
loading: false,
|
loading: false,
|
||||||
bulkLoading: false,
|
bulkLoading: false,
|
||||||
items: [],
|
items: [],
|
||||||
|
itemsApproved: [],
|
||||||
|
itemsPending: [],
|
||||||
total: 0,
|
total: 0,
|
||||||
|
totalApproved: 0,
|
||||||
|
totalPending: 0,
|
||||||
expanded: [],
|
expanded: [],
|
||||||
serverOptions: reactive({
|
serverOptions: reactive({
|
||||||
page: 1,
|
page: 1,
|
||||||
|
|
@ -391,6 +424,8 @@ export default defineComponent({
|
||||||
{ title: "فاکتور", value: "code", sortable: true, visible: true, width: 120 },
|
{ title: "فاکتور", value: "code", sortable: true, visible: true, width: 120 },
|
||||||
{ title: "تاریخ", value: "date", 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: "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: "discountAll", sortable: true, visible: true, width: 120 },
|
||||||
{ title: "حمل و نقل", value: "transferCost", sortable: true, visible: true, width: 120 },
|
{ title: "حمل و نقل", value: "transferCost", sortable: true, visible: true, width: 120 },
|
||||||
{ title: "مبلغ", value: "amount", sortable: true, visible: true, width: 150 },
|
{ title: "مبلغ", value: "amount", sortable: true, visible: true, width: 150 },
|
||||||
|
|
@ -412,13 +447,50 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
visibleHeaders() {
|
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() {
|
tableHeight() {
|
||||||
return window.innerHeight - 200;
|
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: {
|
methods: {
|
||||||
|
approveSelectedInvoices() {
|
||||||
|
if (this.itemsSelected.length === 0) {
|
||||||
|
Swal.fire({ text: 'هیچ موردی انتخاب نشده است.', icon: 'warning', confirmButtonText: 'قبول' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Swal.fire({ title: 'تایید فاکتورهای انتخابی', text: 'فاکتورهای انتخابشده تایید خواهند شد.', icon: 'question', showCancelButton: true, confirmButtonText: 'بله', cancelButtonText: 'خیر' })
|
||||||
|
.then(async (r) => {
|
||||||
|
if (!r.isConfirmed) return;
|
||||||
|
this.loading = true;
|
||||||
|
try {
|
||||||
|
for (const code of this.itemsSelected) {
|
||||||
|
await axios.post(`/api/sell/approve/${code}`);
|
||||||
|
}
|
||||||
|
Swal.fire({ text: 'فاکتورها تایید شدند.', icon: 'success', confirmButtonText: 'قبول' });
|
||||||
|
this.itemsSelected = [];
|
||||||
|
this.loadData();
|
||||||
|
} catch (e) {
|
||||||
|
Swal.fire({ text: 'خطا در تایید فاکتورها', icon: 'error', confirmButtonText: 'قبول' });
|
||||||
|
} finally {
|
||||||
|
this.loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
isPluginActive(pluginCode) {
|
isPluginActive(pluginCode) {
|
||||||
return this.plugins && this.plugins[pluginCode] !== undefined;
|
return this.plugins && this.plugins[pluginCode] !== undefined;
|
||||||
},
|
},
|
||||||
|
|
@ -431,6 +503,26 @@ export default defineComponent({
|
||||||
this.plugins = {};
|
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) {
|
changeLabel(label) {
|
||||||
if (this.itemsSelected.length === 0) {
|
if (this.itemsSelected.length === 0) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
@ -494,12 +586,19 @@ export default defineComponent({
|
||||||
sortBy: this.serverOptions.sortBy, // ارسال اطلاعات مرتبسازی
|
sortBy: this.serverOptions.sortBy, // ارسال اطلاعات مرتبسازی
|
||||||
});
|
});
|
||||||
|
|
||||||
this.items = (response.data.items || []).map(item => ({
|
const all = (response.data.items || []).map(item => ({
|
||||||
...item,
|
...item,
|
||||||
receivedAmount: item.relatedDocsPays || 0, // نگاشت به receivedAmount
|
receivedAmount: item.relatedDocsPays || 0, // نگاشت به receivedAmount
|
||||||
})).filter(item => item.code && typeof item.code !== 'undefined');
|
})).filter(item => item.code && typeof item.code !== 'undefined');
|
||||||
|
this.items = all;
|
||||||
this.total = Number(response.data.total) || 0;
|
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) {
|
} catch (error) {
|
||||||
console.error('Error loading data:', error);
|
console.error('Error loading data:', error);
|
||||||
this.items = [];
|
this.items = [];
|
||||||
|
|
@ -514,6 +613,49 @@ export default defineComponent({
|
||||||
this.loading = false;
|
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) {
|
canEditItem(code) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
axios.post('/api/sell/edit/can/' + code).then((response) => {
|
axios.post('/api/sell/edit/can/' + code).then((response) => {
|
||||||
|
|
@ -875,6 +1017,8 @@ export default defineComponent({
|
||||||
created() {
|
created() {
|
||||||
this.loadColumnSettings();
|
this.loadColumnSettings();
|
||||||
this.loadPlugins();
|
this.loadPlugins();
|
||||||
|
this.loadBusinessInfo();
|
||||||
|
this.loadCurrentUser();
|
||||||
this.loadData();
|
this.loadData();
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<script lang="ts">
|
<script>
|
||||||
import { defineComponent, ref } from 'vue';
|
import { defineComponent, ref } from 'vue';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import Rec from '../component/rec.vue';
|
import Rec from '../component/rec.vue';
|
||||||
|
|
@ -46,12 +46,14 @@ export default defineComponent({
|
||||||
text: '',
|
text: '',
|
||||||
color: 'error'
|
color: 'error'
|
||||||
},
|
},
|
||||||
|
sendSerialsLoading: false,
|
||||||
item: {
|
item: {
|
||||||
doc: { id: 0, date: null, code: null, des: '', amount: 0, profit: 0, shortLink: null },
|
doc: { id: 0, date: null, code: null, des: '', amount: 0, profit: 0, shortLink: null },
|
||||||
relatedDocs: [],
|
relatedDocs: [],
|
||||||
rows: [],
|
rows: [],
|
||||||
},
|
},
|
||||||
person: { nikename: null, mobile: '', tel: '', addres: '', postalcode: '' },
|
warrantySerials: [],
|
||||||
|
person: { id: null, nikename: null, mobile: '', tel: '', addres: '', postalcode: '' },
|
||||||
commoditys: [],
|
commoditys: [],
|
||||||
totalRec: 0,
|
totalRec: 0,
|
||||||
totalDiscount: 0,
|
totalDiscount: 0,
|
||||||
|
|
@ -103,6 +105,33 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
copyWarrantySerials() {
|
||||||
|
const list = this.warrantySerials || [];
|
||||||
|
const text = list.map((s) => s.serialNumber).join(', ');
|
||||||
|
if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
this.snackbar = { show: true, text: 'سریالها کپی شد', color: 'success' };
|
||||||
|
},
|
||||||
|
async sendWarrantySerialsBySms() {
|
||||||
|
if (!this.person.mobile) {
|
||||||
|
this.snackbar = { show: true, text: 'موبایل مشتری ثبت نشده است', color: 'error' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this.item?.doc?.code) return;
|
||||||
|
this.sendSerialsLoading = true;
|
||||||
|
try {
|
||||||
|
const res = await axios.post('/api/plugins/warranty/send-serials', {
|
||||||
|
invoiceCode: this.item.doc.code,
|
||||||
|
mobile: this.person.mobile
|
||||||
|
});
|
||||||
|
this.snackbar = { show: true, text: 'سریالها برای مشتری ارسال شد', color: 'success' };
|
||||||
|
} catch (e) {
|
||||||
|
this.snackbar = { show: true, text: 'خطا در ارسال سریالها', color: 'error' };
|
||||||
|
} finally {
|
||||||
|
this.sendSerialsLoading = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
async checkCanEdit() {
|
async checkCanEdit() {
|
||||||
try {
|
try {
|
||||||
const response = await axios.get(`/api/sell/edit/can/${this.$route.params.id}`);
|
const response = await axios.get(`/api/sell/edit/can/${this.$route.params.id}`);
|
||||||
|
|
@ -131,7 +160,13 @@ export default defineComponent({
|
||||||
this.shortlink_url = this.item.doc.shortLink
|
this.shortlink_url = this.item.doc.shortLink
|
||||||
? `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.shortLink}`
|
? `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.shortLink}`
|
||||||
: `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.id}`;
|
: `${getApiUrl()}/sl/sell/${localStorage.getItem('activeBid')}/${this.item.doc.id}`;
|
||||||
this.totalRec = response.data.relatedDocs.reduce((sum: number, rdoc: any) => sum + parseInt(rdoc.amount), 0);
|
this.totalRec = response.data.relatedDocs.reduce((sum, rdoc) => sum + parseInt(rdoc.amount), 0);
|
||||||
|
// دریافت سریالهای گارانتی مرتبط با فاکتور
|
||||||
|
axios.get(`/api/plugins/warranty/serials/by-invoice/${this.item.doc.code}`).then((res) => {
|
||||||
|
this.warrantySerials = res.data?.items || [];
|
||||||
|
}).catch(() => {
|
||||||
|
this.warrantySerials = [];
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
axios.get(`/api/sell/v2/get/${this.$route.params.id}`).then((response) => {
|
axios.get(`/api/sell/v2/get/${this.$route.params.id}`).then((response) => {
|
||||||
|
|
@ -151,7 +186,7 @@ export default defineComponent({
|
||||||
this.totalTax = 0;
|
this.totalTax = 0;
|
||||||
this.totalDiscount = 0;
|
this.totalDiscount = 0;
|
||||||
|
|
||||||
this.commoditys = data.items.map((item: any) => {
|
this.commoditys = data.items.map((item) => {
|
||||||
this.totalTax += item.tax;
|
this.totalTax += item.tax;
|
||||||
this.totalDiscount += item.discountAmount;
|
this.totalDiscount += item.discountAmount;
|
||||||
return {
|
return {
|
||||||
|
|
@ -215,6 +250,14 @@ export default defineComponent({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<share-options v-if="bid.shortlinks" :shortlink-url="shortlink_url" :mobile="person.mobile" :invoice-id="item.doc.id" />
|
<share-options v-if="bid.shortlinks" :shortlink-url="shortlink_url" :mobile="person.mobile" :invoice-id="item.doc.id" />
|
||||||
|
<v-btn icon :disabled="!warrantySerials.length" @click="copyWarrantySerials">
|
||||||
|
<v-icon>mdi-clipboard-text</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">کپی سریالهای گارانتی</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
|
<v-btn icon :loading="sendSerialsLoading" :disabled="!warrantySerials.length || !person.mobile" @click="sendWarrantySerialsBySms">
|
||||||
|
<v-icon>mdi-message-processing</v-icon>
|
||||||
|
<v-tooltip activator="parent" location="bottom">ارسال سریالهای گارانتی با SMS</v-tooltip>
|
||||||
|
</v-btn>
|
||||||
<print-options :invoice-id="$route.params.id" />
|
<print-options :invoice-id="$route.params.id" />
|
||||||
</v-toolbar>
|
</v-toolbar>
|
||||||
|
|
||||||
|
|
@ -338,6 +381,14 @@ export default defineComponent({
|
||||||
<!-- تب دریافتها -->
|
<!-- تب دریافتها -->
|
||||||
<v-window-item value="payments">
|
<v-window-item value="payments">
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
|
<v-alert v-if="warrantySerials.length" type="info" variant="tonal" class="mb-3">
|
||||||
|
<div class="d-flex align-center justify-space-between">
|
||||||
|
<div>
|
||||||
|
سریالهای گارانتی تخصیص یافته: {{ warrantySerials.map(s=>s.serialNumber).join('، ') }}
|
||||||
|
</div>
|
||||||
|
<v-btn size="small" color="primary" @click="copyWarrantySerials">کپی سریالها</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
<v-data-table v-if="item.relatedDocs.length" :header-props="{ class: 'custom-header' }" :headers="[
|
<v-data-table v-if="item.relatedDocs.length" :header-props="{ class: 'custom-header' }" :headers="[
|
||||||
{ title: 'مشاهده', key: 'view' },
|
{ title: 'مشاهده', key: 'view' },
|
||||||
{ title: 'شماره', key: 'code' },
|
{ title: 'شماره', key: 'code' },
|
||||||
|
|
|
||||||
|
|
@ -311,6 +311,142 @@
|
||||||
</v-card>
|
</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 variant="outlined">
|
||||||
<v-card-title class="text-h6 text-primary">
|
<v-card-title class="text-h6 text-primary">
|
||||||
<v-icon icon="mdi-package-variant" class="mr-2"></v-icon>
|
<v-icon icon="mdi-package-variant" class="mr-2"></v-icon>
|
||||||
|
|
@ -418,7 +554,6 @@
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-card-text>
|
<v-card-text>
|
||||||
<h3 class="text-primary mb-4">نسخه پشتیبان از اطلاعات کسب و کار</h3>
|
<h3 class="text-primary mb-4">نسخه پشتیبان از اطلاعات کسب و کار</h3>
|
||||||
|
|
||||||
<v-row>
|
<v-row>
|
||||||
<v-col cols="12" md="8">
|
<v-col cols="12" md="8">
|
||||||
<v-card variant="outlined" class="mb-6">
|
<v-card variant="outlined" class="mb-6">
|
||||||
|
|
@ -563,6 +698,10 @@ export default {
|
||||||
shortlinks: false,
|
shortlinks: false,
|
||||||
walletEnabled: false,
|
walletEnabled: false,
|
||||||
walletMatchBank: null,
|
walletMatchBank: null,
|
||||||
|
requireTwoStepApproval: false,
|
||||||
|
invoiceApprover: null,
|
||||||
|
warehouseApprover: null,
|
||||||
|
financialApprover: null,
|
||||||
year: {
|
year: {
|
||||||
startShamsi: '',
|
startShamsi: '',
|
||||||
endShamsi: '',
|
endShamsi: '',
|
||||||
|
|
@ -572,7 +711,9 @@ export default {
|
||||||
updateBuyPrice: false,
|
updateBuyPrice: false,
|
||||||
profitCalcType: 'lis'
|
profitCalcType: 'lis'
|
||||||
},
|
},
|
||||||
|
users: [],
|
||||||
listBanks: [],
|
listBanks: [],
|
||||||
|
plugins: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
|
@ -585,6 +726,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
isPluginActive(plugName) {
|
||||||
|
return this.plugins[plugName] !== undefined;
|
||||||
|
},
|
||||||
checkBanksExist() {
|
checkBanksExist() {
|
||||||
if (this.listBanks.length === 0) {
|
if (this.listBanks.length === 0) {
|
||||||
Swal.fire({
|
Swal.fire({
|
||||||
|
|
@ -657,32 +801,36 @@ export default {
|
||||||
//submit data
|
//submit data
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
let data = {
|
let data = {
|
||||||
'bid': localStorage.getItem('activeBid'),
|
'bid': localStorage.getItem('activeBid'),
|
||||||
'name': this.content.name,
|
'name': this.content.name,
|
||||||
'legal_name': this.content.legal_name,
|
'legal_name': this.content.legal_name,
|
||||||
'field': this.content.field,
|
'field': this.content.field,
|
||||||
'type': this.content.type,
|
'type': this.content.type,
|
||||||
'shenasemeli': this.content.shenasemeli,
|
'shenasemeli': this.content.shenasemeli,
|
||||||
'codeeqtesadi': this.content.codeeqtesadi,
|
'codeeqtesadi': this.content.codeeqtesadi,
|
||||||
'shomaresabt': this.content.shomaresabt,
|
'shomaresabt': this.content.shomaresabt,
|
||||||
'country': this.content.country,
|
'country': this.content.country,
|
||||||
'ostan': this.content.ostan,
|
'ostan': this.content.ostan,
|
||||||
'shahrestan': this.content.shahrestan,
|
'shahrestan': this.content.shahrestan,
|
||||||
'postalcode': this.content.postalcode,
|
'postalcode': this.content.postalcode,
|
||||||
'tel': this.content.tel,
|
'tel': this.content.tel,
|
||||||
'mobile': this.content.mobile,
|
'mobile': this.content.mobile,
|
||||||
'address': this.content.address,
|
'address': this.content.address,
|
||||||
'website': this.content.website,
|
'website': this.content.website,
|
||||||
'email': this.content.email,
|
'email': this.content.email,
|
||||||
'arzmain': this.content.arzmain,
|
'arzmain': this.content.arzmain,
|
||||||
'maliyatafzode': this.content.maliyatafzode,
|
'maliyatafzode': this.content.maliyatafzode,
|
||||||
'shortlinks': this.content.shortlinks,
|
'shortlinks': this.content.shortlinks,
|
||||||
'walletEnabled': this.content.walletEnabled,
|
'walletEnabled': this.content.walletEnabled,
|
||||||
'walletMatchBank': this.content.walletMatchBank,
|
'walletMatchBank': this.content.walletMatchBank,
|
||||||
'year': this.content.year,
|
'requireTwoStepApproval': this.content.requireTwoStepApproval,
|
||||||
'commodityUpdateBuyPriceAuto': this.content.updateBuyPrice,
|
'invoiceApprover': this.content.invoiceApprover,
|
||||||
'commodityUpdateSellPriceAuto': this.content.updateSellPrice,
|
'warehouseApprover': this.content.warehouseApprover,
|
||||||
'profitCalcType': this.content.profitCalcType
|
'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)
|
axios.post('/api/business/insert', data)
|
||||||
|
|
@ -715,39 +863,69 @@ export default {
|
||||||
},
|
},
|
||||||
async beforeMount() {
|
async beforeMount() {
|
||||||
this.loading = true
|
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 {
|
try {
|
||||||
const permissionsResponse = await axios.post('/api/business/get/user/permissions');
|
// ابتدا اطلاعات کسب و کار را بارگذاری کن
|
||||||
if (permissionsResponse.data.settings) {
|
const businessResponse = await axios.post('/api/business/get/info/' + localStorage.getItem('activeBid'));
|
||||||
// بررسی فعال بودن افزونه accpro
|
this.content = businessResponse.data;
|
||||||
const pluginResponse = await axios.post('/api/plugin/check/accpro/' + localStorage.getItem('activeBid'));
|
|
||||||
this.showBackupTab = pluginResponse.data.active;
|
// اگر 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) {
|
} catch (error) {
|
||||||
console.error('خطا در بررسی دسترسیها:', error);
|
console.error('Error loading data:', error);
|
||||||
this.showBackupTab = false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -402,6 +402,52 @@
|
||||||
</v-col>
|
</v-col>
|
||||||
</v-row>
|
</v-row>
|
||||||
|
|
||||||
|
<!-- بخش دسترسی انباردار -->
|
||||||
|
<v-row class="mt-4">
|
||||||
|
<v-col cols="12">
|
||||||
|
<v-card-title class="text-h6 font-weight-bold mb-4 text-primary">
|
||||||
|
<v-icon color="primary" class="me-2">mdi-warehouse</v-icon>
|
||||||
|
دسترسی انباردار
|
||||||
|
</v-card-title>
|
||||||
|
<v-alert
|
||||||
|
type="info"
|
||||||
|
variant="tonal"
|
||||||
|
class="warehouse-alert mr-4"
|
||||||
|
density="compact"
|
||||||
|
border="start"
|
||||||
|
border-color="primary"
|
||||||
|
|
||||||
|
>
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-information</v-icon>
|
||||||
|
</template>
|
||||||
|
<div class="text-body-2">
|
||||||
|
دسترسی انباردار شامل تمام بخشهای انبارداری مانند مدیریت انبارها، کالاها، حوالهها، موجودی و گارانتی میباشد.
|
||||||
|
</div>
|
||||||
|
</v-alert>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="6">
|
||||||
|
<v-card variant="outlined" class="h-100">
|
||||||
|
<v-card-text>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch
|
||||||
|
v-model="info.warehouseManager"
|
||||||
|
label="دسترسی کامل انباردار"
|
||||||
|
@change="savePerms('warehouseManager')"
|
||||||
|
hide-details
|
||||||
|
color="primary"
|
||||||
|
density="comfortable"
|
||||||
|
:loading="loadingSwitches.warehouseManager"
|
||||||
|
:disabled="loadingSwitches.warehouseManager"
|
||||||
|
></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
|
||||||
<v-row v-if="isPluginActive('accpro')" class="mt-4">
|
<v-row v-if="isPluginActive('accpro')" class="mt-4">
|
||||||
<v-col cols="12">
|
<v-col cols="12">
|
||||||
<v-card-title class="text-h6 font-weight-bold mb-4">بسته حسابداری پیشرفته</v-card-title>
|
<v-card-title class="text-h6 font-weight-bold mb-4">بسته حسابداری پیشرفته</v-card-title>
|
||||||
|
|
@ -732,7 +778,8 @@ export default {
|
||||||
plugGhestaManager: false,
|
plugGhestaManager: false,
|
||||||
plugTaxSettings: false,
|
plugTaxSettings: false,
|
||||||
inquiry: false,
|
inquiry: false,
|
||||||
ai: false
|
ai: false,
|
||||||
|
warehouseManager: false
|
||||||
};
|
};
|
||||||
|
|
||||||
axios.post('/api/business/get/user/permissions',
|
axios.post('/api/business/get/user/permissions',
|
||||||
|
|
@ -809,4 +856,16 @@ export default {
|
||||||
.v-switch.v-input--disabled {
|
.v-switch.v-input--disabled {
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.warehouse-alert {
|
||||||
|
width: 45%;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
.warehouse-alert {
|
||||||
|
width: 100%;
|
||||||
|
margin-right: 0px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -53,8 +53,12 @@
|
||||||
<v-window-item value="output">
|
<v-window-item value="output">
|
||||||
<v-text-field v-model="outputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
<v-text-field v-model="outputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||||
density="compact" hide-details class="mb-1"></v-text-field>
|
density="compact" hide-details class="mb-1"></v-text-field>
|
||||||
|
<v-tabs v-if="business.requireTwoStepApproval" v-model="outputSubTab" color="primary" density="comfortable" class="mb-2">
|
||||||
|
<v-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"
|
hover density="compact" class="elevation-1 text-center"
|
||||||
:header-props="{ class: 'custom-header' }">
|
:header-props="{ class: 'custom-header' }">
|
||||||
<template v-slot:item="{ item }">
|
<template v-slot:item="{ item }">
|
||||||
|
|
@ -71,6 +75,12 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>مشاهده</v-list-item-title>
|
<v-list-item-title>مشاهده</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item @click="deleteTicket('output', item.code)">
|
<v-list-item @click="deleteTicket('output', item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="error">mdi-delete</v-icon>
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
|
@ -80,9 +90,17 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</td>
|
</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('date')" class="text-center">{{ item.date }}</td>
|
||||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
<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>
|
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -94,8 +112,12 @@
|
||||||
<v-window-item value="input">
|
<v-window-item value="input">
|
||||||
<v-text-field v-model="inputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
<v-text-field v-model="inputSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||||
density="compact" hide-details class="mb-1"></v-text-field>
|
density="compact" hide-details class="mb-1"></v-text-field>
|
||||||
|
<v-tabs v-if="business.requireTwoStepApproval" v-model="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"
|
density="compact" class="elevation-1 text-center"
|
||||||
:header-props="{ class: 'custom-header' }">
|
:header-props="{ class: 'custom-header' }">
|
||||||
<template v-slot:item="{ item }">
|
<template v-slot:item="{ item }">
|
||||||
|
|
@ -112,6 +134,12 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>مشاهده</v-list-item-title>
|
<v-list-item-title>مشاهده</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item @click="deleteTicket('input', item.code)">
|
<v-list-item @click="deleteTicket('input', item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="error">mdi-delete</v-icon>
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
|
@ -121,9 +149,17 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</td>
|
</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('date')" class="text-center">{{ item.date }}</td>
|
||||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
<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>
|
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -135,8 +171,12 @@
|
||||||
<v-window-item value="transfer">
|
<v-window-item value="transfer">
|
||||||
<v-text-field v-model="transferSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
<v-text-field v-model="transferSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||||
density="compact" hide-details class="mb-1"></v-text-field>
|
density="compact" hide-details class="mb-1"></v-text-field>
|
||||||
|
<v-tabs v-if="business.requireTwoStepApproval" v-model="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"
|
density="compact" class="elevation-1 text-center"
|
||||||
:header-props="{ class: 'custom-header' }">
|
:header-props="{ class: 'custom-header' }">
|
||||||
<template v-slot:item="{ item }">
|
<template v-slot:item="{ item }">
|
||||||
|
|
@ -153,6 +193,12 @@
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>مشاهده</v-list-item-title>
|
<v-list-item-title>مشاهده</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item @click="deleteTicket('transfer', item.code)">
|
<v-list-item @click="deleteTicket('transfer', item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="error">mdi-delete</v-icon>
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
|
@ -162,9 +208,17 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</td>
|
</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('date')" class="text-center">{{ item.date }}</td>
|
||||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
<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>
|
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -176,8 +230,12 @@
|
||||||
<v-window-item value="waste">
|
<v-window-item value="waste">
|
||||||
<v-text-field v-model="wasteSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
<v-text-field v-model="wasteSearchValue" prepend-inner-icon="mdi-magnify" label="جستجو" variant="outlined"
|
||||||
density="compact" hide-details class="mb-1"></v-text-field>
|
density="compact" hide-details class="mb-1"></v-text-field>
|
||||||
|
<v-tabs v-if="business.requireTwoStepApproval" v-model="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"
|
density="compact" class="elevation-1 text-center"
|
||||||
:header-props="{ class: 'custom-header' }">
|
:header-props="{ class: 'custom-header' }">
|
||||||
<template v-slot:item="{ item }">
|
<template v-slot:item="{ item }">
|
||||||
|
|
@ -190,10 +248,16 @@
|
||||||
<v-list>
|
<v-list>
|
||||||
<v-list-item :to="'/acc/storeroom/ticket/view/' + item.code">
|
<v-list-item :to="'/acc/storeroom/ticket/view/' + item.code">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="success">mdi-eye</v-icon>
|
<v-icon color="success">mdi-check-decagram</v-icon>
|
||||||
</template>
|
</template>
|
||||||
<v-list-item-title>مشاهده</v-list-item-title>
|
<v-list-item-title>مشاهده</v-list-item-title>
|
||||||
</v-list-item>
|
</v-list-item>
|
||||||
|
<v-list-item v-if="canShowApprovalButton(item)" @click="approveTicket(item.code)">
|
||||||
|
<template v-slot:prepend>
|
||||||
|
<v-icon color="primary">mdi-check-decagram</v-icon>
|
||||||
|
</template>
|
||||||
|
<v-list-item-title>تایید حواله</v-list-item-title>
|
||||||
|
</v-list-item>
|
||||||
<v-list-item @click="deleteTicket('waste', item.code)">
|
<v-list-item @click="deleteTicket('waste', item.code)">
|
||||||
<template v-slot:prepend>
|
<template v-slot:prepend>
|
||||||
<v-icon color="error">mdi-delete</v-icon>
|
<v-icon color="error">mdi-delete</v-icon>
|
||||||
|
|
@ -203,9 +267,17 @@
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</td>
|
</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('date')" class="text-center">{{ item.date }}</td>
|
||||||
<td v-if="isColumnVisible('doc.code')" class="text-center">{{ item.doc.code }}</td>
|
<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>
|
<td v-if="isColumnVisible('person.nikename')" class="text-center">{{ item.person.nikename }}</td>
|
||||||
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
<td v-if="isColumnVisible('des')" class="text-center">{{ item.des }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -267,17 +339,26 @@ interface Ticket {
|
||||||
date: string;
|
date: string;
|
||||||
doc: {
|
doc: {
|
||||||
code: string;
|
code: string;
|
||||||
|
preview?: boolean;
|
||||||
|
approved?: boolean;
|
||||||
};
|
};
|
||||||
person: {
|
person: {
|
||||||
nikename: string;
|
nikename: string;
|
||||||
};
|
};
|
||||||
des: string;
|
des: string;
|
||||||
|
preview?: boolean;
|
||||||
|
approved?: boolean;
|
||||||
|
approvedBy?: {
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
id: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Header {
|
interface Header {
|
||||||
title: string;
|
title: string;
|
||||||
key: string;
|
key: string;
|
||||||
align: string;
|
align?: 'start' | 'center' | 'end';
|
||||||
sortable: boolean;
|
sortable: boolean;
|
||||||
width: number;
|
width: number;
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
|
|
@ -294,7 +375,16 @@ const outputSearchValue = ref('');
|
||||||
const transferSearchValue = ref('');
|
const transferSearchValue = ref('');
|
||||||
const wasteSearchValue = ref('');
|
const wasteSearchValue = ref('');
|
||||||
const activeTab = ref('output');
|
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 showColumnDialog = ref(false);
|
||||||
|
const business = ref({
|
||||||
|
requireTwoStepApproval: false,
|
||||||
|
warehouseApprover: null
|
||||||
|
});
|
||||||
|
const currentUser = ref({ email: '', owner: false });
|
||||||
|
|
||||||
// دیالوگها
|
// دیالوگها
|
||||||
const deleteDialog = ref({
|
const deleteDialog = ref({
|
||||||
|
|
@ -311,22 +401,38 @@ const snackbar = ref({
|
||||||
|
|
||||||
// تعریف همه ستونها
|
// تعریف همه ستونها
|
||||||
const allHeaders = ref<Header[]>([
|
const allHeaders = ref<Header[]>([
|
||||||
{ title: "عملیات", key: "operation", align: 'center', sortable: false, width: 100, visible: true },
|
{ title: "عملیات", key: "operation", align: 'center' as const, sortable: false, width: 100, visible: true },
|
||||||
{ title: "شماره", key: "code", align: 'center', sortable: true, width: 100, visible: true },
|
{ title: "شماره", key: "code", align: 'center' as const, sortable: true, width: 100, visible: true },
|
||||||
{ title: "تاریخ", key: "date", align: 'center', sortable: true, width: 120, visible: true },
|
{ title: "تاریخ", key: "date", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||||
{ title: "شماره فاکتور", key: "doc.code", align: 'center', sortable: true, width: 120, visible: true },
|
{ title: "وضعیت تایید", key: "approvalStatus", align: 'center' as const, sortable: true, width: 150, visible: true },
|
||||||
{ title: "شخص", key: "person.nikename", align: 'center', sortable: true, width: 120, visible: true },
|
{ title: "تاییدکننده", key: "approvedBy", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||||
{ title: "توضیحات", key: "des", align: 'center', sortable: true, width: 200, visible: true },
|
{ title: "شماره فاکتور", key: "doc.code", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||||
|
{ title: "شخص", key: "person.nikename", align: 'center' as const, sortable: true, width: 120, visible: true },
|
||||||
|
{ title: "توضیحات", key: "des", align: 'center' as const, sortable: true, width: 200, visible: true },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// ستونهای قابل نمایش
|
// ستونهای قابل نمایش
|
||||||
const visibleHeaders = computed(() => {
|
const visibleHeaders = computed(() => {
|
||||||
return allHeaders.value.filter((header: Header) => header.visible);
|
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) => {
|
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
|
// کلید ذخیرهسازی در localStorage
|
||||||
|
|
@ -357,6 +463,57 @@ const formatNumber = (value: string | number) => {
|
||||||
return Number(value).toLocaleString('fa-IR');
|
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 () => {
|
const loadData = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
|
|
@ -367,10 +524,10 @@ const loadData = async () => {
|
||||||
axios.post('/api/storeroom/tickets/list/transfer'),
|
axios.post('/api/storeroom/tickets/list/transfer'),
|
||||||
axios.post('/api/storeroom/tickets/list/waste')
|
axios.post('/api/storeroom/tickets/list/waste')
|
||||||
]);
|
]);
|
||||||
inputItems.value = inputResponse.data;
|
inputItems.value = (inputResponse.data || []);
|
||||||
outputItems.value = outputResponse.data;
|
outputItems.value = (outputResponse.data || []);
|
||||||
transferItems.value = transferResponse.data;
|
transferItems.value = (transferResponse.data || []);
|
||||||
wasteItems.value = wasteResponse.data;
|
wasteItems.value = (wasteResponse.data || []);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading data:', error);
|
console.error('Error loading data:', error);
|
||||||
snackbar.value = {
|
snackbar.value = {
|
||||||
|
|
@ -383,6 +540,53 @@ 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/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' };
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// نمایش متن وضعیت تأیید
|
||||||
|
const getApprovalStatusText = (item: Ticket) => {
|
||||||
|
if (!business.value.requireTwoStepApproval) return 'تایید دو مرحلهای غیرفعال';
|
||||||
|
|
||||||
|
if (item?.preview) return 'در انتظار تایید';
|
||||||
|
if (item?.approved) return 'تایید شده';
|
||||||
|
return 'نامشخص';
|
||||||
|
};
|
||||||
|
|
||||||
|
// نمایش رنگ وضعیت تأیید
|
||||||
|
const getApprovalStatusColor = (item: Ticket) => {
|
||||||
|
if (!business.value.requireTwoStepApproval) return 'default';
|
||||||
|
|
||||||
|
if (item?.preview) return 'warning';
|
||||||
|
if (item?.approved) return 'success';
|
||||||
|
return 'default';
|
||||||
|
};
|
||||||
|
|
||||||
// حذف حواله
|
// حذف حواله
|
||||||
const deleteTicket = (type: 'input' | 'output' | 'transfer' | 'waste', code: string) => {
|
const deleteTicket = (type: 'input' | 'output' | 'transfer' | 'waste', code: string) => {
|
||||||
deleteDialog.value = {
|
deleteDialog.value = {
|
||||||
|
|
@ -438,6 +642,8 @@ const confirmDelete = async () => {
|
||||||
// مانت کامپوننت
|
// مانت کامپوننت
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadColumnSettings();
|
loadColumnSettings();
|
||||||
|
loadBusinessInfo();
|
||||||
|
loadCurrentUser();
|
||||||
loadData();
|
loadData();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import moment from 'jalali-moment'
|
||||||
|
|
||||||
interface Business {
|
interface Business {
|
||||||
legal_name: string
|
legal_name: string
|
||||||
|
|
@ -19,6 +20,8 @@ interface Ticket {
|
||||||
type: string
|
type: string
|
||||||
typeString: string
|
typeString: string
|
||||||
storeroom: Storeroom
|
storeroom: Storeroom
|
||||||
|
preview: boolean
|
||||||
|
approved: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Person {
|
interface Person {
|
||||||
|
|
@ -70,7 +73,9 @@ const item = ref<Item>({
|
||||||
typeString: '',
|
typeString: '',
|
||||||
storeroom: {
|
storeroom: {
|
||||||
manager: ''
|
manager: ''
|
||||||
}
|
},
|
||||||
|
preview: false,
|
||||||
|
approved: false
|
||||||
},
|
},
|
||||||
rows: [],
|
rows: [],
|
||||||
person: {
|
person: {
|
||||||
|
|
@ -128,8 +133,150 @@ const printInvoice = async () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attachments
|
||||||
|
const attachments = ref<any[]>([])
|
||||||
|
const loadingAttachments = ref(false)
|
||||||
|
const uploading = ref(false)
|
||||||
|
const selectedFile = ref<File | null>(null)
|
||||||
|
const attachDes = ref('')
|
||||||
|
|
||||||
|
const canPrint = computed(() => {
|
||||||
|
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
|
||||||
|
const res = await axios.get(`/api/storeroom/ticket/attachments/${router.currentRoute.value.params.id}`)
|
||||||
|
attachments.value = Array.isArray(res.data) ? res.data : []
|
||||||
|
} catch (e) {
|
||||||
|
attachments.value = []
|
||||||
|
} finally {
|
||||||
|
loadingAttachments.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uploadAttachment = async () => {
|
||||||
|
if (!selectedFile.value) return
|
||||||
|
try {
|
||||||
|
uploading.value = true
|
||||||
|
const fd = new FormData()
|
||||||
|
fd.append('file', selectedFile.value)
|
||||||
|
if (attachDes.value) fd.append('des', attachDes.value)
|
||||||
|
await axios.post(`/api/storeroom/ticket/attachments/upload/${router.currentRoute.value.params.id}`, fd, {
|
||||||
|
headers: { 'Content-Type': 'multipart/form-data' }
|
||||||
|
})
|
||||||
|
selectedFile.value = null
|
||||||
|
attachDes.value = ''
|
||||||
|
await loadAttachments()
|
||||||
|
} catch (e) {
|
||||||
|
// خطا نمایش داده نمیشود تا ساده بماند
|
||||||
|
} finally {
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(() => {
|
onMounted(() => {
|
||||||
loadData()
|
loadData()
|
||||||
|
loadAttachments()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -150,6 +297,7 @@ onMounted(() => {
|
||||||
@click="printInvoice"
|
@click="printInvoice"
|
||||||
color="primary"
|
color="primary"
|
||||||
icon="mdi-printer"
|
icon="mdi-printer"
|
||||||
|
:disabled="!canPrint"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</v-tooltip>
|
</v-tooltip>
|
||||||
|
|
@ -293,6 +441,117 @@ onMounted(() => {
|
||||||
</v-data-table>
|
</v-data-table>
|
||||||
</v-card-text>
|
</v-card-text>
|
||||||
</v-card>
|
</v-card>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<v-file-input
|
||||||
|
v-model="selectedFile"
|
||||||
|
label="انتخاب فایل"
|
||||||
|
accept="image/*,.pdf,.jpg,.jpeg,.png"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
style="max-width: 360px"
|
||||||
|
:disabled="isAttachmentsDisabled"
|
||||||
|
/>
|
||||||
|
<v-text-field
|
||||||
|
v-model="attachDes"
|
||||||
|
label="توضیحات"
|
||||||
|
variant="outlined"
|
||||||
|
density="compact"
|
||||||
|
hide-details
|
||||||
|
:disabled="isAttachmentsDisabled"
|
||||||
|
/>
|
||||||
|
<v-btn color="primary" :loading="uploading" :disabled="!selectedFile || isAttachmentsDisabled" @click="uploadAttachment">آپلود</v-btn>
|
||||||
|
<v-spacer />
|
||||||
|
<v-btn variant="text" icon="mdi-refresh" :loading="loadingAttachments" :disabled="isAttachmentsDisabled" @click="loadAttachments" />
|
||||||
|
</div>
|
||||||
|
<v-data-table
|
||||||
|
:headers="[
|
||||||
|
{ 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.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>
|
</v-container>
|
||||||
</template>
|
</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