diff --git a/hesabixCore/composer.json b/hesabixCore/composer.json index 66aedb9..f44927d 100644 --- a/hesabixCore/composer.json +++ b/hesabixCore/composer.json @@ -16,6 +16,7 @@ "doctrine/orm": "^3.2", "dompdf/dompdf": "^3.0", "melipayamak/php": "^1.0", + "morilog/jalali": "*", "mpdf/mpdf": "^8.2", "nelmio/api-doc-bundle": "^4.35", "nelmio/cors-bundle": "^2.5", diff --git a/hesabixCore/composer.lock b/hesabixCore/composer.lock index fa625a3..c53e754 100644 --- a/hesabixCore/composer.lock +++ b/hesabixCore/composer.lock @@ -4,8 +4,75 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "43db0ad2bb94569ed6d44cabf503210e", + "content-hash": "01b5daf5a6fd011b4eb616e0e4ae18fe", "packages": [ + { + "name": "beberlei/assert", + "version": "v3.3.3", + "source": { + "type": "git", + "url": "https://github.com/beberlei/assert.git", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/beberlei/assert/zipball/b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "reference": "b5fd8eacd8915a1b627b8bfc027803f1939734dd", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=6.0.0", + "yoast/phpunit-polyfills": "^0.1.0" + }, + "suggest": { + "ext-intl": "Needed to allow Assertion::count(), Assertion::isCountable(), Assertion::minCount(), and Assertion::maxCount() to operate on ResourceBundles" + }, + "type": "library", + "autoload": { + "files": [ + "lib/Assert/functions.php" + ], + "psr-4": { + "Assert\\": "lib/Assert" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de", + "role": "Lead Developer" + }, + { + "name": "Richard Quadling", + "email": "rquadling@gmail.com", + "role": "Collaborator" + } + ], + "description": "Thin assertion library for input validation in business models.", + "keywords": [ + "assert", + "assertion", + "validation" + ], + "support": { + "issues": "https://github.com/beberlei/assert/issues", + "source": "https://github.com/beberlei/assert/tree/v3.3.3" + }, + "time": "2024-07-15T13:18:35+00:00" + }, { "name": "brick/math", "version": "0.12.3", @@ -66,6 +133,75 @@ ], "time": "2025-02-28T13:11:00+00:00" }, + { + "name": "carbonphp/carbon-doctrine-types", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "conflict": { + "doctrine/dbal": "<4.0.0 || >=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, { "name": "composer/pcre", "version": "3.3.2", @@ -2297,6 +2433,71 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "morilog/jalali", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/morilog/jalali.git", + "reference": "f475f4db7bd540c6abc01126e46824c897ed1e03" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/morilog/jalali/zipball/f475f4db7bd540c6abc01126e46824c897ed1e03", + "reference": "f475f4db7bd540c6abc01126e46824c897ed1e03", + "shasum": "" + }, + "require": { + "beberlei/assert": "^3.0", + "nesbot/carbon": "^1.21 || ^2.0 || ^3.0", + "php": "^7.0 | ^8.0" + }, + "require-dev": { + "phpunit/phpunit": ">4.0" + }, + "type": "library", + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Morilog\\Jalali\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Milad Rey", + "email": "miladr@gmail.com" + }, + { + "name": "Morteza Parvini", + "email": "m.parvini@outlook.com" + } + ], + "description": "This Package helps developers to easily work with Jalali (Shamsi or Iranian) dates in PHP applications, based on Jalali (Shamsi) DateTime class.", + "keywords": [ + "Jalali", + "date", + "datetime", + "laravel", + "morilog" + ], + "support": { + "issues": "https://github.com/morilog/jalali/issues", + "source": "https://github.com/morilog/jalali/tree/v3.4.2" + }, + "funding": [ + { + "url": "https://issuehunt.io/r/morilog", + "type": "issuehunt" + } + ], + "time": "2024-05-09T08:44:51+00:00" + }, { "name": "mpdf/mpdf", "version": "v8.2.5", @@ -2714,6 +2915,111 @@ }, "time": "2024-06-24T21:25:28+00:00" }, + { + "name": "nesbot/carbon", + "version": "3.10.2", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "reference": "76b5c07b8a9d2025ed1610e14cef1f3fd6ad2c24", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.75.0", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.17", + "phpunit/phpunit": "^10.5.46", + "squizlabs/php_codesniffer": "^3.13.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbon.nesbot.com", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbon.nesbot.com/docs", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2025-08-02T09:36:06+00:00" + }, { "name": "nikic/php-parser", "version": "v5.4.0", diff --git a/hesabixCore/migrations/Version20250113000000.php b/hesabixCore/migrations/Version20250113000000.php new file mode 100644 index 0000000..73e672a --- /dev/null +++ b/hesabixCore/migrations/Version20250113000000.php @@ -0,0 +1,40 @@ +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'); + } +} diff --git a/hesabixCore/migrations/Version20250113000001.php b/hesabixCore/migrations/Version20250113000001.php new file mode 100644 index 0000000..bb87e08 --- /dev/null +++ b/hesabixCore/migrations/Version20250113000001.php @@ -0,0 +1,31 @@ +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'); + } +} diff --git a/hesabixCore/migrations/Version20250113000002.php b/hesabixCore/migrations/Version20250113000002.php new file mode 100644 index 0000000..794b897 --- /dev/null +++ b/hesabixCore/migrations/Version20250113000002.php @@ -0,0 +1,40 @@ +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'); + } +} diff --git a/hesabixCore/migrations/Version20250815143325.php b/hesabixCore/migrations/Version20250815143325.php new file mode 100644 index 0000000..3008029 --- /dev/null +++ b/hesabixCore/migrations/Version20250815143325.php @@ -0,0 +1,101 @@ +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); + } +} diff --git a/hesabixCore/migrations/Version20250816171207.php b/hesabixCore/migrations/Version20250816171207.php new file mode 100644 index 0000000..5bfe61d --- /dev/null +++ b/hesabixCore/migrations/Version20250816171207.php @@ -0,0 +1,47 @@ +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); + } +} diff --git a/hesabixCore/migrations/Version20250811113332.php b/hesabixCore/migrations/Version20250816185111.php similarity index 76% rename from hesabixCore/migrations/Version20250811113332.php rename to hesabixCore/migrations/Version20250816185111.php index b46c836..9274184 100644 --- a/hesabixCore/migrations/Version20250811113332.php +++ b/hesabixCore/migrations/Version20250816185111.php @@ -10,7 +10,7 @@ use Doctrine\Migrations\AbstractMigration; /** * Auto-generated Migration: Please modify to your needs! */ -final class Version20250811113332 extends AbstractMigration +final class Version20250816185111 extends AbstractMigration { public function getDescription(): string { @@ -21,7 +21,7 @@ final class Version20250811113332 extends AbstractMigration { // this up() migration is auto-generated, please modify it to your needs $this->addSql(<<<'SQL' - ALTER TABLE person ADD require_two_step TINYINT(1) DEFAULT NULL + ALTER TABLE plug_warranty_serial ADD activation VARCHAR(20) NOT NULL SQL); } @@ -29,7 +29,7 @@ final class Version20250811113332 extends AbstractMigration { // this down() migration is auto-generated, please modify it to your needs $this->addSql(<<<'SQL' - ALTER TABLE person DROP require_two_step + ALTER TABLE plug_warranty_serial DROP activation SQL); } } diff --git a/hesabixCore/migrations/Version20250816185556.php b/hesabixCore/migrations/Version20250816185556.php new file mode 100644 index 0000000..d6ea780 --- /dev/null +++ b/hesabixCore/migrations/Version20250816185556.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/hesabixCore/migrations/Version20250818042052.php b/hesabixCore/migrations/Version20250818042052.php new file mode 100644 index 0000000..67a5631 --- /dev/null +++ b/hesabixCore/migrations/Version20250818042052.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/hesabixCore/migrations/Version20250818042232.php b/hesabixCore/migrations/Version20250818042232.php new file mode 100644 index 0000000..ea3f647 --- /dev/null +++ b/hesabixCore/migrations/Version20250818042232.php @@ -0,0 +1,35 @@ +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); + } +} diff --git a/hesabixCore/src/Cog/PersonService.php b/hesabixCore/src/Cog/PersonService.php index 7895d6c..1137a6d 100644 --- a/hesabixCore/src/Cog/PersonService.php +++ b/hesabixCore/src/Cog/PersonService.php @@ -288,10 +288,7 @@ class PersonService if (isset($params['shenasemeli'])) $person->setShenasemeli($params['shenasemeli']); if (isset($params['company'])) $person->setCompany($params['company']); if (isset($params['tags'])) $person->setTags($params['tags']); - if (isset($params['requireTwoStep'])) { - error_log("Setting requireTwoStep: " . var_export($params['requireTwoStep'], true)); - $person->setRequireTwoStep((bool)$params['requireTwoStep']); - } + if (array_key_exists('prelabel', $params)) { if ($params['prelabel'] != '') { $prelabel = $em->getRepository(\App\Entity\PersonPrelabel::class)->findOneBy(['label' => $params['prelabel']]); diff --git a/hesabixCore/src/Controller/ApprovalController.php b/hesabixCore/src/Controller/ApprovalController.php new file mode 100644 index 0000000..e4f9b85 --- /dev/null +++ b/hesabixCore/src/Controller/ApprovalController.php @@ -0,0 +1,426 @@ +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(); + } +} diff --git a/hesabixCore/src/Controller/BusinessController.php b/hesabixCore/src/Controller/BusinessController.php index af3ed20..15b8754 100644 --- a/hesabixCore/src/Controller/BusinessController.php +++ b/hesabixCore/src/Controller/BusinessController.php @@ -246,6 +246,22 @@ class BusinessController extends AbstractController $business->setWalletEnable(false); } } + if (array_key_exists('requireTwoStepApproval', $params)) { + $business->setRequireTwoStepApproval((bool)$params['requireTwoStepApproval']); + } + + // Set approvers + if (array_key_exists('invoiceApprover', $params)) { + $business->setInvoiceApprover($params['invoiceApprover']); + } + + if (array_key_exists('warehouseApprover', $params)) { + $business->setWarehouseApprover($params['warehouseApprover']); + } + + if (array_key_exists('financialApprover', $params)) { + $business->setFinancialApprover($params['financialApprover']); + } //get Money type if (!array_key_exists('arzmain', $params) && $isNew) { diff --git a/hesabixCore/src/Controller/ImportWorkflowController.php b/hesabixCore/src/Controller/ImportWorkflowController.php index e0e058f..82bb075 100644 --- a/hesabixCore/src/Controller/ImportWorkflowController.php +++ b/hesabixCore/src/Controller/ImportWorkflowController.php @@ -13,6 +13,7 @@ use App\Service\Access; use App\Service\Log; use App\Service\Provider; use Doctrine\ORM\EntityManagerInterface; +use Morilog\Jalali\CalendarUtils; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -22,6 +23,8 @@ use App\Entity\StoreroomTicket; use App\Entity\Storeroom; use App\Entity\Person; use App\Entity\HesabdariDoc; +use App\Entity\Commodity; +use App\Service\FileStorage; class ImportWorkflowController extends AbstractController { @@ -80,6 +83,33 @@ class ImportWorkflowController extends AbstractController ]); } + #[Route('/api/import-workflow/documents/{docId}/download', name: 'api_import_workflow_document_download', methods: ['GET'])] + public function downloadDocument(string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): \Symfony\Component\HttpFoundation\Response + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc) { + throw $this->createNotFoundException('سند یافت نشد'); + } + // Enforce business ownership + $workflow = $doc->getImportWorkflow(); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + throw $this->createAccessDeniedException('دسترسی ندارید'); + } + $abs = $storage->absolutePath((string)$doc->getFilePath()); + if (!is_file($abs) || !is_readable($abs)) { + throw $this->createNotFoundException('فایل یافت نشد'); + } + $response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs); + $response->setContentDisposition( + \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT, + $doc->getFileName() ?: basename($abs) + ); + $response->headers->set('Content-Type', $doc->getFileType() ?: 'application/octet-stream'); + return $response; + } + #[Route('/api/import-workflow/create', name: 'api_import_workflow_create', methods: ['POST'])] public function create( Request $request, @@ -228,6 +258,11 @@ class ImportWorkflowController extends AbstractController foreach ($workflow->getItems() as $item) { $data['items'][] = [ 'id' => $item->getId(), + 'commodity' => $item->getCommodity() ? [ + 'id' => $item->getCommodity()->getId(), + 'code' => method_exists($item->getCommodity(), 'getCode') ? $item->getCommodity()->getCode() : null, + 'name' => method_exists($item->getCommodity(), 'getName') ? $item->getCommodity()->getName() : null, + ] : null, 'name' => $item->getName(), 'productCode' => $item->getProductCode(), 'brand' => $item->getBrand(), @@ -270,14 +305,13 @@ class ImportWorkflowController extends AbstractController 'id' => $document->getId(), 'type' => $document->getType(), 'title' => $document->getTitle(), - 'filePath' => $document->getFilePath(), + 'filePath' => $document->getFilePath(), // relative storage path 'fileName' => $document->getFileName(), 'fileSize' => $document->getFileSize(), 'fileType' => $document->getFileType(), 'description' => $document->getDescription(), 'documentNumber' => $document->getDocumentNumber(), 'issueDate' => $document->getIssueDate(), - 'expiryDate' => $document->getExpiryDate(), 'status' => $document->getStatus(), 'dateSubmit' => $document->getDateSubmit() ]; @@ -348,6 +382,530 @@ class ImportWorkflowController extends AbstractController ]); } + // Documents CRUD + #[Route('/api/import-workflow/{id}/documents/create', name: 'api_import_workflow_document_create', methods: ['POST'])] + public function createDocument(string $id, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = $request->request->all(); + $doc = new ImportWorkflowDocument(); + $doc->setImportWorkflow($workflow); + $doc->setType($data['type'] ?? 'other'); + $doc->setTitle($data['title'] ?? ''); + $doc->setDocumentNumber($data['documentNumber'] ?? null); + $doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null); + $doc->setDescription($data['description'] ?? null); + $doc->setStatus('active'); + // handle file + $file = $request->files->get('file'); + if ($file) { + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs'); + $doc->setFilePath($stored['relativePath']); + $doc->setFileName($stored['originalName']); + $doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $doc->setFileType($stored['mime']); + } + $entityManager->persist($doc); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]); + } + + #[Route('/api/import-workflow/{id}/documents/{docId}/update', name: 'api_import_workflow_document_update', methods: ['PUT','POST'])] + public function updateDocument(string $id, string $docId, Request $request, Access $access, EntityManagerInterface $entityManager, FileStorage $storage): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404); + } + $data = $request->request->all(); + if (isset($data['type'])) $doc->setType($data['type']); + if (isset($data['title'])) $doc->setTitle($data['title']); + if (isset($data['documentNumber'])) $doc->setDocumentNumber($data['documentNumber']); + if (isset($data['issueDate'])) $doc->setIssueDate($this->jalaliToGregorian($data['issueDate']) ?? null); + if (isset($data['description'])) $doc->setDescription($data['description']); + $file = $request->files->get('file'); + if ($file) { + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'import_docs'); + $doc->setFilePath($stored['relativePath']); + $doc->setFileName($stored['originalName']); + $doc->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); + $doc->setFileType($stored['mime']); + } + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$doc->getId()]]); + } + + #[Route('/api/import-workflow/{id}/documents/{docId}/delete', name: 'api_import_workflow_document_delete', methods: ['DELETE'])] + public function deleteDocument(string $id, string $docId, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $doc = $entityManager->getRepository(ImportWorkflowDocument::class)->find((int)$docId); + if (!$doc || $doc->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'سند یافت نشد','Result'=>null], 404); + } + $entityManager->remove($doc); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Stages CRUD + #[Route('/api/import-workflow/{id}/stages/create', name: 'api_import_workflow_stage_create', methods: ['POST'])] + public function createStage(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $st = new ImportWorkflowStage(); + $st->setImportWorkflow($workflow); + $st->setStage($data['stage'] ?? ''); + $st->setStatus($data['status'] ?? 'pending'); + $st->setStartDate(isset($data['startDate']) ? ($this->jalaliToGregorian($data['startDate']) ?? null) : null); + $st->setEndDate(isset($data['endDate']) ? ($this->jalaliToGregorian($data['endDate']) ?? null) : null); + $st->setDescription($data['description'] ?? null); + $st->setAssignedTo($data['assignedTo'] ?? null); + $st->setNotes($data['notes'] ?? null); + $entityManager->persist($st); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]); + } + + #[Route('/api/import-workflow/{id}/stages/{sid}/update', name: 'api_import_workflow_stage_update', methods: ['PUT'])] + public function updateStage(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid); + if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['stage'])) $st->setStage($data['stage']); + if (isset($data['status'])) $st->setStatus($data['status']); + if (isset($data['startDate'])) $st->setStartDate($this->jalaliToGregorian($data['startDate']) ?? null); + if (isset($data['endDate'])) $st->setEndDate($this->jalaliToGregorian($data['endDate']) ?? null); + if (isset($data['description'])) $st->setDescription($data['description']); + if (isset($data['assignedTo'])) $st->setAssignedTo($data['assignedTo']); + if (isset($data['notes'])) $st->setNotes($data['notes']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$st->getId()]]); + } + + #[Route('/api/import-workflow/{id}/stages/{sid}/delete', name: 'api_import_workflow_stage_delete', methods: ['DELETE'])] + public function deleteStage(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $st = $entityManager->getRepository(ImportWorkflowStage::class)->find((int)$sid); + if (!$st || $st->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'مرحله یافت نشد','Result'=>null], 404); + } + $entityManager->remove($st); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Shipping CRUD + #[Route('/api/import-workflow/{id}/shipping/create', name: 'api_import_workflow_shipping_create', methods: ['POST'])] + public function createShipping(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $sh = new ImportWorkflowShipping(); + $sh->setImportWorkflow($workflow); + $sh->setType($data['type'] ?? 'sea'); + $sh->setContainerNumber($data['containerNumber'] ?? null); + $sh->setBillOfLading($data['billOfLading'] ?? null); + $sh->setShippingDate(isset($data['shippingDate']) ? ($this->jalaliToGregorian($data['shippingDate']) ?? null) : null); + $sh->setArrivalDate(isset($data['arrivalDate']) ? ($this->jalaliToGregorian($data['arrivalDate']) ?? null) : null); + $sh->setUnloadingDate(isset($data['unloadingDate']) ? ($this->jalaliToGregorian($data['unloadingDate']) ?? null) : null); + $sh->setShippingCompany($data['shippingCompany'] ?? null); + $sh->setShippingCompanyPhone($data['shippingCompanyPhone'] ?? null); + $sh->setShippingCompanyEmail($data['shippingCompanyEmail'] ?? null); + $sh->setOriginPort($data['originPort'] ?? null); + $sh->setDestinationPort($data['destinationPort'] ?? null); + $sh->setVesselName($data['vesselName'] ?? null); + $sh->setVoyageNumber($data['voyageNumber'] ?? null); + $sh->setDescription($data['description'] ?? null); + $sh->setStatus($data['status'] ?? 'active'); + $entityManager->persist($sh); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]); + } + + #[Route('/api/import-workflow/{id}/shipping/{sid}/update', name: 'api_import_workflow_shipping_update', methods: ['PUT','POST'])] + public function updateShipping(string $id, string $sid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid); + if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['type'])) $sh->setType($data['type']); + if (isset($data['containerNumber'])) $sh->setContainerNumber($data['containerNumber']); + if (isset($data['billOfLading'])) $sh->setBillOfLading($data['billOfLading']); + if (isset($data['shippingDate'])) $sh->setShippingDate($this->jalaliToGregorian($data['shippingDate']) ?? null); + if (isset($data['arrivalDate'])) $sh->setArrivalDate($this->jalaliToGregorian($data['arrivalDate']) ?? null); + if (isset($data['unloadingDate'])) $sh->setUnloadingDate($this->jalaliToGregorian($data['unloadingDate']) ?? null); + if (isset($data['shippingCompany'])) $sh->setShippingCompany($data['shippingCompany']); + if (isset($data['shippingCompanyPhone'])) $sh->setShippingCompanyPhone($data['shippingCompanyPhone']); + if (isset($data['shippingCompanyEmail'])) $sh->setShippingCompanyEmail($data['shippingCompanyEmail']); + if (isset($data['originPort'])) $sh->setOriginPort($data['originPort']); + if (isset($data['destinationPort'])) $sh->setDestinationPort($data['destinationPort']); + if (isset($data['vesselName'])) $sh->setVesselName($data['vesselName']); + if (isset($data['voyageNumber'])) $sh->setVoyageNumber($data['voyageNumber']); + if (isset($data['description'])) $sh->setDescription($data['description']); + if (isset($data['status'])) $sh->setStatus($data['status']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$sh->getId()]]); + } + + #[Route('/api/import-workflow/{id}/shipping/{sid}/delete', name: 'api_import_workflow_shipping_delete', methods: ['DELETE'])] + public function deleteShipping(string $id, string $sid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $sh = $entityManager->getRepository(ImportWorkflowShipping::class)->find((int)$sid); + if (!$sh || $sh->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات حمل یافت نشد','Result'=>null], 404); + } + $entityManager->remove($sh); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Customs CRUD + #[Route('/api/import-workflow/{id}/customs/create', name: 'api_import_workflow_customs_create', methods: ['POST'])] + public function createCustoms(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $c = new ImportWorkflowCustoms(); + $c->setImportWorkflow($workflow); + $c->setDeclarationNumber($data['declarationNumber'] ?? ''); + $c->setCustomsCode($data['customsCode'] ?? null); + $c->setClearanceDate(isset($data['clearanceDate']) ? ($this->jalaliToGregorian($data['clearanceDate']) ?? null) : null); + $c->setCustomsDuty(isset($data['customsDuty']) ? (string)$data['customsDuty'] : null); + $c->setValueAddedTax(isset($data['valueAddedTax']) ? (string)$data['valueAddedTax'] : null); + $c->setOtherCharges(isset($data['otherCharges']) ? (string)$data['otherCharges'] : null); + $c->setTotalCustomsCharges(isset($data['totalCustomsCharges']) ? (string)$data['totalCustomsCharges'] : null); + $c->setCustomsBroker($data['customsBroker'] ?? null); + $c->setCustomsBrokerPhone($data['customsBrokerPhone'] ?? null); + $c->setCustomsBrokerEmail($data['customsBrokerEmail'] ?? null); + $c->setWarehouseNumber($data['warehouseNumber'] ?? null); + $c->setWarehouseLocation($data['warehouseLocation'] ?? null); + $c->setDescription($data['description'] ?? null); + $c->setStatus($data['status'] ?? 'active'); + $entityManager->persist($c); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]); + } + + #[Route('/api/import-workflow/{id}/customs/{cid}/update', name: 'api_import_workflow_customs_update', methods: ['PUT','POST'])] + public function updateCustoms(string $id, string $cid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid); + if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['declarationNumber'])) $c->setDeclarationNumber($data['declarationNumber']); + if (isset($data['customsCode'])) $c->setCustomsCode($data['customsCode']); + if (isset($data['clearanceDate'])) $c->setClearanceDate($this->jalaliToGregorian($data['clearanceDate']) ?? null); + if (isset($data['customsDuty'])) $c->setCustomsDuty((string)$data['customsDuty']); + if (isset($data['valueAddedTax'])) $c->setValueAddedTax((string)$data['valueAddedTax']); + if (isset($data['otherCharges'])) $c->setOtherCharges((string)$data['otherCharges']); + if (isset($data['totalCustomsCharges'])) $c->setTotalCustomsCharges((string)$data['totalCustomsCharges']); + if (isset($data['customsBroker'])) $c->setCustomsBroker($data['customsBroker']); + if (isset($data['customsBrokerPhone'])) $c->setCustomsBrokerPhone($data['customsBrokerPhone']); + if (isset($data['customsBrokerEmail'])) $c->setCustomsBrokerEmail($data['customsBrokerEmail']); + if (isset($data['warehouseNumber'])) $c->setWarehouseNumber($data['warehouseNumber']); + if (isset($data['warehouseLocation'])) $c->setWarehouseLocation($data['warehouseLocation']); + if (isset($data['description'])) $c->setDescription($data['description']); + if (isset($data['status'])) $c->setStatus($data['status']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$c->getId()]]); + } + + #[Route('/api/import-workflow/{id}/customs/{cid}/delete', name: 'api_import_workflow_customs_delete', methods: ['DELETE'])] + public function deleteCustoms(string $id, string $cid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { throw $this->createAccessDeniedException(); } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $c = $entityManager->getRepository(ImportWorkflowCustoms::class)->find((int)$cid); + if (!$c || $c->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'اطلاعات ترخیص یافت نشد','Result'=>null], 404); + } + $entityManager->remove($c); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + // Payments CRUD + #[Route('/api/import-workflow/{id}/payments/create', name: 'api_import_workflow_payment_create', methods: ['POST'])] + public function createPayment(string $id, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + $p = new ImportWorkflowPayment(); + $p->setImportWorkflow($workflow); + $p->setType($data['type'] ?? 'other'); + $p->setAmount((string)($data['amount'] ?? '0')); + $p->setCurrency($data['currency'] ?? 'IRR'); + $p->setAmountIRR(isset($data['amountIRR']) ? (string)$data['amountIRR'] : null); + $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); + $p->setReferenceNumber($data['referenceNumber'] ?? null); + $p->setBankName($data['bankName'] ?? null); + $p->setAccountNumber($data['accountNumber'] ?? null); + $p->setRecipientName($data['recipientName'] ?? null); + $p->setStatus($data['status'] ?? 'pending'); + $p->setDescription($data['description'] ?? null); + $p->setReceiptNumber($data['receiptNumber'] ?? null); + $entityManager->persist($p); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]); + } + + #[Route('/api/import-workflow/{id}/payments/{pid}/update', name: 'api_import_workflow_payment_update', methods: ['PUT'])] + public function updatePayment(string $id, string $pid, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid); + if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404); + } + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['type'])) $p->setType($data['type']); + if (isset($data['amount'])) $p->setAmount((string)$data['amount']); + if (isset($data['currency'])) $p->setCurrency($data['currency']); + if (isset($data['amountIRR'])) $p->setAmountIRR((string)$data['amountIRR']); + if (isset($data['paymentDate'])) $p->setPaymentDate($this->jalaliToGregorian($data['paymentDate']) ?? date('Y-m-d')); + if (isset($data['referenceNumber'])) $p->setReferenceNumber($data['referenceNumber']); + if (isset($data['bankName'])) $p->setBankName($data['bankName']); + if (isset($data['accountNumber'])) $p->setAccountNumber($data['accountNumber']); + if (isset($data['recipientName'])) $p->setRecipientName($data['recipientName']); + if (isset($data['status'])) $p->setStatus($data['status']); + if (isset($data['description'])) $p->setDescription($data['description']); + if (isset($data['receiptNumber'])) $p->setReceiptNumber($data['receiptNumber']); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$p->getId()]]); + } + + #[Route('/api/import-workflow/{id}/payments/{pid}/delete', name: 'api_import_workflow_payment_delete', methods: ['DELETE'])] + public function deletePayment(string $id, string $pid, Access $access, EntityManagerInterface $entityManager): JsonResponse + { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $p = $entityManager->getRepository(ImportWorkflowPayment::class)->find((int)$pid); + if (!$p || $p->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرداخت یافت نشد','Result'=>null], 404); + } + $entityManager->remove($p); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + + #[Route('/api/import-workflow/{id}/items/create', name: 'api_import_workflow_item_create', methods: ['POST'])] + public function createItem( + string $id, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + + $data = json_decode($request->getContent() ?: '{}', true); + $commodityId = isset($data['commodity_id']) ? (int)$data['commodity_id'] : 0; + if ($commodityId <= 0) { + return $this->json(['Success'=>false,'ErrorCode'=>400,'ErrorMessage'=>'کالا انتخاب نشده است','Result'=>null], 400); + } + $commodity = $entityManager->getRepository(Commodity::class)->find($commodityId); + if (!$commodity || $commodity->getBid()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'کالا یافت نشد','Result'=>null], 404); + } + + $item = new ImportWorkflowItem(); + $item->setImportWorkflow($workflow); + $item->setCommodity($commodity); + $item->setName($data['name'] ?? ($commodity->getName() ?? '')); + $item->setProductCode($data['productCode'] ?? ($commodity->getCode() ?? '')); + $item->setBrand($data['brand'] ?? null); + $item->setModel($data['model'] ?? null); + $item->setOriginCountry($data['originCountry'] ?? null); + $item->setQuantity($data['quantity'] ?? '0'); + $item->setUnitPrice($data['unitPrice'] ?? null); + $item->setUnitPriceIRR($data['unitPriceIRR'] ?? null); + $item->setTotalPrice($data['totalPrice'] ?? null); + $item->setTotalPriceIRR($data['totalPriceIRR'] ?? null); + $item->setWeight($data['weight'] ?? null); + $item->setVolume($data['volume'] ?? null); + $item->setDescription($data['description'] ?? null); + $item->setSpecifications($data['specifications'] ?? null); + + $entityManager->persist($item); + $entityManager->flush(); + + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]); + } + + #[Route('/api/import-workflow/{id}/items/{itemId}/update', name: 'api_import_workflow_item_update', methods: ['PUT'])] + public function updateItem( + string $id, + string $itemId, + Request $request, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId); + if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404); + } + + $data = json_decode($request->getContent() ?: '{}', true); + if (isset($data['commodity_id'])) { + $commodity = $entityManager->getRepository(Commodity::class)->find((int)$data['commodity_id']); + if ($commodity && $commodity->getBid()->getId() === $acc['bid']->getId()) { + $item->setCommodity($commodity); + if (!isset($data['name'])) $item->setName($commodity->getName()); + if (!isset($data['productCode'])) $item->setProductCode($commodity->getCode()); + } + } + if (isset($data['name'])) $item->setName($data['name']); + if (isset($data['productCode'])) $item->setProductCode($data['productCode']); + if (isset($data['brand'])) $item->setBrand($data['brand']); + if (isset($data['model'])) $item->setModel($data['model']); + if (isset($data['originCountry'])) $item->setOriginCountry($data['originCountry']); + if (isset($data['quantity'])) $item->setQuantity($data['quantity']); + if (isset($data['unitPrice'])) $item->setUnitPrice($data['unitPrice']); + if (isset($data['unitPriceIRR'])) $item->setUnitPriceIRR($data['unitPriceIRR']); + if (isset($data['totalPrice'])) $item->setTotalPrice($data['totalPrice']); + if (isset($data['totalPriceIRR'])) $item->setTotalPriceIRR($data['totalPriceIRR']); + if (isset($data['weight'])) $item->setWeight($data['weight']); + if (isset($data['volume'])) $item->setVolume($data['volume']); + if (isset($data['description'])) $item->setDescription($data['description']); + if (isset($data['specifications'])) $item->setSpecifications($data['specifications']); + + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>['id'=>$item->getId()]]); + } + + #[Route('/api/import-workflow/{id}/items/{itemId}/delete', name: 'api_import_workflow_item_delete', methods: ['DELETE'])] + public function deleteItem( + string $id, + string $itemId, + Access $access, + EntityManagerInterface $entityManager + ): JsonResponse { + $acc = $access->hasRole('import_workflow'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + $workflow = $entityManager->getRepository(ImportWorkflow::class)->find((int)$id); + if (!$workflow || $workflow->getBusiness()->getId() !== $acc['bid']->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'پرونده واردات یافت نشد','Result'=>null], 404); + } + $item = $entityManager->getRepository(ImportWorkflowItem::class)->find((int)$itemId); + if (!$item || $item->getImportWorkflow()->getId() !== $workflow->getId()) { + return $this->json(['Success'=>false,'ErrorCode'=>404,'ErrorMessage'=>'آیتم یافت نشد','Result'=>null], 404); + } + $entityManager->remove($item); + $entityManager->flush(); + return $this->json(['Success'=>true,'ErrorCode'=>0,'ErrorMessage'=>'','Result'=>true]); + } + #[Route('/api/import-workflow/{id}/update', name: 'api_import_workflow_update', methods: ['PUT'])] public function update( string $id, @@ -497,7 +1055,7 @@ class ImportWorkflowController extends AbstractController $ticket->setType('input'); $ticket->setTypeString('ورود از واردات'); $ticket->setDes('ورود از پرونده واردات #' . $workflow->getCode()); - $ticket->setStatus('in_progress'); + // $ticket->setStatus('in_progress'); $ticket->setImportWorkflowCode($workflow->getCode()); $entityManager->persist($ticket); $entityManager->flush(); @@ -513,4 +1071,41 @@ class ImportWorkflowController extends AbstractController ] ]); } + + private function jalaliToGregorian(?string $input): ?string + { + if (!$input) { + return null; + } + + $s = trim($input); + + // پشتیبانی از جلالی با جداکننده '/' + if (preg_match('/^(\d{4})\/(\d{2})\/(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + + // پشتیبانی از جلالی با جداکننده '-' + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + // اگر سال جلالی باشد (مثلاً 1400) آن را تبدیل کن، در غیر اینصورت همان مقدار را برگردان + if ($y < 1700) { + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + return $s; // احتمالاً میلادی است + } + + return $s; + } } diff --git a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php index 96aca7e..debc191 100644 --- a/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php +++ b/hesabixCore/src/Controller/Plugins/PlugWarrantyController.php @@ -2,6 +2,8 @@ namespace App\Controller\Plugins; +use App\Repository\PlugWarrantySerialRepository; +use Morilog\Jalali\CalendarUtils; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; @@ -10,6 +12,7 @@ use Doctrine\ORM\EntityManagerInterface; use App\Entity\PlugWarrantySerial; use App\Entity\Commodity; use App\Entity\HesabdariDoc; +use App\Entity\Business; use App\Service\Access; use App\Service\Log; use App\Service\Provider; @@ -19,57 +22,98 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use App\Service\PluginService; use App\Service\SMS; use App\Service\registryMGR; +use Symfony\Component\Validator\Constraints as Assert; class PlugWarrantyController extends AbstractController { private $entityManager; - public function __construct(EntityManagerInterface $entityManager) + private function toDate(?string $s): ?\DateTimeImmutable + { + if (!$s) + return null; + try { + return new \DateTimeImmutable($s); + } catch (\Throwable $e) { + return null; + } + } + + private function expiredFlag(?\DateTimeImmutable $end): bool + { + return $end !== null && $end < new \DateTimeImmutable('today'); + } + + public function __construct(EntityManagerInterface $entityManager, private PlugWarrantySerialRepository $repository) { $this->entityManager = $entityManager; } #[Route('/api/plugins/warranty/assign/request', name: 'plugin_warranty_assign_request', methods: ['POST'])] - public function plugin_warranty_assign_request(Request $request, EntityManagerInterface $entityManager, Access $access, PluginService $pluginService): JsonResponse + public function plugin_warranty_assign_request(Request $request, EntityManagerInterface $em, Access $access, PluginService $pluginService): JsonResponse { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) + throw $this->createAccessDeniedException(); + if (!$pluginService->isActive('warranty', $acc['bid'])) + return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); + + $p = json_decode($request->getContent() ?: '{}', true); + $items = $p['items'] ?? []; // [{commodity_id, count, document_id, document_item_id}] + if (!is_array($items) || empty($items)) + return $this->json(['success' => false, 'message' => 'پارامترهای نامعتبر'], 400); + + $em->beginTransaction(); try { - $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) - throw $this->createAccessDeniedException(); + $business = $em->getRepository(Business::class)->find($acc['bid']); + if (!$business) + return $this->json(['success' => false, 'message' => 'کسب‌وکار یافت نشد'], 404); - if (!$pluginService->isActive('warranty', $acc['bid'])) { - return $this->json(['success' => false, 'message' => 'افزونه گارانتی فعال نیست'], 403); - } + $result = []; - $params = json_decode($request->getContent() ?: '{}', true); - // ورودی: commodity_id و count برای هر آیتم حواله - if (!isset($params['items']) || !is_array($params['items'])) { - return $this->json(['error' => 'پارامترهای نامعتبر'], 400); - } + foreach ($items as $i) { + $commodityId = (int) ($i['commodity_id'] ?? 0); + $qty = (int) ($i['count'] ?? 0); + $documentId = isset($i['document_id']) ? (int) $i['document_id'] : null; - $assignmentCodes = []; - foreach ($params['items'] as $index => $item) { - $commodityId = $item['commodity_id'] ?? null; - $count = (int)($item['count'] ?? 0); - if (!$commodityId || $count <= 0) { continue; } + if ($commodityId <= 0 || $qty <= 0) + continue; - // تولید کد یکتای تخصیص (تصادفی) - $assignmentCode = bin2hex(random_bytes(10)); - $assignmentCodes[] = [ + // انتخاب N کد available با قفل + $ids = $em->getConnection()->executeQuery( + "SELECT id FROM plug_warranty_serial + WHERE commodity_id = :cid AND status = 'available' + ORDER BY id ASC + FOR UPDATE SKIP LOCKED + LIMIT :lim", + ['cid' => $commodityId, 'lim' => $qty], + ['cid' => \PDO::PARAM_INT, 'lim' => \PDO::PARAM_INT] + )->fetchFirstColumn(); + + if (count($ids) < $qty) { + $em->rollBack(); + return $this->json(['success' => false, 'message' => 'کد گارانتی کافی برای یکی از اقلام موجود نیست'], 409); + } + + $now = new \DateTimeImmutable(); + $em->createQuery('UPDATE App\Entity\PlugWarrantySerial s SET s.status = :st, s.allocatedToDocumentId = :doc, s.allocatedAt = :at WHERE s.id IN (:ids)') + ->setParameter('st', PlugWarrantySerial::STATUS_ALLOCATED) + ->setParameter('doc', $documentId) + ->setParameter('at', $now) + ->setParameter('ids', $ids) + ->execute(); + + $result[] = [ 'commodity_id' => $commodityId, - 'count' => $count, - 'assignmentCode' => $assignmentCode + 'allocated' => $ids ]; } - return $this->json([ - 'success' => true, - 'codes' => $assignmentCodes - ]); - } catch (\Exception $e) { - return $this->json([ - 'error' => $e->getMessage() - ], 500); + $em->commit(); + return $this->json(['success' => true, 'allocated' => $result]); + } catch (\Throwable $e) { + $em->rollBack(); + return $this->json(['success' => false, 'message' => $e->getMessage()], 500); } } @@ -78,7 +122,7 @@ class PlugWarrantyController extends AbstractController { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); if (!$pluginService->isActive('warranty', $acc['bid'])) { @@ -87,11 +131,13 @@ class PlugWarrantyController extends AbstractController $params = json_decode($request->getContent() ?: '{}', true); $serialNumber = $params['serialNumber'] ?? null; - $assignmentCode = $params['assignmentCode'] ?? null; - $commodityId = $params['commodity_id'] ?? null; // نوع کالای مورد انتظار - $ticketCode = $params['ticketCode'] ?? null; // اختیاری برای ثبت استفاده + $commodityId = isset($params['commodity_id']) ? (int) $params['commodity_id'] : null; + $documentId = isset($params['document_id']) ? (int) $params['document_id'] : null; + $documentItemId = isset($params['document_item_id']) ? (int) $params['document_item_id'] : 0; + $physicalBoxBarcode = $params['physicalBoxBarcode'] ?? ''; + $productTypeCode = $params['productTypeCode'] ?? ''; - if (!$serialNumber || !$assignmentCode || !$commodityId) { + if (!$serialNumber || !$commodityId || !$documentItemId) { return $this->json(['success' => false, 'message' => 'پارامترهای ناقص'], 400); } @@ -99,30 +145,30 @@ class PlugWarrantyController extends AbstractController /** @var PlugWarrantySerial|null $serial */ $serial = $repo->findOneBy([ 'serialNumber' => $serialNumber, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); if (!$serial) { return $this->json(['success' => false, 'message' => 'کد گارانتی یافت نشد']); } - if ($serial->isUsed()) { - return $this->json(['success' => false, 'message' => 'این سریال قبلاً استفاده شده است']); - } - - if (!$serial->getCommodity() || $serial->getCommodity()->getId() !== (int)$commodityId) { + if ($serial->getCommodity()?->getId() !== $commodityId) { return $this->json(['success' => false, 'message' => 'مغایرت نوع محصول']); } - // در این نسخه، صرفاً صحت را تایید می‌کنیم و در صورت ارائه ticketCode، وضعیت را مصرف‌شده می‌کنیم - if ($ticketCode) { - $serial->setUsed(true); - $serial->setUsedAt(date('Y-m-d H:i:s')); - $serial->setUsedTicketCode((string)$ticketCode); - $entityManager->persist($serial); - $entityManager->flush(); + if (!in_array($serial->getStatus(), [PlugWarrantySerial::STATUS_ALLOCATED, PlugWarrantySerial::STATUS_VERIFIED], true)) { + return $this->json(['success' => false, 'message' => 'وضعیت کد برای اسکن معتبر نیست'], 409); } + if ($documentId !== null && $serial->getAllocatedToDocumentId() !== $documentId) { + return $this->json(['success' => false, 'message' => 'کد به سند دیگری تخصیص داده شده'], 409); + } + + // در این نسخه، صرفاً صحت را تایید می‌کنیم و وضعیت را verified می‌کنیم + $serial->setStatus(PlugWarrantySerial::STATUS_VERIFIED); + $entityManager->persist($serial); + $entityManager->flush(); + return $this->json(['success' => true, 'message' => 'تأیید شد']); } catch (\Exception $e) { return $this->json([ @@ -152,26 +198,23 @@ class PlugWarrantyController extends AbstractController return $this->json(['success' => false, 'message' => 'فاکتور یافت نشد'], 404); } - // سریال‌هایی که در حواله‌های مرتبط با این فاکتور استفاده شده‌اند + // سریال‌هایی که به این فاکتور تخصیص داده شده‌اند $serialRepo = $entityManager->getRepository(PlugWarrantySerial::class); $serials = $serialRepo->createQueryBuilder('s') - ->where('s.bid = :bid') - ->andWhere('s.used = true') - ->andWhere('s.usedTicketCode IN ( - SELECT t.code FROM App\\Entity\\StoreroomTicket t WHERE t.doc = :doc - )') + ->where('s.business = :bid') + ->andWhere('s.allocatedToDocumentId = :docId') ->setParameter('bid', $acc['bid']) - ->setParameter('doc', $doc) + ->setParameter('docId', $doc->getId()) ->getQuery() ->getResult(); - $result = array_map(function(PlugWarrantySerial $s) { + $result = array_map(function (PlugWarrantySerial $s) { return [ 'serialNumber' => $s->getSerialNumber(), 'commodity' => $s->getCommodity() ? $s->getCommodity()->getName() : null, - 'usedAt' => $s->getUsedAt(), - 'usedTicketCode' => $s->getUsedTicketCode(), - 'status' => $s->isUsed() ? 'used' : 'free' + 'status' => $s->getStatus(), + 'warrantyEndDate' => $s->getWarrantyEndDate()?->format('Y-m-d'), + 'expired' => $this->expiredFlag($s->getWarrantyEndDate()) ]; }, $serials); @@ -232,7 +275,7 @@ class PlugWarrantyController extends AbstractController { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); /** @var UploadedFile|null $file */ @@ -252,26 +295,33 @@ class PlugWarrantyController extends AbstractController // ردیف اول را عنوان فرض می‌کنیم $isHeader = true; foreach ($rows as $rowIndex => $row) { - if ($isHeader) { $isHeader = false; continue; } - $serialNumber = trim((string)($row['A'] ?? '')); - $commodity = trim((string)($row['B'] ?? '')); - $description = trim((string)($row['C'] ?? '')); - $warrantyStartDate = trim((string)($row['D'] ?? '')); - $warrantyEndDate = trim((string)($row['E'] ?? '')); - $status = trim((string)($row['F'] ?? 'active')); + if ($isHeader) { + $isHeader = false; + continue; + } + $serialNumber = trim((string) ($row['A'] ?? '')); + $commodity = trim((string) ($row['B'] ?? '')); + $description = trim((string) ($row['C'] ?? '')); + $warrantyStartDate = trim((string) ($row['D'] ?? '')); + $warrantyEndDate = trim((string) ($row['E'] ?? '')); + $status = trim((string) ($row['F'] ?? 'available')); if ($serialNumber === '' || $commodity === '') { $errors[] = 'ردیف ' . ($rowIndex) . ': شماره سریال یا محصول خالی است'; continue; } + // نگاشت وضعیت‌های قدیمی به جدید + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $status = $map[$status] ?? 'available'; + $preview[] = [ 'serialNumber' => $serialNumber, 'commodity' => $commodity, 'description' => $description, 'warrantyStartDate' => $warrantyStartDate, 'warrantyEndDate' => $warrantyEndDate, - 'status' => $status ?: 'active' + 'status' => $status ]; } @@ -291,7 +341,7 @@ class PlugWarrantyController extends AbstractController { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); /** @var UploadedFile|null $file */ @@ -307,25 +357,32 @@ class PlugWarrantyController extends AbstractController $serials = []; $isHeader = true; foreach ($rows as $row) { - if ($isHeader) { $isHeader = false; continue; } - $serialNumber = trim((string)($row['A'] ?? '')); - $commodity = trim((string)($row['B'] ?? '')); - $description = trim((string)($row['C'] ?? '')); - $warrantyStartDate = trim((string)($row['D'] ?? '')); - $warrantyEndDate = trim((string)($row['E'] ?? '')); - $status = trim((string)($row['F'] ?? 'active')); + if ($isHeader) { + $isHeader = false; + continue; + } + $serialNumber = trim((string) ($row['A'] ?? '')); + $commodity = trim((string) ($row['B'] ?? '')); + $description = trim((string) ($row['C'] ?? '')); + $warrantyStartDate = trim((string) ($row['D'] ?? '')); + $warrantyEndDate = trim((string) ($row['E'] ?? '')); + $status = trim((string) ($row['F'] ?? 'available')); if ($serialNumber === '' || $commodity === '') { continue; } - // توجه: ستون محصول می‌تواند ID یا CODE یا NAME باشد. اینجا صرفاً پاس‌ترو می‌کنیم و در مرحله نهایی، فرانت با ID نگاشت می‌کند. + + // نگاشت وضعیت‌های قدیمی به جدید + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $status = $map[$status] ?? 'available'; + $serials[] = [ 'serialNumber' => $serialNumber, 'commodity' => $commodity, 'description' => $description, 'warrantyStartDate' => $warrantyStartDate, 'warrantyEndDate' => $warrantyEndDate, - 'status' => $status ?: 'active' + 'status' => $status ]; } @@ -339,64 +396,20 @@ class PlugWarrantyController extends AbstractController } } - private function updateExpiredSerials(EntityManagerInterface $entityManager, $businessId): void - { - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - - $expiredSerials = $repository->createQueryBuilder('s') - ->where('s.bid = :businessId') - ->andWhere('s.status = :status') - ->andWhere('s.warrantyEndDate IS NOT NULL') - ->andWhere('s.warrantyEndDate < :today') - ->setParameter('businessId', $businessId) - ->setParameter('status', 'active') - ->setParameter('today', $today) - ->getQuery() - ->getResult(); - - foreach ($expiredSerials as $serial) { - $serial->setStatus('expired'); - } - - if (!empty($expiredSerials)) { - $entityManager->flush(); - } - } - - private function checkAndUpdateSerialStatus($serial, EntityManagerInterface $entityManager): void - { - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - $warrantyEndDate = $serial->getWarrantyEndDate(); - - if ($serial->getStatus() === 'active' && - $warrantyEndDate && - $warrantyEndDate < $today) { - $serial->setStatus('expired'); - $entityManager->flush(); - } - } - #[Route('/api/plugins/warranty/serials', name: 'plugin_warranty_serials', methods: ['GET'])] - public function plugin_warranty_serials(EntityManagerInterface $entityManager, Access $access, Request $request): JsonResponse + public function plugin_warranty_serials(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Request $request): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $this->updateExpiredSerials($entityManager, $acc['bid']); - $page = $request->query->get('page', 1); $limit = $request->query->get('limit', 20); $status = $request->query->get('status'); $commodityId = $request->query->get('commodity_id'); $search = $request->query->get('search'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - if ($search) { $serials = $repository->searchSerials($acc['bid'], $search); } elseif ($status) { @@ -408,10 +421,13 @@ class PlugWarrantyController extends AbstractController } $data = []; - foreach($serials as $serial){ + foreach ($serials as $serial) { $commodity = $serial->getCommodity(); $submitter = $serial->getSubmitter(); - + $buyer = $serial->getBuyer(); + + $allocatedToDocument = $serial->getAllocatedToDocumentId() ? $entityManager->getRepository(HesabdariDoc::class)->find($serial->getAllocatedToDocumentId()) : null; + $data[] = [ 'id' => $serial->getId(), 'serialNumber' => $serial->getSerialNumber(), @@ -420,16 +436,28 @@ class PlugWarrantyController extends AbstractController 'name' => $commodity->getName(), 'code' => $commodity->getCode() ] : null, - 'dateSubmit' => $serial->getDateSubmit(), + 'dateSubmit' => $serial->getDateSubmit()->format('Y-m-d H:i:s'), 'description' => $serial->getDescription(), - 'warrantyStartDate' => $serial->getWarrantyStartDate(), - 'warrantyEndDate' => $serial->getWarrantyEndDate(), + 'warrantyStartDate' => $serial->getWarrantyStartDate()?->format('Y-m-d'), + 'warrantyEndDate' => $serial->getWarrantyEndDate()?->format('Y-m-d'), 'status' => $serial->getStatus(), + 'activation' => $serial->getActivation(), 'notes' => $serial->getNotes(), + 'expired' => $this->expiredFlag($serial->getWarrantyEndDate()), 'submitter' => $submitter ? [ 'id' => $submitter->getId(), 'name' => $submitter->getFullName() - ] : null + ] : null, + 'commoditySerial' => $serial->getCommoditySerial(), + 'buyer' => $buyer ? [ + 'id' => $buyer->getId(), + 'code' => $buyer->getCode(), + 'name' => $buyer->getName(), + 'nikename' => $buyer->getNikename(), + 'mobile' => $buyer->getMobile() + ] : null, + 'activationAt' => $serial->getActivationAt()?->format('Y-m-d H:i:s'), + 'allocatedToDocumentCode' => $allocatedToDocument ? $allocatedToDocument->getCode() : null ]; } @@ -446,19 +474,19 @@ class PlugWarrantyController extends AbstractController { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - + $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); - - $this->checkAndUpdateSerialStatus($serial, $entityManager); - + + $allocatedToDocument = $serial->getAllocatedToDocumentId() ? $entityManager->getRepository(HesabdariDoc::class)->find($serial->getAllocatedToDocumentId()) : null; + $data = [ 'id' => $serial->getId(), 'serialNumber' => $serial->getSerialNumber(), @@ -467,18 +495,30 @@ class PlugWarrantyController extends AbstractController 'name' => $serial->getCommodity()->getName(), 'code' => $serial->getCommodity()->getCode() ], - 'dateSubmit' => $serial->getDateSubmit(), + 'dateSubmit' => $serial->getDateSubmit()->format('Y-m-d H:i:s'), 'description' => $serial->getDescription(), - 'warrantyStartDate' => $serial->getWarrantyStartDate(), - 'warrantyEndDate' => $serial->getWarrantyEndDate(), + 'warrantyStartDate' => $this->jalaliToGregorian($serial->getWarrantyStartDate()?->format('Y-m-d')), + 'warrantyEndDate' => $this->jalaliToGregorian($serial->getWarrantyEndDate()?->format('Y-m-d')), 'status' => $serial->getStatus(), + 'activation' => $serial->getActivation(), 'notes' => $serial->getNotes(), + 'expired' => $this->expiredFlag($serial->getWarrantyEndDate()), 'submitter' => [ 'id' => $serial->getSubmitter()->getId(), 'name' => $serial->getSubmitter()->getFullName() - ] + ], + 'commoditySerial' => $serial->getCommoditySerial(), + 'buyer' => $serial->getBuyer() ? [ + 'id' => $serial->getBuyer()->getId(), + 'code' => $serial->getBuyer()->getCode(), + 'name' => $serial->getBuyer()->getName(), + 'nikename' => $serial->getBuyer()->getNikename(), + 'mobile' => $serial->getBuyer()->getMobile() + ] : null, + 'activationAt' => $serial->getActivationAt()?->format('Y-m-d H:i:s'), + 'allocatedToDocumentCode' => $allocatedToDocument ? $allocatedToDocument->getCode() : null ]; - + return $this->json($data); } catch (\Exception $e) { return $this->json([ @@ -488,11 +528,11 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/add', name: 'plugin_warranty_serial_add', methods: ['POST'])] - public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse + public function plugin_warranty_serial_add(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $params = []; @@ -500,12 +540,10 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(!array_key_exists('serialNumber', $params) || !array_key_exists('commodity_id', $params)) + if (!array_key_exists('serialNumber', $params) || !array_key_exists('commodity_id', $params)) throw $this->createAccessDeniedException('پارامترهای ناقص'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - - if($repository->isSerialNumberExists($params['serialNumber'], $acc['bid'])) { + if ($repository->isSerialNumberExists($params['serialNumber'], $acc['bid'])) { return $this->json(['error' => 'شماره سریال تکراری است'], 400); } @@ -513,21 +551,27 @@ class PlugWarrantyController extends AbstractController 'id' => $params['commodity_id'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { return $this->json(['error' => 'محصول یافت نشد'], 400); } + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + if (!$business) { + return $this->json(['error' => 'کسب‌وکار یافت نشد'], 404); + } + $serial = new PlugWarrantySerial(); $serial->setSerialNumber($params['serialNumber']); $serial->setCommodity($commodity); - $serial->setBid($acc['bid']); + $serial->setBusiness($business); $serial->setSubmitter($this->getUser()); $serial->setDescription($params['description'] ?? null); - $serial->setWarrantyStartDate($params['warrantyStartDate'] ?? null); - $serial->setWarrantyEndDate($params['warrantyEndDate'] ?? null); - $serial->setStatus($params['status'] ?? 'active'); + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($params['warrantyStartDate']) ?? null)); + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($params['warrantyEndDate']) ?? null)); + $serial->setStatus($params['status'] ?? PlugWarrantySerial::STATUS_AVAILABLE); $serial->setNotes($params['notes'] ?? null); + $serial->setActivation('deactive'); $entityManager->persist($serial); $entityManager->flush(); @@ -552,19 +596,19 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/edit/{id}', name: 'plugin_warranty_serial_edit', methods: ['POST'])] - public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse + public function plugin_warranty_serial_edit(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); $params = []; @@ -572,11 +616,10 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(array_key_exists('serialNumber', $params)) { - $repository = $entityManager->getRepository(PlugWarrantySerial::class); + if (array_key_exists('serialNumber', $params)) { $existingSerial = $repository->createQueryBuilder('p') ->andWhere('p.serialNumber = :serialNumber') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.id != :id') ->setParameter('serialNumber', $params['serialNumber']) ->setParameter('bid', $acc['bid']) @@ -584,46 +627,50 @@ class PlugWarrantyController extends AbstractController ->getQuery() ->getOneOrNullResult(); - if($existingSerial) { + if ($existingSerial) { return $this->json(['error' => 'شماره سریال تکراری است'], 400); } - + $serial->setSerialNumber($params['serialNumber']); } - if(array_key_exists('commodity_id', $params)) { + if (array_key_exists('commodity_id', $params)) { $commodity = $entityManager->getRepository(Commodity::class)->findOneBy([ 'id' => $params['commodity_id'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { return $this->json(['error' => 'محصول یافت نشد'], 400); } - + $serial->setCommodity($commodity); } - if(array_key_exists('description', $params)) { + if (array_key_exists('description', $params)) { $serial->setDescription($params['description']); } - if(array_key_exists('warrantyStartDate', $params)) { - $serial->setWarrantyStartDate($params['warrantyStartDate']); + if (array_key_exists('warrantyStartDate', $params)) { + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($params['warrantyStartDate']))); } - if(array_key_exists('warrantyEndDate', $params)) { - $serial->setWarrantyEndDate($params['warrantyEndDate']); + if (array_key_exists('warrantyEndDate', $params)) { + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($params['warrantyEndDate']))); } - if(array_key_exists('status', $params)) { + if (array_key_exists('status', $params)) { $serial->setStatus($params['status']); } - if(array_key_exists('notes', $params)) { + if (array_key_exists('notes', $params)) { $serial->setNotes($params['notes']); } + if (array_key_exists('activation', $params)) { + $serial->setActivation($params['activation']); + } + $entityManager->flush(); $log->insert( @@ -645,19 +692,19 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/{id}', name: 'plugin_warranty_serial_delete', methods: ['DELETE'])] - public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, Access $access, $id, Log $log): JsonResponse + public function plugin_warranty_serial_delete(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, $id, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ 'id' => $id, - 'bid' => $acc['bid'] + 'business' => $acc['bid'] ]); - - if(!$serial) + + if (!$serial) throw $this->createNotFoundException(); $serialNumber = $serial->getSerialNumber(); @@ -683,11 +730,11 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/serials/bulk-import', name: 'plugin_warranty_serial_bulk_import', methods: ['POST'])] - public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, Access $access, Log $log): JsonResponse + public function plugin_warranty_serial_bulk_import(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); $params = []; @@ -695,36 +742,40 @@ class PlugWarrantyController extends AbstractController $params = json_decode($content, true); } - if(!array_key_exists('serials', $params) || !is_array($params['serials'])) + if (!array_key_exists('serials', $params) || !is_array($params['serials'])) throw $this->createAccessDeniedException('داده‌های نامعتبر'); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); $commodityRepo = $entityManager->getRepository(Commodity::class); - + $business = $entityManager->getRepository(Business::class)->find($acc['bid']); + + if (!$business) { + return $this->json(['error' => 'کسب‌وکار یافت نشد'], 404); + } + $successCount = 0; $errorCount = 0; $errors = []; - foreach($params['serials'] as $index => $serialData) { + foreach ($params['serials'] as $index => $serialData) { try { - if(!array_key_exists('serialNumber', $serialData) || !array_key_exists('commodity_id', $serialData)) { + if (!array_key_exists('serialNumber', $serialData) || !array_key_exists('commodity_code', $serialData)) { $errors[] = "ردیف " . ($index + 1) . ": پارامترهای ناقص"; $errorCount++; continue; } - if($repository->isSerialNumberExists($serialData['serialNumber'], $acc['bid'])) { + if ($repository->isSerialNumberExists($serialData['serialNumber'], $acc['bid'])) { $errors[] = "ردیف " . ($index + 1) . ": شماره سریال تکراری است"; $errorCount++; continue; } $commodity = $commodityRepo->findOneBy([ - 'id' => $serialData['commodity_id'], + 'code' => $serialData['commodity_code'], 'bid' => $acc['bid'] ]); - - if(!$commodity) { + + if (!$commodity) { $errors[] = "ردیف " . ($index + 1) . ": محصول یافت نشد"; $errorCount++; continue; @@ -733,12 +784,17 @@ class PlugWarrantyController extends AbstractController $serial = new PlugWarrantySerial(); $serial->setSerialNumber($serialData['serialNumber']); $serial->setCommodity($commodity); - $serial->setBid($acc['bid']); + $serial->setBusiness($business); $serial->setSubmitter($this->getUser()); $serial->setDescription($serialData['description'] ?? null); - $serial->setWarrantyStartDate($serialData['warrantyStartDate'] ?? null); - $serial->setWarrantyEndDate($serialData['warrantyEndDate'] ?? null); - $serial->setStatus($serialData['status'] ?? 'active'); + $serial->setWarrantyStartDate($this->toDate($this->jalaliToGregorian($serialData['warrantyStartDate']) ?? null)); + $serial->setWarrantyEndDate($this->toDate($this->jalaliToGregorian($serialData['warrantyEndDate']) ?? null)); + $serial->setActivation('deactive'); + + $incomingStatus = $serialData['status'] ?? null; + $map = ['active' => 'available', 'inactive' => 'void', 'expired' => 'void']; + $serial->setStatus($map[$incomingStatus] ?? PlugWarrantySerial::STATUS_AVAILABLE); + $serial->setNotes($serialData['notes'] ?? null); $entityManager->persist($serial); @@ -774,48 +830,42 @@ class PlugWarrantyController extends AbstractController } #[Route('/api/plugins/warranty/stats', name: 'plugin_warranty_stats', methods: ['GET'])] - public function plugin_warranty_stats(EntityManagerInterface $entityManager, Access $access): JsonResponse + public function plugin_warranty_stats(EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $this->updateExpiredSerials($entityManager, $acc['bid']); - - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $allSerials = $repository->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->setParameter('bid', $acc['bid']) ->getQuery() ->getResult(); - + $totalSerials = count($allSerials); - $activeSerials = 0; - $inactiveSerials = 0; - $expiredSerials = 0; - + $byStatus = [ + PlugWarrantySerial::STATUS_AVAILABLE => 0, + PlugWarrantySerial::STATUS_ALLOCATED => 0, + PlugWarrantySerial::STATUS_VERIFIED => 0, + PlugWarrantySerial::STATUS_BOUND => 0, + PlugWarrantySerial::STATUS_CONSUMED => 0, + PlugWarrantySerial::STATUS_VOID => 0, + ]; + $expired = 0; + foreach ($allSerials as $serial) { - $status = $serial->getStatus(); - switch ($status) { - case 'active': - $activeSerials++; - break; - case 'inactive': - $inactiveSerials++; - break; - case 'expired': - $expiredSerials++; - break; - } + $st = $serial->getStatus(); + if (isset($byStatus[$st])) + $byStatus[$st]++; + if ($this->expiredFlag($serial->getWarrantyEndDate())) + $expired++; } return $this->json([ 'totalSerials' => $totalSerials, - 'activeSerials' => $activeSerials, - 'inactiveSerials' => $inactiveSerials, - 'expiredSerials' => $expiredSerials, + 'byStatus' => $byStatus, + 'expiredFlagCount' => $expired ]); } catch (\Exception $e) { return $this->json([ @@ -824,49 +874,141 @@ class PlugWarrantyController extends AbstractController } } - #[Route('/api/plugins/warranty/serials/update-expired', name: 'plugin_warranty_update_expired', methods: ['POST'])] - public function plugin_warranty_update_expired(EntityManagerInterface $entityManager, Access $access): JsonResponse + #[Route('/api/plugins/warranty/serials/bulk-delete', name: 'plugin_warranty_serial_bulk_delete', methods: ['POST'])] + public function plugin_warranty_serial_bulk_delete(Request $request, EntityManagerInterface $entityManager, PlugWarrantySerialRepository $repository, Access $access, Log $log): JsonResponse { try { $acc = $access->hasRole('plugWarrantyManager'); - if(!$acc) + if (!$acc) throw $this->createAccessDeniedException(); - $repository = $entityManager->getRepository(PlugWarrantySerial::class); - $jdate = new Jdate(); - $today = $jdate->GetTodayDate(); - - $expiredSerials = $repository->createQueryBuilder('s') - ->where('s.bid = :businessId') - ->andWhere('s.status = :status') - ->andWhere('s.warrantyEndDate IS NOT NULL') - ->andWhere('s.warrantyEndDate < :today') - ->setParameter('businessId', $acc['bid']) - ->setParameter('status', 'active') - ->setParameter('today', $today) - ->getQuery() - ->getResult(); - - $updatedCount = 0; - - foreach ($expiredSerials as $serial) { - $serial->setStatus('expired'); - $updatedCount++; + $params = json_decode($request->getContent() ?: '{}', true); + $ids = $params['ids'] ?? []; + + if (!is_array($ids) || empty($ids)) { + return $this->json([ + 'success' => false, + 'message' => 'هیچ آیتمی برای حذف انتخاب نشده است' + ], 400); } - - if ($updatedCount > 0) { + + $deletedCount = 0; + $errors = []; + + foreach ($ids as $id) { + try { + $serial = $repository->findOneBy([ + 'id' => $id, + 'business' => $acc['bid'] + ]); + + if ($serial) { + $serialNumber = $serial->getSerialNumber(); + $entityManager->remove($serial); + $deletedCount++; + + $log->insert( + 'گارانتی', + 'حذف گروهی سریال: ' . $serialNumber, + $this->getUser(), + $acc['bid'] + ); + } + } catch (\Exception $e) { + $errors[] = "خطا در حذف سریال با ID {$id}: " . $e->getMessage(); + } + } + + if ($deletedCount > 0) { $entityManager->flush(); } return $this->json([ 'success' => true, - 'message' => 'به‌روزرسانی وضعیت سریال‌های منقضی شده تکمیل شد', - 'updatedCount' => $updatedCount + 'message' => "{$deletedCount} سریال با موفقیت حذف شد", + 'deletedCount' => $deletedCount, + 'errors' => $errors ]); } catch (\Exception $e) { return $this->json([ + 'success' => false, 'error' => $e->getMessage() ], 500); } } -} \ No newline at end of file + + #[Route('/api/plugins/warranty/settings/get', name: 'plugin_warranty_settings_get', methods: ['GET'])] + public function plugin_warranty_settings_get(Access $access, registryMGR $registryMGR): JsonResponse + { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $require = filter_var($registryMGR->get('warranty', 'requireWarrantyOnDelivery'), FILTER_VALIDATE_BOOLEAN); + $grace = (int) ($registryMGR->get('warranty', 'activationGraceDays') ?? 7); + $match = filter_var($registryMGR->get('warranty', 'matchWarrantyToSerial'), FILTER_VALIDATE_BOOLEAN); + return $this->json([ + 'requireWarrantyOnDelivery' => (bool) $require, + 'activationGraceDays' => max(0, $grace), + 'matchWarrantyToSerial' => (bool) $match + ]); + } + + #[Route('/api/plugins/warranty/settings/save', name: 'plugin_warranty_settings_save', methods: ['POST'])] + public function plugin_warranty_settings_save(Request $request, Access $access, registryMGR $registryMGR): JsonResponse + { + $acc = $access->hasRole('plugWarrantyManager'); + if (!$acc) { + throw $this->createAccessDeniedException(); + } + + $params = json_decode($request->getContent() ?: '{}', true); + $require = isset($params['requireWarrantyOnDelivery']) && ($params['requireWarrantyOnDelivery'] === true || $params['requireWarrantyOnDelivery'] === '1' || $params['requireWarrantyOnDelivery'] === 1 || $params['requireWarrantyOnDelivery'] === 'true'); + $graceDays = isset($params['activationGraceDays']) ? (int) $params['activationGraceDays'] : 7; + if ($graceDays < 0) { $graceDays = 0; } + $match = isset($params['matchWarrantyToSerial']) && ($params['matchWarrantyToSerial'] === true || $params['matchWarrantyToSerial'] === '1' || $params['matchWarrantyToSerial'] === 1 || $params['matchWarrantyToSerial'] === 'true'); + + $registryMGR->update('warranty', 'requireWarrantyOnDelivery', $require ? '1' : '0'); + $registryMGR->update('warranty', 'activationGraceDays', (string) $graceDays); + $registryMGR->update('warranty', 'matchWarrantyToSerial', $match ? '1' : '0'); + return $this->json(['success' => true]); + } + + private function jalaliToGregorian(?string $input): ?string + { + if (!$input) { + return null; + } + + $s = trim($input); + + // پشتیبانی از جلالی با جداکننده '/' + if (preg_match('/^(\d{4})\/(\d{2})\/(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + + // پشتیبانی از جلالی با جداکننده '-' + if (preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $s, $m)) { + $y = (int)$m[1]; $mo = (int)$m[2]; $d = (int)$m[3]; + // اگر سال جلالی باشد (مثلاً 1400) آن را تبدیل کن، در غیر اینصورت همان مقدار را برگردان + if ($y < 1700) { + try { + $g = CalendarUtils::toGregorian($y, $mo, $d); + return sprintf('%04d-%02d-%02d', $g[0], $g[1], $g[2]); + } catch (\Throwable $e) { + return $s; + } + } + return $s; // احتمالاً میلادی است + } + + return $s; + } +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/PublicController.php b/hesabixCore/src/Controller/PublicController.php new file mode 100644 index 0000000..04b87a8 --- /dev/null +++ b/hesabixCore/src/Controller/PublicController.php @@ -0,0 +1,400 @@ +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])); +} \ No newline at end of file diff --git a/hesabixCore/src/Controller/SellController.php b/hesabixCore/src/Controller/SellController.php index 240d207..23d56bf 100644 --- a/hesabixCore/src/Controller/SellController.php +++ b/hesabixCore/src/Controller/SellController.php @@ -230,6 +230,16 @@ class SellController extends AbstractController $doc->setSubmitter($this->getUser()); $doc->setMoney($acc['money']); $doc->setCode($provider->getAccountingCode($acc['bid'], 'accounting')); + + // Set approval fields based on business settings + $business = $acc['bid']; + if ($business->isRequireTwoStepApproval()) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + } else { + $doc->setIsPreview(false); + $doc->setIsApproved(true); + } } if ($params['transferCost'] != 0) { $hesabdariRow = new HesabdariRow(); @@ -322,13 +332,17 @@ class SellController extends AbstractController $hesabdariRow->setPerson($person); $entityManager->persist($hesabdariRow); - // Two-step approval: اگر پرمیشن کسب‌وکار تأیید دو مرحله‌ای فروش را الزامی کرده باشد - $permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]); - $personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false; - if (($permission && $permission->isRequireTwoStepSell()) || $personRequire) { - $doc->setStatus('pending_approval'); + // Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + $doc->setApprovedBy(null); } else { - $doc->setStatus('approved'); + $doc->setIsPreview(false); + $doc->setIsApproved(true); + $doc->setApprovedBy($this->getUser()); } $entityManager->persist($doc); $entityManager->flush(); @@ -467,10 +481,13 @@ class SellController extends AbstractController $queryBuilder = $entityManager->createQueryBuilder() ->select('DISTINCT d.id, d.dateSubmit, d.date, d.type, d.code, d.des, d.amount') + ->addSelect('d.isPreview, d.isApproved') ->addSelect('u.fullName as submitter') + ->addSelect('approver.fullName as approvedByName, approver.id as approvedById, approver.email as approvedByEmail') ->addSelect('l.code as labelCode, l.label as labelLabel') ->from(HesabdariDoc::class, 'd') ->leftJoin('d.submitter', 'u') + ->leftJoin('d.approvedBy', 'approver') ->leftJoin('d.InvoiceLabel', 'l') ->leftJoin('d.hesabdariRows', 'r') ->where('d.bid = :bid') @@ -532,7 +549,9 @@ class SellController extends AbstractController 'plugin' => 'd.plugin', 'refData' => 'd.refData', 'shortlink' => 'd.shortlink', - 'status' => 'd.status', + 'isPreview' => 'd.isPreview', + 'isApproved' => 'd.isApproved', + 'approvedBy' => 'd.approvedBy', 'submitter' => 'u.fullName', 'label' => 'l.label', // از InvoiceLabel ]; @@ -578,6 +597,13 @@ class SellController extends AbstractController 'code' => $doc['labelCode'], 'label' => $doc['labelLabel'] ] : null, + 'isPreview' => $doc['isPreview'], + 'isApproved' => $doc['isApproved'], + 'approvedBy' => $doc['approvedByName'] ? [ + 'fullName' => $doc['approvedByName'], + 'id' => $doc['approvedById'], + 'email' => $doc['approvedByEmail'] + ] : null, ]; $mainRow = $entityManager->getRepository(HesabdariRow::class) @@ -1234,6 +1260,19 @@ class SellController extends AbstractController $hesabdariRow->setPerson($person); $entityManager->persist($hesabdariRow); + // Two-step approval: اگر کسب‌وکار تأیید دو مرحله‌ای را الزامی کرده باشد + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $doc->setIsPreview(true); + $doc->setIsApproved(false); + $doc->setApprovedBy(null); + } else { + $doc->setIsPreview(false); + $doc->setIsApproved(true); + $doc->setApprovedBy($this->getUser()); + } + // ذخیره فاکتور $entityManager->persist($doc); $entityManager->flush(); @@ -1320,13 +1359,13 @@ class SellController extends AbstractController $entityManager->persist($receiveRow); // Two-step approval برای دریافت/پرداخت - $permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]); - $personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false; - if (($permission && $permission->isRequireTwoStepPayment()) || $personRequire) { - $paymentDoc->setStatus('pending_approval'); - } else { - $paymentDoc->setStatus('approved'); - } + // $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + // $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + // if ($businessRequire) { + // $paymentDoc->setStatus('pending_approval'); + // } else { + // $paymentDoc->setStatus('approved'); + // } $entityManager->persist($paymentDoc); } $entityManager->flush(); diff --git a/hesabixCore/src/Controller/StoreroomController.php b/hesabixCore/src/Controller/StoreroomController.php index 673cad1..9d68632 100644 --- a/hesabixCore/src/Controller/StoreroomController.php +++ b/hesabixCore/src/Controller/StoreroomController.php @@ -57,7 +57,7 @@ class StoreroomController extends AbstractController } #[Route('/api/storeroom/ticket/attachments/upload/{code}', name: 'app_storeroom_ticket_upload_attachment', methods: ['POST'])] - public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager): JsonResponse + public function uploadTicketAttachment(string $code, Request $request, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): JsonResponse { $acc = $access->hasRole('store'); if (!$acc) throw $this->createAccessDeniedException(); @@ -68,23 +68,21 @@ class StoreroomController extends AbstractController if (!$file) { return $this->json(['result'=>-1,'message'=>'فایل ارسال نشده است'], 400); } - $uploadDir = __DIR__ . '/../../../public_html/uploads/storeroom/'; - if (!is_dir($uploadDir)) @mkdir($uploadDir, 0775, true); - $safeName = uniqid('st_', true) . '_' . preg_replace('/[^A-Za-z0-9_\.-]/','_', $file->getClientOriginalName()); - $file->move($uploadDir, $safeName); + // Store securely in var/storage + $stored = $storage->store($file, (string)$acc['bid']->getId(), 'storeroom_attachments'); $archive = new ArchiveFile(); $archive->setBid($acc['bid']); $archive->setSubmitter($acc['user']); $archive->setDateSubmit(date('Y-m-d H:i:s')); - $archive->setFilename('/uploads/storeroom/' . $safeName); + $archive->setFilename($stored['relativePath']); $archive->setCat('storeroom_ticket'); - $archive->setFileType($file->getClientMimeType() ?: 'application/octet-stream'); + $archive->setFileType($stored['mime'] ?: 'application/octet-stream'); $archive->setPublic(false); $archive->setDes($request->request->get('des')); $archive->setRelatedDocType('storeroom_ticket'); $archive->setRelatedDocCode($ticket->getCode()); - $archive->setFileSize((string) $file->getSize()); + $archive->setFileSize($stored['size'] !== null ? (string)$stored['size'] : null); $entityManager->persist($archive); $entityManager->flush(); @@ -115,6 +113,28 @@ class StoreroomController extends AbstractController }, $items)); } + #[Route('/api/storeroom/ticket/attachments/download/{id}', name: 'app_storeroom_ticket_download_attachment', methods: ['GET'])] + public function downloadTicketAttachment(int $id, Access $access, EntityManagerInterface $entityManager, \App\Service\FileStorage $storage): Response + { + $acc = $access->hasRole('store'); + if (!$acc) throw $this->createAccessDeniedException(); + $a = $entityManager->getRepository(ArchiveFile::class)->find($id); + if (!$a || $a->getBid()->getId() !== $acc['bid']->getId()) { + throw $this->createNotFoundException('فایل یافت نشد'); + } + $abs = $storage->absolutePath((string)$a->getFilename()); + if (!is_file($abs) || !is_readable($abs)) { + throw $this->createNotFoundException('فایل موجود نیست'); + } + $response = new \Symfony\Component\HttpFoundation\BinaryFileResponse($abs); + $response->setContentDisposition( + \Symfony\Component\HttpFoundation\ResponseHeaderBag::DISPOSITION_ATTACHMENT, + basename($abs) + ); + $response->headers->set('Content-Type', $a->getFileType() ?: 'application/octet-stream'); + return $response; + } + #[Route('/api/storeroom/mod/{code}', name: 'app_storeroom_mod')] public function app_storeroom_mod(Provider $provider, Request $request, Access $access, Log $log, EntityManagerInterface $entityManager, $code = 0): JsonResponse { @@ -195,8 +215,12 @@ class StoreroomController extends AbstractController foreach ($buys as $buy) { $temp = $provider->Entity2Array($buy, 0); $person = $this->getPerson($buy); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($buy, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $buy, $entityManager); @@ -216,8 +240,12 @@ class StoreroomController extends AbstractController foreach ($sells as $sell) { $temp = $provider->Entity2Array($sell, 0); $person = $this->getPerson($sell); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($sell, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $sell, $entityManager); @@ -237,8 +265,12 @@ class StoreroomController extends AbstractController foreach ($rfsells as $sell) { $temp = $provider->Entity2Array($sell, 0); $person = $this->getPerson($sell); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($sell, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $sell, $entityManager); @@ -258,8 +290,12 @@ class StoreroomController extends AbstractController foreach ($rfbuys as $buy) { $temp = $provider->Entity2Array($buy, 0); $person = $this->getPerson($buy); - $temp['person'] = Explore::ExplorePerson($person); - $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $temp['person'] = Explore::ExplorePerson($person); + $temp['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $temp['person'] = null; + } $temp['commodities'] = $this->getCommodities($buy, $provider); //check storeroom exist $this->calcStoreRemaining($temp, $buy, $entityManager); @@ -342,8 +378,12 @@ class StoreroomController extends AbstractController } } $res = $provider->Entity2Array($doc, 0); - $res['person'] = $provider->Entity2Array($person, 0); - $res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + if ($person) { + $res['person'] = $provider->Entity2Array($person, 0); + $res['person']['des'] = ' # ' . $person->getCode() . ' ' . $person->getNikename(); + } else { + $res['person'] = null; + } $res['commodities'] = $provider->ArrayEntity2Array($commodities, 1, ['doc', 'bid', 'year']); //calculate rows data $this->calcStoreRemaining($res, $doc, $entityManager); @@ -464,6 +504,10 @@ class StoreroomController extends AbstractController $ticket->setTransfer($params['ticket']['transfer']); $ticket->setYear($acc['year']); $ticket->setCode($provider->getAccountingCode($acc['bid'], 'storeroom')); + $alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $rand = ''; + for ($i = 0; $i < 8; $i++) { $rand .= $alphabet[random_int(0, strlen($alphabet)-1)]; } + $ticket->setActivationCode($rand); $ticket->setReceiver($params['ticket']['receiver']); $ticket->setTransferType($transferType); $ticket->setReferral($params['ticket']['referral']); @@ -472,9 +516,6 @@ class StoreroomController extends AbstractController $ticket->setType($params['ticket']['type']); $ticket->setTypeString($params['ticket']['typeString']); $ticket->setDes($params['ticket']['des']); - // وضعیت اولیه - $ticket->setStatus('in_progress'); - // اگر از پرونده واردات آمده if (array_key_exists('importWorkflowCode', $params['ticket'])) { $ticket->setImportWorkflowCode($params['ticket']['importWorkflowCode']); } @@ -485,37 +526,22 @@ class StoreroomController extends AbstractController $docRows = $entityManager->getRepository(HesabdariRow::class)->findBy([ 'doc' => $doc ]); - // بررسی الزام سریال: اگر در بدنه درخواست گزینه requireWarrantySerial=true باشد، قبل از ثبت نهایی، کف سریال‌های آزاد کالا را چک می‌کنیم - $requireWarrantySerial = isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true; + + // Determine if warranty serials are required based on flag or provided lines + $hasSerialLines = false; + foreach (($params['items'] ?? []) as $it) { + if (!empty($it['serialLines']) && is_array($it['serialLines'])) { $hasSerialLines = true; break; } + } + $requireWarrantySerial = (isset($params['ticket']['requireWarrantySerial']) && $params['ticket']['requireWarrantySerial'] === true) || $hasSerialLines; if ($requireWarrantySerial) { if (!$pluginService->isActive('warranty', $acc['bid'])) { - return $this->json([ - 'result' => -5, - 'message' => 'افزونه گارانتی فعال نیست' - ], 403); + return $this->json(['result' => -5, 'message' => 'افزونه گارانتی فعال نیست'], 403); } - } - if ($requireWarrantySerial) { + // Validate counts up-front foreach ($params['items'] as $item) { - $row = $entityManager->getRepository(HesabdariRow::class)->findOneBy([ - 'bid' => $acc['bid'], - 'doc' => $doc, - 'id' => $item['id'], - ]); - if (!$row || !$row->getCommodity()) - throw $this->createNotFoundException('کالا یافت نشد!'); - $commodity = $row->getCommodity(); - $freeSerialCount = $entityManager->getRepository(PlugWarrantySerial::class)->count([ - 'bid' => $acc['bid'], - 'commodity' => $commodity, - 'used' => null, - 'status' => 'active' - ]); - if ($freeSerialCount < (int)$item['ticketCount']) { - return $this->json([ - 'result' => -2, - 'message' => 'تعداد سریال گارانتی آزاد برای کالا کافی نیست' - ], 400); + $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; + if ((int)($item['ticketCount'] ?? 0) > 0 && count($lines) < (int)$item['ticketCount']) { + return $this->json(['result' => -3, 'message' => 'تعداد سریال/گارانتی با تعداد حواله همخوانی ندارد'], 400); } } } @@ -543,44 +569,57 @@ class StoreroomController extends AbstractController $ticketItem->setCommodity($row->getCommodity()); $ticketItem->setType($item['type']); $entityManager->persist($ticketItem); - // اگر الزام سریال فعال است و سریال‌ها هم در همین درخواست آمده‌اند، آن‌ها را مصرف کنیم - if ($requireWarrantySerial && isset($item['warrantySerials']) && is_array($item['warrantySerials'])) { - $serials = $item['warrantySerials']; - if (count($serials) != (int)$item['ticketCount']) { - return $this->json([ - 'result' => -3, - 'message' => 'تعداد سریال‌های ارسالی با تعداد حواله همخوانی ندارد' - ], 400); - } - foreach ($serials as $serialNumber) { - /** @var PlugWarrantySerial|null $serial */ - $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ - 'bid' => $acc['bid'], - 'serialNumber' => $serialNumber, - 'commodity' => $row->getCommodity(), - 'status' => 'active' - ]); - if (!$serial || $serial->isUsed()) { - return $this->json([ - 'result' => -4, - 'message' => 'سریال نامعتبر یا قبلاً مصرف شده: ' . $serialNumber - ], 400); + + // Bind warranty serials per item if provided + if ($requireWarrantySerial) { + $lines = isset($item['serialLines']) && is_array($item['serialLines']) ? $item['serialLines'] : []; + if ((int)$item['ticketCount'] > 0) { + // Ensure we have an id to bind to + $entityManager->flush(); + $lines = array_slice($lines, 0, (int)$item['ticketCount']); + foreach ($lines as $ln) { + $warrantyCode = $ln['warranty'] ?? null; + $deviceSerial = $ln['serial'] ?? null; + if (!$warrantyCode) { + return $this->json(['result' => -4, 'message' => 'کد گارانتی ارسال نشده است'], 400); + } + /** @var PlugWarrantySerial|null $serial */ + $serial = $entityManager->getRepository(PlugWarrantySerial::class)->findOneBy([ + 'business' => $acc['bid'], + 'serialNumber' => $warrantyCode, + 'commodity' => $row->getCommodity(), + ]); + if (!$serial || $serial->getStatus() !== PlugWarrantySerial::STATUS_AVAILABLE) { + return $this->json(['result' => -4, 'message' => 'گارانتی نامعتبر یا آزاد نیست: ' . $warrantyCode], 400); + } + $serial->setStatus(PlugWarrantySerial::STATUS_CONSUMED); + $serial->setCommoditySerial($deviceSerial); + $serial->setBuyer($person); + $serial->setAllocatedToDocumentId($doc->getId()); + $serial->setAllocatedAt(new \DateTimeImmutable()); + $serial->setBoundToItemId($ticketItem->getId()); + $serial->setBoundAt(new \DateTimeImmutable()); + $serial->setActivationTicketCode($ticket->getCode()); + $serial->setActivationTicketSecret($ticket->getActivationCode()); + $entityManager->persist($serial); } - $serial->setUsed(true); - $serial->setUsedAt(date('Y-m-d H:i:s')); - $serial->setUsedTicketCode($ticket->getCode()); - $entityManager->persist($serial); } } + } + $entityManager->flush(); - // اگر تأیید دو مرحله‌ای حواله فعال باشد، وضعیت را pending_approval بگذاریم - $permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]); - $personRequire = $person && method_exists($person, 'isRequireTwoStep') ? (bool)$person->isRequireTwoStep() : false; - if (($permission && $permission->isRequireTwoStepStore()) || $personRequire) { - $ticket->setStatus('pending_approval'); - $entityManager->persist($ticket); - $entityManager->flush(); + + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + $ticket->setIsPreview(true); + $ticket->setIsApproved(false); + $ticket->setApprovedBy(null); // هنوز تأیید نشده + } else { + $ticket->setIsPreview(false); + $ticket->setIsApproved(true); + $ticket->setApprovedBy($this->getUser()); // تأیید شده توسط کاربر فعلی } //save logs $log->insert('انبارداری', 'حواله انبار با شماره ' . $ticket->getCode() . ' اضافه / ویرایش شد.', $this->getUser(), $acc['bid']); @@ -660,7 +699,7 @@ class StoreroomController extends AbstractController if (!$ticket) { throw $this->createNotFoundException('حواله یافت نشد.'); } - $ticket->setStatus($status); + // $ticket->setStatus($status); $entityManager->persist($ticket); $entityManager->flush(); return $this->json(['result' => 0]); @@ -687,7 +726,6 @@ class StoreroomController extends AbstractController 'date' => $t->getDate(), 'type' => $t->getType(), 'typeString' => $t->getTypeString(), - 'status' => $t->getStatus(), 'importWorkflowCode' => $t->getImportWorkflowCode(), 'person' => $t->getPerson() ? $t->getPerson()->getNikename() : null, 'storeroom' => $t->getStoreroom() ? $t->getStoreroom()->getName() : null, @@ -708,15 +746,33 @@ class StoreroomController extends AbstractController ], [ 'date' => 'DESC' ]); - return $this->json($provider->ArrayEntity2ArrayJustIncludes($tickets, [ + $result = $provider->ArrayEntity2ArrayJustIncludes($tickets, [ 'getDes', 'getCode', 'getDate', 'getPerson', 'getNikename', 'getDoc', - 'getTypeString' - ])); + 'getTypeString', + 'isPreview', + 'isApproved' + ], 2); + + foreach ($result as $key => &$ticket) { + $ticketEntity = $tickets[$key]; + if ($ticketEntity->getApprovedBy()) { + $approvedBy = $ticketEntity->getApprovedBy(); + $ticket['approvedBy'] = [ + 'id' => $approvedBy->getId(), + 'fullName' => $approvedBy->getFullName(), + 'email' => $approvedBy->getEmail() + ]; + } else { + $ticket['approvedBy'] = null; + } + } + + return $this->json($result); } #[Route('/api/storeroom/tickets/info/{code}', name: 'app_storeroom_ticket_view')] @@ -734,7 +790,7 @@ class StoreroomController extends AbstractController //get items $items = $entityManager->getRepository(StoreroomItem::class)->findBy(['ticket' => $ticket]); $res = []; - $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString'], 2); + $res['ticket'] = $provider->Entity2ArrayJustIncludes($ticket, ['getStoreroom', 'getManager', 'getDate', 'getSubmitDate', 'getDes', 'getReceiver', 'getTransfer', 'getCode', 'getType', 'getReferral', 'getTypeString', 'isPreview', 'isApproved'], 2); $res['transferType'] = $provider->Entity2ArrayJustIncludes($ticket->getTransferType(), ['getName'], 0); $res['person'] = $provider->Entity2ArrayJustIncludes($ticket->getPerson(), ['getKeshvar', 'getOstan', 'getShahr', 'getAddress', 'getNikename', 'getCodeeghtesadi', 'getPostalcode', 'getName', 'getTel', 'getSabt'], 0); //get rows @@ -851,10 +907,12 @@ class StoreroomController extends AbstractController } else { $title = 'حواله خروج از انبار'; } - // جلوگیری از چاپ پیش از تایید در صورت نیاز - $permission = $entityManager->getRepository(\App\Entity\Permission::class)->findOneBy(['bid' => $acc['bid'], 'user' => $acc['user']]); - if ($permission && $permission->isRequireTwoStepStore()) { - if ($doc->getStatus() !== 'approved' && $doc->getStatus() !== 'done') { + + $business = $entityManager->getRepository(\App\Entity\Business::class)->find($acc['bid']); + $businessRequire = $business && method_exists($business, 'isRequireTwoStepApproval') ? (bool)$business->isRequireTwoStepApproval() : false; + if ($businessRequire) { + // بررسی وضعیت تأیید از طریق StoreroomTicket + if ($doc->isPreview()) { return $this->json(['result' => -10, 'message' => 'حواله هنوز تایید نشده است'], 403); } } diff --git a/hesabixCore/src/Entity/Business.php b/hesabixCore/src/Entity/Business.php index 39e6b28..68263fb 100644 --- a/hesabixCore/src/Entity/Business.php +++ b/hesabixCore/src/Entity/Business.php @@ -313,6 +313,18 @@ class Business #[ORM\OneToMany(mappedBy: 'business', targetEntity: ImportWorkflow::class, orphanRemoval: true)] private Collection $importWorkflows; + #[ORM\Column(nullable: true)] + private ?bool $requireTwoStepApproval = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $invoiceApprover = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $warehouseApprover = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $financialApprover = null; + public function __construct() { $this->logs = new ArrayCollection(); @@ -2198,4 +2210,48 @@ class Business } return $this; } + + public function isRequireTwoStepApproval(): ?bool + { + return $this->requireTwoStepApproval; + } + + public function setRequireTwoStepApproval(?bool $requireTwoStepApproval): static + { + $this->requireTwoStepApproval = $requireTwoStepApproval; + return $this; + } + + public function getInvoiceApprover(): ?string + { + return $this->invoiceApprover; + } + + public function setInvoiceApprover(?string $invoiceApprover): static + { + $this->invoiceApprover = $invoiceApprover; + return $this; + } + + public function getWarehouseApprover(): ?string + { + return $this->warehouseApprover; + } + + public function setWarehouseApprover(?string $warehouseApprover): static + { + $this->warehouseApprover = $warehouseApprover; + return $this; + } + + public function getFinancialApprover(): ?string + { + return $this->financialApprover; + } + + public function setFinancialApprover(?string $financialApprover): static + { + $this->financialApprover = $financialApprover; + return $this; + } } diff --git a/hesabixCore/src/Entity/HesabdariDoc.php b/hesabixCore/src/Entity/HesabdariDoc.php index c7f33c9..b3b86ff 100644 --- a/hesabixCore/src/Entity/HesabdariDoc.php +++ b/hesabixCore/src/Entity/HesabdariDoc.php @@ -726,4 +726,48 @@ class HesabdariDoc return $this; } + + // Approval fields + #[ORM\Column(nullable: true)] + private ?bool $isPreview = null; + + #[ORM\Column(nullable: true)] + private ?bool $isApproved = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?User $approvedBy = null; + + public function isPreview(): ?bool + { + return $this->isPreview; + } + + public function setIsPreview(?bool $isPreview): static + { + $this->isPreview = $isPreview; + return $this; + } + + public function isApproved(): ?bool + { + return $this->isApproved; + } + + public function setIsApproved(?bool $isApproved): static + { + $this->isApproved = $isApproved; + return $this; + } + + public function getApprovedBy(): ?User + { + return $this->approvedBy; + } + + public function setApprovedBy(?User $approvedBy): static + { + $this->approvedBy = $approvedBy; + return $this; + } } \ No newline at end of file diff --git a/hesabixCore/src/Entity/HesabdariRow.php b/hesabixCore/src/Entity/HesabdariRow.php index 6e91683..b138312 100644 --- a/hesabixCore/src/Entity/HesabdariRow.php +++ b/hesabixCore/src/Entity/HesabdariRow.php @@ -368,4 +368,6 @@ class HesabdariRow return $this; } + + } \ No newline at end of file diff --git a/hesabixCore/src/Entity/ImportWorkflowItem.php b/hesabixCore/src/Entity/ImportWorkflowItem.php index 19a0b2b..aa3dd68 100644 --- a/hesabixCore/src/Entity/ImportWorkflowItem.php +++ b/hesabixCore/src/Entity/ImportWorkflowItem.php @@ -6,6 +6,7 @@ use App\Repository\ImportWorkflowItemRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Ignore; +use App\Entity\Commodity; #[ORM\Entity(repositoryClass: ImportWorkflowItemRepository::class)] class ImportWorkflowItem @@ -20,6 +21,10 @@ class ImportWorkflowItem #[Ignore] private ?ImportWorkflow $importWorkflow = null; + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?Commodity $commodity = null; + #[ORM\Column(length: 255)] private ?string $name = null; @@ -86,6 +91,17 @@ class ImportWorkflowItem return $this; } + public function getCommodity(): ?Commodity + { + return $this->commodity; + } + + public function setCommodity(?Commodity $commodity): static + { + $this->commodity = $commodity; + return $this; + } + public function getName(): ?string { return $this->name; diff --git a/hesabixCore/src/Entity/Permission.php b/hesabixCore/src/Entity/Permission.php index cb3a0b2..48ac1a0 100644 --- a/hesabixCore/src/Entity/Permission.php +++ b/hesabixCore/src/Entity/Permission.php @@ -147,15 +147,6 @@ class Permission #[ORM\Column(nullable: true)] private ?bool $importWorkflow = null; - #[ORM\Column(nullable: true)] - private ?bool $requireTwoStepSell = null; - - #[ORM\Column(nullable: true)] - private ?bool $requireTwoStepPayment = null; - - #[ORM\Column(nullable: true)] - private ?bool $requireTwoStepStore = null; - public function getId(): ?int { return $this->id; @@ -688,37 +679,4 @@ class Permission return $this; } - - public function isRequireTwoStepSell(): ?bool - { - return $this->requireTwoStepSell; - } - - public function setRequireTwoStepSell(?bool $requireTwoStepSell): static - { - $this->requireTwoStepSell = $requireTwoStepSell; - return $this; - } - - public function isRequireTwoStepPayment(): ?bool - { - return $this->requireTwoStepPayment; - } - - public function setRequireTwoStepPayment(?bool $requireTwoStepPayment): static - { - $this->requireTwoStepPayment = $requireTwoStepPayment; - return $this; - } - - public function isRequireTwoStepStore(): ?bool - { - return $this->requireTwoStepStore; - } - - public function setRequireTwoStepStore(?bool $requireTwoStepStore): static - { - $this->requireTwoStepStore = $requireTwoStepStore; - return $this; - } -} +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/Person.php b/hesabixCore/src/Entity/Person.php index 19ac6cb..13e26f0 100644 --- a/hesabixCore/src/Entity/Person.php +++ b/hesabixCore/src/Entity/Person.php @@ -161,8 +161,7 @@ class Person #[ORM\Column(type: Types::TEXT, nullable: true)] private ?string $tags = null; - #[ORM\Column(nullable: true)] - private ?bool $requireTwoStep = null; + public function __construct() { @@ -917,14 +916,5 @@ class Person return $this; } - public function isRequireTwoStep(): ?bool - { - return $this->requireTwoStep; - } - public function setRequireTwoStep(?bool $requireTwoStep): self - { - $this->requireTwoStep = $requireTwoStep; - return $this; - } } diff --git a/hesabixCore/src/Entity/PlugWarrantySerial.php b/hesabixCore/src/Entity/PlugWarrantySerial.php index d055782..a557047 100644 --- a/hesabixCore/src/Entity/PlugWarrantySerial.php +++ b/hesabixCore/src/Entity/PlugWarrantySerial.php @@ -2,212 +2,177 @@ namespace App\Entity; -use App\Repository\PlugWarrantySerialRepository; use Doctrine\ORM\Mapping as ORM; -use Symfony\Component\Serializer\Annotation\Ignore; -#[ORM\Entity(repositoryClass: PlugWarrantySerialRepository::class)] +#[ORM\Entity] +#[ORM\Table(name: 'plug_warranty_serial')] +#[ORM\UniqueConstraint(name: 'uniq_warranty_serial', columns: ['serial_number'])] +#[ORM\Index(name: 'idx_status_product', columns: ['status', 'commodity_id'])] +#[ORM\Index(name: 'idx_alloc_doc', columns: ['allocated_to_document_id'])] class PlugWarrantySerial { + public const STATUS_AVAILABLE = 'available'; + public const STATUS_ALLOCATED = 'allocated'; + public const STATUS_VERIFIED = 'verified'; + public const STATUS_BOUND = 'bound'; + public const STATUS_CONSUMED = 'consumed'; + public const STATUS_VOID = 'void'; + #[ORM\Id] #[ORM\GeneratedValue] - #[ORM\Column] + #[ORM\Column(type: 'integer')] private ?int $id = null; #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] #[ORM\JoinColumn(nullable: false)] - #[Ignore] - private ?Business $bid = null; + private ?Business $business = null; #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] #[ORM\JoinColumn(nullable: false)] - #[Ignore] private ?Commodity $commodity = null; - #[ORM\Column(length: 255, unique: true)] + #[ORM\Column(name: 'serial_number', length: 255, unique: true)] private ?string $serialNumber = null; - #[ORM\Column(length: 25)] - private ?string $dateSubmit = null; + #[ORM\Column(type: 'datetime_immutable')] + private \DateTimeImmutable $dateSubmit; - #[ORM\ManyToOne(inversedBy: 'plugWarrantySerials')] - #[Ignore] + #[ORM\ManyToOne] private ?User $submitter = null; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: 'text', nullable: true)] private ?string $description = null; - #[ORM\Column(length: 25, nullable: true)] - private ?string $warrantyStartDate = null; + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $warrantyStartDate = null; - #[ORM\Column(length: 25, nullable: true)] - private ?string $warrantyEndDate = null; + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $warrantyEndDate = null; - #[ORM\Column(length: 50, nullable: true)] - private ?string $status = 'active'; + #[ORM\Column(length: 20)] + private string $status = self::STATUS_AVAILABLE; - #[ORM\Column(length: 255, nullable: true)] + #[ORM\Column(type: 'string', length: 20)] + private string $activation = 'deactive'; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $activationAt = null; + + #[ORM\Column(type: 'text', nullable: true)] private ?string $notes = null; - #[ORM\Column(nullable: true)] - private ?bool $used = null; + #[ORM\Column(name: 'allocated_to_document_id', type: 'integer', nullable: true)] + private ?int $allocatedToDocumentId = null; - #[ORM\Column(length: 50, nullable: true)] - private ?string $usedAt = null; + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $allocatedAt = null; - #[ORM\Column(length: 255, nullable: true)] - private ?string $usedTicketCode = null; + #[ORM\Column(name: 'bound_to_item_id', type: 'integer', nullable: true)] + private ?int $boundToItemId = null; + + #[ORM\Column(type: 'datetime_immutable', nullable: true)] + private ?\DateTimeImmutable $boundAt = null; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $voidReason = null; + + #[ORM\Column(type: 'string', length: 255, nullable: true)] + private ?string $commoditySerial = null; + + #[ORM\ManyToOne] + private ?Person $buyer = null; + + #[ORM\Column(type: 'string', length: 32, nullable: true)] + private ?string $activationTicketCode = null; + + #[ORM\Column(type: 'string', length: 32, nullable: true)] + private ?string $activationTicketSecret = null; public function __construct() { - $this->dateSubmit = date('Y-m-d H:i:s'); + $this->dateSubmit = new \DateTimeImmutable(); } - public function getId(): ?int - { - return $this->id; + public function getId(): ?int { return $this->id; } + + public function getBusiness(): ?Business { return $this->business; } + public function setBusiness(?Business $business): self { $this->business = $business; return $this; } + + public function getCommodity(): ?Commodity { return $this->commodity; } + public function setCommodity(?Commodity $commodity): self { $this->commodity = $commodity; return $this; } + + public function getSerialNumber(): ?string { return $this->serialNumber; } + public function setSerialNumber(string $serialNumber): self { $this->serialNumber = $serialNumber; return $this; } + + public function getDateSubmit(): \DateTimeImmutable { return $this->dateSubmit; } + public function setDateSubmit(\DateTimeImmutable $d): self { $this->dateSubmit = $d; return $this; } + + public function getSubmitter(): ?User { return $this->submitter; } + public function setSubmitter(?User $submitter): self { $this->submitter = $submitter; return $this; } + + public function getDescription(): ?string { return $this->description; } + public function setDescription(?string $description): self { $this->description = $description; return $this; } + + public function getWarrantyStartDate(): ?\DateTimeImmutable { return $this->warrantyStartDate; } + public function setWarrantyStartDate(?\DateTimeImmutable $d): self { $this->warrantyStartDate = $d; return $this; } + + public function getWarrantyEndDate(): ?\DateTimeImmutable { return $this->warrantyEndDate; } + public function setWarrantyEndDate(?\DateTimeImmutable $d): self { $this->warrantyEndDate = $d; return $this; } + + public function getStatus(): string { return $this->status; } + public function setStatus(string $status): self { $this->status = $status; return $this; } + + public function getActivation(): string { return $this->activation; } + public function setActivation(string $activation): self { $this->activation = $activation; return $this; } + + public function getActivationAt(): ?\DateTimeImmutable { return $this->activationAt; } + public function setActivationAt(?\DateTimeImmutable $d): self { $this->activationAt = $d; return $this; } + + public function getNotes(): ?string { return $this->notes; } + public function setNotes(?string $notes): self { $this->notes = $notes; return $this; } + + public function getAllocatedToDocumentId(): ?int { return $this->allocatedToDocumentId; } + public function setAllocatedToDocumentId(?int $id): self { $this->allocatedToDocumentId = $id; return $this; } + + public function getAllocatedAt(): ?\DateTimeImmutable { return $this->allocatedAt; } + public function setAllocatedAt(?\DateTimeImmutable $d): self { $this->allocatedAt = $d; return $this; } + + public function getBoundToItemId(): ?int { return $this->boundToItemId; } + public function setBoundToItemId(?int $id): self { $this->boundToItemId = $id; return $this; } + + public function getBoundAt(): ?\DateTimeImmutable { return $this->boundAt; } + public function setBoundAt(?\DateTimeImmutable $d): self { $this->boundAt = $d; return $this; } + + public function getVoidReason(): ?string { return $this->voidReason; } + public function setVoidReason(?string $r): self { $this->voidReason = $r; return $this; } + + public function getCommoditySerial(): ?string { return $this->commoditySerial; } + public function setCommoditySerial(?string $commoditySerial): self { $this->commoditySerial = $commoditySerial; return $this; } + + public function getBuyer(): ?Person { return $this->buyer; } + public function setBuyer(?Person $buyer): self { $this->buyer = $buyer; return $this; } + + public function getActivationTicketCode(): ?string { return $this->activationTicketCode; } + public function setActivationTicketCode(?string $code): self { $this->activationTicketCode = $code; return $this; } + public function getActivationTicketSecret(): ?string { return $this->activationTicketSecret; } + public function setActivationTicketSecret(?string $secret): self { $this->activationTicketSecret = $secret; return $this; } + + public function isUsed(): bool { + return $this->status === self::STATUS_CONSUMED; } - public function getBid(): ?Business - { - return $this->bid; - } - - public function setBid(?Business $bid): static - { - $this->bid = $bid; + public function setUsed(bool $used): self { + $this->status = $used ? self::STATUS_CONSUMED : self::STATUS_AVAILABLE; return $this; } - public function getCommodity(): ?Commodity - { - return $this->commodity; - } - - public function setCommodity(?Commodity $commodity): static - { - $this->commodity = $commodity; + public function setUsedAt(string $usedAt): self { + $this->usedAt = new \DateTimeImmutable($usedAt); return $this; } - public function getSerialNumber(): ?string - { - return $this->serialNumber; - } - - public function setSerialNumber(string $serialNumber): static - { - $this->serialNumber = $serialNumber; + public function setUsedTicketCode(string $ticketCode): self { + $this->usedTicketCode = $ticketCode; return $this; } - - public function getDateSubmit(): ?string - { - return $this->dateSubmit; - } - - public function setDateSubmit(string $dateSubmit): static - { - $this->dateSubmit = $dateSubmit; - return $this; - } - - public function getSubmitter(): ?User - { - return $this->submitter; - } - - public function setSubmitter(?User $submitter): static - { - $this->submitter = $submitter; - return $this; - } - - public function getDescription(): ?string - { - return $this->description; - } - - public function setDescription(?string $description): static - { - $this->description = $description; - return $this; - } - - public function getWarrantyStartDate(): ?string - { - return $this->warrantyStartDate; - } - - public function setWarrantyStartDate(?string $warrantyStartDate): static - { - $this->warrantyStartDate = $warrantyStartDate; - return $this; - } - - public function getWarrantyEndDate(): ?string - { - return $this->warrantyEndDate; - } - - public function setWarrantyEndDate(?string $warrantyEndDate): static - { - $this->warrantyEndDate = $warrantyEndDate; - return $this; - } - - public function getStatus(): ?string - { - return $this->status; - } - - public function setStatus(?string $status): static - { - $this->status = $status; - return $this; - } - - public function getNotes(): ?string - { - return $this->notes; - } - - public function setNotes(?string $notes): static - { - $this->notes = $notes; - return $this; - } - - public function isUsed(): ?bool - { - return $this->used; - } - - public function setUsed(?bool $used): static - { - $this->used = $used; - return $this; - } - - public function getUsedAt(): ?string - { - return $this->usedAt; - } - - public function setUsedAt(?string $usedAt): static - { - $this->usedAt = $usedAt; - return $this; - } - - public function getUsedTicketCode(): ?string - { - return $this->usedTicketCode; - } - - public function setUsedTicketCode(?string $usedTicketCode): static - { - $this->usedTicketCode = $usedTicketCode; - return $this; - } -} \ No newline at end of file +} \ No newline at end of file diff --git a/hesabixCore/src/Entity/StoreroomTicket.php b/hesabixCore/src/Entity/StoreroomTicket.php index b1e3b1e..728e72e 100644 --- a/hesabixCore/src/Entity/StoreroomTicket.php +++ b/hesabixCore/src/Entity/StoreroomTicket.php @@ -77,12 +77,25 @@ class StoreroomTicket #[ORM\Column(nullable: true)] private ?bool $canShare = null; - #[ORM\Column(length: 50, nullable: true)] - private ?string $status = null; // in_progress | done | rejected + #[ORM\Column(length: 255, nullable: true)] private ?string $importWorkflowCode = null; + #[ORM\Column(length: 32, nullable: true)] + private ?string $activationCode = null; + + // Approval fields + #[ORM\Column(nullable: true)] + private ?bool $isPreview = null; + + #[ORM\Column(nullable: true)] + private ?bool $isApproved = null; + + #[ORM\ManyToOne] + #[ORM\JoinColumn(nullable: true)] + private ?User $approvedBy = null; + public function __construct() { $this->storeroomItems = new ArrayCollection(); @@ -339,16 +352,7 @@ class StoreroomTicket return $this; } - public function getStatus(): ?string - { - return $this->status; - } - public function setStatus(?string $status): static - { - $this->status = $status; - return $this; - } public function getImportWorkflowCode(): ?string { @@ -360,4 +364,49 @@ class StoreroomTicket $this->importWorkflowCode = $importWorkflowCode; return $this; } + + public function getActivationCode(): ?string + { + return $this->activationCode; + } + + public function setActivationCode(?string $activationCode): static + { + $this->activationCode = $activationCode; + return $this; + } + + // Approval methods + public function isPreview(): ?bool + { + return $this->isPreview; + } + + public function setIsPreview(?bool $isPreview): static + { + $this->isPreview = $isPreview; + return $this; + } + + public function isApproved(): ?bool + { + return $this->isApproved; + } + + public function setIsApproved(?bool $isApproved): static + { + $this->isApproved = $isApproved; + return $this; + } + + public function getApprovedBy(): ?User + { + return $this->approvedBy; + } + + public function setApprovedBy(?User $approvedBy): static + { + $this->approvedBy = $approvedBy; + return $this; + } } diff --git a/hesabixCore/src/Repository/PlugWarrantySerialRepository.php b/hesabixCore/src/Repository/PlugWarrantySerialRepository.php index 63fa855..746a565 100644 --- a/hesabixCore/src/Repository/PlugWarrantySerialRepository.php +++ b/hesabixCore/src/Repository/PlugWarrantySerialRepository.php @@ -22,7 +22,8 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByBusiness($bid): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :val') + ->join('p.business', 'b') + ->andWhere('b.id = :val') ->setParameter('val', $bid) ->orderBy('p.dateSubmit', 'DESC') ->getQuery() @@ -36,7 +37,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByCommodity($bid, $commodityId): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.commodity = :commodityId') ->setParameter('bid', $bid) ->setParameter('commodityId', $commodityId) @@ -52,7 +53,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository public function findByStatus($bid, $status): array { return $this->createQueryBuilder('p') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.status = :status') ->setParameter('bid', $bid) ->setParameter('status', $status) @@ -72,7 +73,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository ->setParameter('serialNumber', $serialNumber); if ($bid) { - $qb->andWhere('p.bid = :bid') + $qb->andWhere('p.business = :bid') ->setParameter('bid', $bid); } @@ -86,7 +87,7 @@ class PlugWarrantySerialRepository extends ServiceEntityRepository { return $this->createQueryBuilder('p') ->leftJoin('p.commodity', 'c') - ->andWhere('p.bid = :bid') + ->andWhere('p.business = :bid') ->andWhere('p.serialNumber LIKE :keyword OR c.name LIKE :keyword OR p.description LIKE :keyword') ->setParameter('bid', $bid) ->setParameter('keyword', '%' . $keyword . '%') diff --git a/hesabixCore/src/Service/Explore.php b/hesabixCore/src/Service/Explore.php index a146dc1..6d6aa05 100644 --- a/hesabixCore/src/Service/Explore.php +++ b/hesabixCore/src/Service/Explore.php @@ -171,6 +171,10 @@ class Explore 'amount' => $doc->getAmount(), 'mdate' => '', 'plugin' => $doc->getPlugin(), + // Approval fields + 'isPreview' => $doc->isPreview(), + 'isApproved' => $doc->isApproved(), + 'approvedBy' => $doc->getApprovedBy() ? self::ExploreUser($doc->getApprovedBy()) : null, ]; } @@ -210,6 +214,9 @@ class Explore $row->setDiscount(0); $temp['tax'] = $row->getTax(); $temp['discount'] = $row->getDiscount(); + + // Approval fields - از سند اصلی گرفته می‌شود + return $temp; } @@ -347,7 +354,7 @@ class Explore 'address' => $person->getAddress(), 'prelabel' => null, 'tags' => $person->getTags(), - 'requireTwoStep' => $person->isRequireTwoStep(), + 'requireTwoStep' => $person->getBid() ? $person->getBid()->isRequireTwoStepApproval() : false, ]; if ($person->getPrelabel()) { $res['prelabel'] = $person->getPrelabel()->getLabel(); @@ -571,6 +578,10 @@ class Explore 'shortlinks' => $item->isShortlinks(), 'walletEnabled' => $item->isWalletEnable(), 'walletMatchBank' => $item->getWalletMatchBank() ? $item->getWalletMatchBank()->getId() : null, + 'requireTwoStepApproval' => $item->isRequireTwoStepApproval(), + 'invoiceApprover' => $item->getInvoiceApprover(), + 'warehouseApprover' => $item->getWarehouseApprover(), + 'financialApprover' => $item->getFinancialApprover(), 'updateSellPrice' => $item->isCommodityUpdateSellPriceAuto(), 'updateBuyPrice' => $item->isCommodityUpdateBuyPriceAuto(), ]; diff --git a/hesabixCore/src/Service/FileStorage.php b/hesabixCore/src/Service/FileStorage.php new file mode 100644 index 0000000..938a4cb --- /dev/null +++ b/hesabixCore/src/Service/FileStorage.php @@ -0,0 +1,42 @@ +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; + } +} + + diff --git a/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg b/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg new file mode 100644 index 0000000..ffe5a5e Binary files /dev/null and b/public_html/uploads/storeroom/st_6899dc09b4eb05.86469834_4837DCA8-0915-4C7B-9BE1-70C87FF4385A.jpeg differ diff --git a/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png b/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png new file mode 100644 index 0000000..795a746 Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1ad4e1bb6c5.93976672_Screenshot_2025-03-30_233821.png differ diff --git a/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png b/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png new file mode 100644 index 0000000..795a746 Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1af08b47979.56036064_Screenshot_2025-03-30_233821.png differ diff --git a/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf b/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf new file mode 100644 index 0000000..6717a5f Binary files /dev/null and b/public_html/uploads/storeroom/st_68a1b350a9b8c3.34236142_NDr4WkNHDI.pdf differ diff --git a/webUI/package.json b/webUI/package.json index b1999b0..e9b282f 100755 --- a/webUI/package.json +++ b/webUI/package.json @@ -41,6 +41,7 @@ "maz-ui": "^3.50.1", "monaco-editor": "^0.52.2", "pinia": "^3.0.2", + "qr-scanner": "^1.4.2", "sweetalert2": "^11.4.8", "v-money3": "^3.24.1", "vue": "^3.5.13", diff --git a/webUI/src/components/common/ApprovalManager.vue b/webUI/src/components/common/ApprovalManager.vue new file mode 100644 index 0000000..d180969 --- /dev/null +++ b/webUI/src/components/common/ApprovalManager.vue @@ -0,0 +1,268 @@ + + + + + diff --git a/webUI/src/components/common/ApprovalStatus.vue b/webUI/src/components/common/ApprovalStatus.vue new file mode 100644 index 0000000..e608050 --- /dev/null +++ b/webUI/src/components/common/ApprovalStatus.vue @@ -0,0 +1,163 @@ + + + + + diff --git a/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue b/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue index f3208ce..f93ac61 100644 --- a/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue +++ b/webUI/src/components/plugins/import-workflow/ImportWorkflowCustoms.vue @@ -22,7 +22,7 @@ no-data-text="اطلاعات ترخیص گمرکی ثبت نشده است" >