diff --git a/backend/app/controller/ApprovalController.php b/backend/app/controller/ApprovalController.php index 482c208..150f2a2 100644 --- a/backend/app/controller/ApprovalController.php +++ b/backend/app/controller/ApprovalController.php @@ -50,7 +50,7 @@ class ApprovalController extends BaseController { $id = (int) $this->getRouteParam($request, 'id'); - $approval = ApprovalOrder::with(['flow', 'applicant', 'records']) + $approval = ApprovalOrder::with(['flow.nodes', 'applicant', 'records.approver']) ->findOrFail($id); return $this->success($approval); diff --git a/backend/app/service/ApprovalService.php b/backend/app/service/ApprovalService.php index 42c1fb8..fc5ff17 100644 --- a/backend/app/service/ApprovalService.php +++ b/backend/app/service/ApprovalService.php @@ -190,13 +190,15 @@ class ApprovalService $query->whereIn('id', $pendingOrderIds); break; case 'approved': - // 获取用户已审批的订单 + // 获取用户已审批的订单(只显示已完成的:approved、rejected、cancelled) $approvedOrderIds = ApprovalRecord::where('approver_id', $userId) ->where('action', '!=', ApprovalRecord::ACTION_SUBMIT) + ->whereIn('action', [ApprovalRecord::ACTION_APPROVE, ApprovalRecord::ACTION_REJECT]) ->pluck('order_id') ->unique() ->toArray(); - $query->whereIn('id', $approvedOrderIds); + $query->whereIn('id', $approvedOrderIds) + ->whereIn('status', [ApprovalOrder::STATUS_APPROVED, ApprovalOrder::STATUS_REJECTED, ApprovalOrder::STATUS_CANCELLED]); break; } diff --git a/backend/app/service/BusinessTripService.php b/backend/app/service/BusinessTripService.php index 3301f58..0ec8a9f 100644 --- a/backend/app/service/BusinessTripService.php +++ b/backend/app/service/BusinessTripService.php @@ -161,7 +161,8 @@ class BusinessTripService */ public function getList(int $userId, string $type = 'my', int $page = 1, int $pageSize = 20): array { - $query = BusinessTrip::with(['user', 'itineraries', 'approvalOrder']); + // 先不加 approvalOrder 关系,避免 PHP 8.5 兼容性问题 + $query = BusinessTrip::with(['user', 'itineraries']); switch ($type) { case 'my': diff --git a/backend/composer.json b/backend/composer.json index 82a5eca..25689ea 100644 --- a/backend/composer.json +++ b/backend/composer.json @@ -7,11 +7,14 @@ "workerman/webman": "^2.0", "workerman/webman-framework": "^2.0", "illuminate/database": "^10.0", - "illuminate/pagination": "^10.0", + "illuminate/pagination": "^10.49", "illuminate/redis": "^10.0", - "illuminate/events": "^10.0", + "illuminate/events": "^10.49", "firebase/php-jwt": "^6.10", - "vlucas/phpdotenv": "^5.6" + "vlucas/phpdotenv": "^5.6", + "webman/database": "^2.1", + "symfony/var-dumper": "^8.0", + "webman/redis": "^2.1" }, "require-dev": { "mockery/mockery": "^1.5", diff --git a/backend/composer.lock b/backend/composer.lock index 13276c6..0894d1d 100644 --- a/backend/composer.lock +++ b/backend/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "1f33bdcee0a2bedeb8baa362e561fc69", + "content-hash": "76cf41958d6f742bb6839fffc9775c3a", "packages": [ { "name": "brick/math", @@ -288,6 +288,77 @@ }, "time": "2025-04-09T20:32:01+00:00" }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, { "name": "graham-campbell/result-type", "version": "v1.1.4", @@ -350,6 +421,92 @@ ], "time": "2025-12-27T19:43:20+00:00" }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.5", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "reference": "4f4bbd4e7172148801e76e3decc1e559bdee34e1", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2025-08-22T14:27:06+00:00" + }, { "name": "illuminate/bus", "version": "v10.49.0", @@ -731,6 +888,133 @@ }, "time": "2025-03-24T11:47:24+00:00" }, + { + "name": "illuminate/filesystem", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/filesystem.git", + "reference": "584ff4da2218e63e7210bba1c541ce526f24f37e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/filesystem/zipball/584ff4da2218e63e7210bba1c541ce526f24f37e", + "reference": "584ff4da2218e63e7210bba1c541ce526f24f37e", + "shasum": "" + }, + "require": { + "illuminate/collections": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1", + "symfony/finder": "^6.2" + }, + "suggest": { + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-hash": "Required to use the Filesystem class.", + "illuminate/http": "Required for handling uploaded files (^7.0).", + "league/flysystem": "Required to use the Flysystem local driver (^3.0.16).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^6.2).", + "symfony/mime": "Required to enable support for guessing extensions (^6.2)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "files": [ + "functions.php" + ], + "psr-4": { + "Illuminate\\Filesystem\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Filesystem package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-01-24T16:09:31+00:00" + }, + { + "name": "illuminate/http", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/http.git", + "reference": "45dd7db0731bb1f28acbb5d6b59b029e83d3cb8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/http/zipball/45dd7db0731bb1f28acbb5d6b59b029e83d3cb8e", + "reference": "45dd7db0731bb1f28acbb5d6b59b029e83d3cb8e", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "fruitcake/php-cors": "^1.2", + "guzzlehttp/uri-template": "^1.0", + "illuminate/collections": "^10.0", + "illuminate/macroable": "^10.0", + "illuminate/session": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1", + "symfony/http-foundation": "^6.4", + "symfony/http-kernel": "^6.2", + "symfony/mime": "^6.2" + }, + "suggest": { + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "guzzlehttp/guzzle": "Required to use the HTTP Client (^7.5)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Http\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Http package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, { "name": "illuminate/macroable", "version": "v10.49.0", @@ -929,6 +1213,63 @@ }, "time": "2025-03-24T11:47:24+00:00" }, + { + "name": "illuminate/session", + "version": "v10.49.0", + "source": { + "type": "git", + "url": "https://github.com/illuminate/session.git", + "reference": "ddfa808aadcfeaec6586349d013fc55776146283" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/illuminate/session/zipball/ddfa808aadcfeaec6586349d013fc55776146283", + "reference": "ddfa808aadcfeaec6586349d013fc55776146283", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-session": "*", + "illuminate/collections": "^10.0", + "illuminate/contracts": "^10.0", + "illuminate/filesystem": "^10.0", + "illuminate/support": "^10.0", + "php": "^8.1", + "symfony/finder": "^6.2", + "symfony/http-foundation": "^6.4" + }, + "suggest": { + "illuminate/console": "Required to use the session:table command (^10.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "10.x-dev" + } + }, + "autoload": { + "psr-4": { + "Illuminate\\Session\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Illuminate Session package.", + "homepage": "https://laravel.com", + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2025-03-24T11:47:24+00:00" + }, { "name": "illuminate/support", "version": "v10.49.0", @@ -1000,6 +1341,67 @@ }, "time": "2025-09-08T19:05:53+00:00" }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.10", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "reference": "870fc81d2f879903dfc5b60bf8a0f94a1609e669", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", + "nesbot/carbon": "^2.67|^3.0", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/phpstan": "^2.0", + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-02-20T19:59:49+00:00" + }, { "name": "monolog/monolog", "version": "2.11.0", @@ -1435,6 +1837,56 @@ }, "time": "2021-11-05T16:47:00+00:00" }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, { "name": "psr/log", "version": "3.0.2", @@ -1701,6 +2153,605 @@ ], "time": "2024-09-25T14:21:43+00:00" }, + { + "name": "symfony/error-handler", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "reference": "8da531f364ddfee53e36092a7eebbbd0b775f6b8", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5", + "symfony/http-kernel": "<6.4" + }, + "require-dev": { + "symfony/console": "^6.4|^7.0|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^6.4|^7.0|^8.0", + "symfony/serializer": "^6.4|^7.0|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-20T16:42:42+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v7.4.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/dc2c0eba1af673e736bb851d747d266108aea746", + "reference": "dc2c0eba1af673e736bb851d747d266108aea746", + "shasum": "" + }, + "require": { + "php": ">=8.2", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/dependency-injection": "<6.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^6.4|^7.0|^8.0", + "symfony/dependency-injection": "^6.4|^7.0|^8.0", + "symfony/error-handler": "^6.4|^7.0|^8.0", + "symfony/expression-language": "^6.4|^7.0|^8.0", + "symfony/framework-bundle": "^6.4|^7.0|^8.0", + "symfony/http-foundation": "^6.4|^7.0|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v7.4.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T11:45:34+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/59eb412e93815df44f05f342958efa9f46b1e586", + "reference": "59eb412e93815df44f05f342958efa9f46b1e586", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-25T14:21:43+00:00" + }, + { + "name": "symfony/finder", + "version": "v6.4.33", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/24965ca011dac87431729640feef8bcf7b5523e0", + "reference": "24965ca011dac87431729640feef8bcf7b5523e0", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "symfony/filesystem": "^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v6.4.33" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-26T13:03:48+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v6.4.33", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "f1a490cc9d595ba7ebe684220e625d1e472ad278" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f1a490cc9d595ba7ebe684220e625d1e472ad278", + "reference": "f1a490cc9d595ba7ebe684220e625d1e472ad278", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php83": "^1.27" + }, + "conflict": { + "symfony/cache": "<6.4.12|>=7.0,<7.1.5" + }, + "require-dev": { + "doctrine/dbal": "^2.13.1|^3|^4", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^6.4.12|^7.1.5", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4|^7.0", + "symfony/mime": "^5.4|^6.0|^7.0", + "symfony/rate-limiter": "^5.4|^6.0|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v6.4.33" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-27T15:04:55+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v6.4.33", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "73fa5c999d7f741ca544a97d3c791cc97890ae4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/73fa5c999d7f741ca544a97d3c791cc97890ae4d", + "reference": "73fa5c999d7f741ca544a97d3c791cc97890ae4d", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^6.4|^7.0", + "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/http-foundation": "^6.4|^7.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/browser-kit": "<5.4", + "symfony/cache": "<5.4", + "symfony/config": "<6.1", + "symfony/console": "<5.4", + "symfony/dependency-injection": "<6.4", + "symfony/doctrine-bridge": "<5.4", + "symfony/form": "<5.4", + "symfony/http-client": "<5.4", + "symfony/http-client-contracts": "<2.5", + "symfony/mailer": "<5.4", + "symfony/messenger": "<5.4", + "symfony/translation": "<5.4", + "symfony/translation-contracts": "<2.5", + "symfony/twig-bridge": "<5.4", + "symfony/validator": "<6.4", + "symfony/var-dumper": "<6.3", + "twig/twig": "<2.13" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^5.4|^6.0|^7.0", + "symfony/clock": "^6.2|^7.0", + "symfony/config": "^6.1|^7.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/css-selector": "^5.4|^6.0|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/dom-crawler": "^5.4|^6.0|^7.0", + "symfony/expression-language": "^5.4|^6.0|^7.0", + "symfony/finder": "^5.4|^6.0|^7.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/property-access": "^5.4.5|^6.0.5|^7.0", + "symfony/routing": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.4|^7.0.4", + "symfony/stopwatch": "^5.4|^6.0|^7.0", + "symfony/translation": "^5.4|^6.0|^7.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^5.4|^6.0|^7.0", + "symfony/validator": "^6.4|^7.0", + "symfony/var-dumper": "^5.4|^6.4|^7.0", + "symfony/var-exporter": "^6.2|^7.0", + "twig/twig": "^2.13|^3.0.4" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v6.4.33" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-28T10:02:13+00:00" + }, + { + "name": "symfony/mime", + "version": "v6.4.32", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "7409686879ca36c09fc970a5fa8ff6e93504dba4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/7409686879ca36c09fc970a5fa8ff6e93504dba4", + "reference": "7409686879ca36c09fc970a5fa8ff6e93504dba4", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<3.2.2", + "phpdocumentor/type-resolver": "<1.4.0", + "symfony/mailer": "<5.4", + "symfony/serializer": "<6.4.3|>7.0,<7.0.3" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "symfony/dependency-injection": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.4|^7.0", + "symfony/property-access": "^5.4|^6.0|^7.0", + "symfony/property-info": "^5.4|^6.0|^7.0", + "symfony/serializer": "^6.4.3|^7.0.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v6.4.32" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-04T11:53:14+00:00" + }, { "name": "symfony/polyfill-ctype", "version": "v1.33.0", @@ -1866,6 +2917,93 @@ ], "time": "2025-06-27T09:58:17+00:00" }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "reference": "9614ac4d8061dc257ecc64cba1b140873dce8ad3", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2024-09-10T14:38:51+00:00" + }, { "name": "symfony/polyfill-intl-normalizer", "version": "v1.33.0", @@ -2120,6 +3258,166 @@ ], "time": "2025-01-02T08:10:11+00:00" }, + { + "name": "symfony/polyfill-php83", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php83.git", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php83/zipball/17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "reference": "17f6f9a6b1735c0f163024d959f700cfbc5155e5", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php83\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.3+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php83/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-07-08T02:45:35+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.33.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "reference": "d4e5fcd4ab3d998ab16c0db48e6cbb9a01993f91", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.33.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-06-23T16:12:55+00:00" + }, { "name": "symfony/service-contracts", "version": "v3.6.1", @@ -2479,6 +3777,93 @@ ], "time": "2025-07-15T13:41:35+00:00" }, + { + "name": "symfony/var-dumper", + "version": "v8.0.4", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/326e0406fc315eca57ef5740fa4a280b7a068c82", + "reference": "326e0406fc315eca57ef5740fa4a280b7a068c82", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.4" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-01T23:07:29+00:00" + }, { "name": "vlucas/phpdotenv", "version": "v5.6.3", @@ -2691,6 +4076,80 @@ }, "time": "2026-02-23T02:46:47+00:00" }, + { + "name": "webman/database", + "version": "v2.1.9", + "source": { + "type": "git", + "url": "https://github.com/webman-php/database.git", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/database/zipball/5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "reference": "5a1463c96c79b35225f1cbd2f3e65830a7b4da0e", + "shasum": "" + }, + "require": { + "illuminate/database": "^10.0 || ^11.0 || ^12.0", + "illuminate/http": "^10.0 || ^11.0 || ^12.0", + "laravel/serializable-closure": "^1.0 || ^2.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Database\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman database", + "support": { + "issues": "https://github.com/webman-php/database/issues", + "source": "https://github.com/webman-php/database/tree/v2.1.9" + }, + "time": "2026-02-02T10:45:05+00:00" + }, + { + "name": "webman/redis", + "version": "v2.1.3", + "source": { + "type": "git", + "url": "https://github.com/webman-php/redis.git", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webman-php/redis/zipball/559eb1692d39c6fef5cf526223fff728be6c0fb9", + "reference": "559eb1692d39c6fef5cf526223fff728be6c0fb9", + "shasum": "" + }, + "require": { + "illuminate/redis": "^10.0 || ^11.0 || ^12.0", + "workerman/webman-framework": "^2.1 || dev-master" + }, + "type": "library", + "autoload": { + "psr-4": { + "support\\": "src/support", + "Webman\\Redis\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Webman redis", + "support": { + "issues": "https://github.com/webman-php/redis/issues", + "source": "https://github.com/webman-php/redis/tree/v2.1.3" + }, + "time": "2025-03-14T03:52:14+00:00" + }, { "name": "workerman/coroutine", "version": "v1.1.4", diff --git a/backend/test_data.php b/backend/test_data.php new file mode 100644 index 0000000..6c20a7f --- /dev/null +++ b/backend/test_data.php @@ -0,0 +1,263 @@ +where('business_type', 'business_trip')->where('status', 1)->first(); + +if (!$flow) { + echo "Error: No approval flow found\n"; + exit; +} + +echo "Creating test data...\n"; + +// 1. 创建用户2提交的申请(让用户1审批) +$trip1 = Db::table('business_trip')->insertGetId([ + 'user_id' => $userId2, + 'reason' => '北京技术交流', + 'start_date' => '2026-03-15', + 'end_date' => '2026-03-17', + 'destination' => '北京', + 'estimated_cost' => 5000, + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +Db::table('business_trip_itinerary')->insert([ + 'business_trip_id' => $trip1, + 'start_time' => '2026-03-15 08:00:00', + 'end_time' => '2026-03-17 18:00:00', + 'departure' => '郑州', + 'destination' => '北京', + 'transport' => '高铁', + 'trip_type' => 'roundtrip', + 'remark' => '参加技术交流会议', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +$order1 = Db::table('approval_order')->insertGetId([ + 'business_type' => 'business_trip', + 'business_id' => $trip1, + 'title' => '出差申请 - 闫继光', + 'applicant_id' => $userId2, + 'status' => 'pending', + 'current_nodes' => json_encode([1]), + 'flow_id' => $flow->id, + 'form_data' => json_encode(['reason' => '北京技术交流', 'approvers' => [$userId1]]), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +Db::table('business_trip')->where('id', $trip1)->update(['approval_order_id' => $order1]); + +Db::table('approval_record')->insert([ + 'order_id' => $order1, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId2, + 'action' => 'submit', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +echo "Created trip for user 2 (闫继光), order_id: $order1\n"; + +// 2. 创建用户3提交的申请(让用户1审批) +$trip2 = Db::table('business_trip')->insertGetId([ + 'user_id' => $userId3, + 'reason' => '上海客户拜访', + 'start_date' => '2026-03-20', + 'end_date' => '2026-03-21', + 'destination' => '上海', + 'estimated_cost' => 3000, + 'status' => 'pending', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +Db::table('business_trip_itinerary')->insert([ + 'business_trip_id' => $trip2, + 'start_time' => '2026-03-20 08:00:00', + 'end_time' => '2026-03-21 18:00:00', + 'departure' => '郑州', + 'destination' => '上海', + 'transport' => '高铁', + 'trip_type' => 'roundtrip', + 'remark' => '拜访重要客户', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +$order2 = Db::table('approval_order')->insertGetId([ + 'business_type' => 'business_trip', + 'business_id' => $trip2, + 'title' => '出差申请 - 孟启', + 'applicant_id' => $userId3, + 'status' => 'pending', + 'current_nodes' => json_encode([1]), + 'flow_id' => $flow->id, + 'form_data' => json_encode(['reason' => '上海客户拜访', 'approvers' => [$userId1]]), + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +Db::table('business_trip')->where('id', $trip2)->update(['approval_order_id' => $order2]); + +Db::table('approval_record')->insert([ + 'order_id' => $order2, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId3, + 'action' => 'submit', + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), +]); + +echo "Created trip for user 3 (孟启), order_id: $order2\n"; + +// 3. 创建一个已通过的申请(用户1审批的) +$trip3 = Db::table('business_trip')->insertGetId([ + 'user_id' => $userId2, + 'reason' => '广州展会', + 'start_date' => '2026-03-10', + 'end_date' => '2026-03-12', + 'destination' => '广州', + 'estimated_cost' => 8000, + 'status' => 'approved', + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')), +]); + +Db::table('business_trip_itinerary')->insert([ + 'business_trip_id' => $trip3, + 'start_time' => '2026-03-10 08:00:00', + 'end_time' => '2026-03-12 18:00:00', + 'departure' => '郑州', + 'destination' => '广州', + 'transport' => '飞机', + 'trip_type' => 'roundtrip', + 'remark' => '参加行业展会', + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')), +]); + +$order3 = Db::table('approval_order')->insertGetId([ + 'business_type' => 'business_trip', + 'business_id' => $trip3, + 'title' => '出差申请 - 闫继光', + 'applicant_id' => $userId2, + 'status' => 'approved', + 'current_nodes' => json_encode([]), + 'flow_id' => $flow->id, + 'form_data' => json_encode(['reason' => '广州展会', 'approvers' => [$userId1]]), + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')), +]); + +Db::table('business_trip')->where('id', $trip3)->update(['approval_order_id' => $order3]); + +Db::table('approval_record')->insert([ + 'order_id' => $order3, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId2, + 'action' => 'submit', + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')), +]); + +Db::table('approval_record')->insert([ + 'order_id' => $order3, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId1, + 'action' => 'approve', + 'comment' => '同意', + 'created_at' => date('Y-m-d H:i:s', strtotime('-1 day')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-1 day')), +]); + +echo "Created approved trip, order_id: $order3\n"; + +// 4. 创建一个已拒绝的申请 +$trip4 = Db::table('business_trip')->insertGetId([ + 'user_id' => $userId3, + 'reason' => '深圳培训', + 'start_date' => '2026-03-05', + 'end_date' => '2026-03-06', + 'destination' => '深圳', + 'estimated_cost' => 2000, + 'status' => 'rejected', + 'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')), +]); + +Db::table('business_trip_itinerary')->insert([ + 'business_trip_id' => $trip4, + 'start_time' => '2026-03-05 08:00:00', + 'end_time' => '2026-03-06 18:00:00', + 'departure' => '郑州', + 'destination' => '深圳', + 'transport' => '高铁', + 'trip_type' => 'roundtrip', + 'remark' => '参加产品培训', + 'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')), +]); + +$order4 = Db::table('approval_order')->insertGetId([ + 'business_type' => 'business_trip', + 'business_id' => $trip4, + 'title' => '出差申请 - 孟启', + 'applicant_id' => $userId3, + 'status' => 'rejected', + 'current_nodes' => json_encode([]), + 'flow_id' => $flow->id, + 'form_data' => json_encode(['reason' => '深圳培训', 'approvers' => [$userId1]]), + 'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')), +]); + +Db::table('business_trip')->where('id', $trip4)->update(['approval_order_id' => $order4]); + +Db::table('approval_record')->insert([ + 'order_id' => $order4, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId3, + 'action' => 'submit', + 'created_at' => date('Y-m-d H:i:s', strtotime('-3 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-3 days')), +]); + +Db::table('approval_record')->insert([ + 'order_id' => $order4, + 'node_id' => 1, + 'node_type' => 'single_approve', + 'approver_id' => $userId1, + 'action' => 'reject', + 'comment' => '预算不足,暂不批准', + 'created_at' => date('Y-m-d H:i:s', strtotime('-2 days')), + 'updated_at' => date('Y-m-d H:i:s', strtotime('-2 days')), +]); + +echo "Created rejected trip, order_id: $order4\n"; + +// 5. 更新现有订单4的审批节点,让用户1成为审批人 +Db::table('approval_order')->where('id', 4)->update([ + 'form_data' => json_encode(['reason' => '11', 'approvers' => [$userId1]]), +]); + +echo "Updated order 4 to make user 1 the approver\n"; + +echo "\n=== Test data created successfully ===\n"; diff --git a/frontend/src/pages/approval/detail.module.css b/frontend/src/pages/approval/detail.module.css new file mode 100644 index 0000000..ebcb167 --- /dev/null +++ b/frontend/src/pages/approval/detail.module.css @@ -0,0 +1,249 @@ +.detail { + min-height: 100vh; + background: #f5f5f5; + padding-bottom: 80px; +} + +.loading { + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; + color: #999; +} + +.content { + padding: 12px; +} + +.statusCard { + margin-bottom: 12px; +} + +.statusHeader { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + padding-bottom: 12px; + border-bottom: 1px solid #f0f0f0; +} + +.title { + font-size: 18px; + font-weight: 600; + color: #333; +} + +.statusInfo { + display: flex; + flex-direction: column; + gap: 8px; +} + +.infoItem { + display: flex; + font-size: 14px; +} + +.label { + color: #666; + width: 80px; + flex-shrink: 0; +} + +.value { + color: #333; + flex: 1; +} + +.section { + margin-bottom: 12px; +} + +.approverItem { + padding: 8px 0; + border-bottom: 1px solid #f0f0f0; +} + +.approverItem:last-child { + border-bottom: none; +} + +.approverName { + font-size: 15px; + color: #333; + margin-bottom: 4px; +} + +.approverType { + font-size: 13px; + color: #999; +} + +.formData { + display: flex; + flex-direction: column; + gap: 8px; +} + +.formRow { + display: flex; + font-size: 14px; + line-height: 1.6; +} + +.formLabel { + color: #666; + width: 100px; + flex-shrink: 0; +} + +.formValue { + color: #333; + flex: 1; + word-break: break-all; +} + +.flowNodes { + display: flex; + flex-direction: column; + gap: 16px; +} + +.flowNode { + display: flex; + align-items: flex-start; + gap: 12px; + position: relative; +} + +.flowNodeDot { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 12px; + font-weight: 600; + flex-shrink: 0; +} + +.flowNode.pending .flowNodeDot { + background: #e8e8e8; + color: #999; +} + +.flowNode.done .flowNodeDot { + background: #52c41a; + color: #fff; +} + +.flowNode.current .flowNodeDot { + background: #1890ff; + color: #fff; + box-shadow: 0 0 0 4px rgba(24, 144, 255, 0.2); +} + +.flowNodeContent { + flex: 1; + padding-top: 2px; +} + +.flowNodeName { + font-size: 15px; + color: #333; + font-weight: 500; + margin-bottom: 4px; +} + +.flowNodeType { + font-size: 13px; + color: #999; +} + +.timeline { + display: flex; + flex-direction: column; + gap: 16px; +} + +.timelineItem { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.timelineDot { + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.timelineDotIcon { + font-size: 14px; +} + +.timelineContent { + flex: 1; +} + +.timelineTitle { + font-size: 15px; + color: #333; + margin-bottom: 4px; +} + +.timelineName { + color: #666; + font-size: 14px; + margin-left: 8px; +} + +.timelineTime { + font-size: 13px; + color: #999; + margin-bottom: 4px; +} + +.timelineComment { + font-size: 14px; + color: #666; + background: #f5f5f5; + padding: 8px 12px; + border-radius: 4px; + margin-top: 4px; +} + +.empty { + text-align: center; + color: #999; + padding: 20px 0; + font-size: 14px; +} + +.footer { + position: fixed; + bottom: 0; + left: 0; + right: 0; + padding: 12px 16px; + background: #fff; + border-top: 1px solid #f0f0f0; + display: flex; + gap: 12px; + box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05); +} + +.footer button { + flex: 1; +} + +.commentBox { + padding: 8px 0; +} diff --git a/frontend/src/pages/approval/detail.tsx b/frontend/src/pages/approval/detail.tsx new file mode 100644 index 0000000..066a9af --- /dev/null +++ b/frontend/src/pages/approval/detail.tsx @@ -0,0 +1,424 @@ +import { useState, useEffect } from 'react' +import { useNavigate, useParams } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { NavBar, Card, Tag, Button, Dialog, Toast, TextArea } from 'antd-mobile' +import { + LeftOutline, + ClockCircleOutline, + CheckCircleOutline, + CloseCircleOutline, + ExclamationCircleOutline, +} from 'antd-mobile-icons' +import { approvalApi } from '../../services/api' +import { useUserStore } from '@/stores/user' +import dayjs from 'dayjs' +import styles from './detail.module.css' + +// 状态配置 +const statusConfig: Record = { + draft: { color: 'default', icon: , text: '草稿' }, + pending: { color: 'warning', icon: , text: '审批中' }, + approved: { color: 'success', icon: , text: '已通过' }, + rejected: { color: 'danger', icon: , text: '已拒绝' }, + cancelled: { color: 'default', icon: , text: '已撤销' }, +} + +// 操作配置 +const actionConfig: Record = { + submit: { color: '#999', icon: '📝', text: '提交申请' }, + approve: { color: '#52c41a', icon: '✅', text: '已同意' }, + reject: { color: '#ff4d4f', icon: '❌', text: '已拒绝' }, + cancel: { color: '#999', icon: '🔄', text: '已撤销' }, +} + +// 业务类型映射 +const businessTypeConfig: Record = { + business_trip: '出差申请', + expense: '费用报销', + leave: '请假申请', + purchase: '采购申请', +} + +interface ApprovalRecord { + id: number + node_type: string + action: string + comment: string | null + created_at: string + approver: { + name: string + avatar?: string + } +} + +interface ApprovalNode { + id: number + type: string + name: string + approvers: number[] +} + +interface ApprovalDetail { + id: number + title: string + business_type: string + business_id: number + status: string + created_at: string + current_nodes: number[] + applicant: { + name: string + avatar?: string + } + flow: { + name: string + nodes?: ApprovalNode[] + } + form_data: Record + records: ApprovalRecord[] +} + +export default function ApprovalDetail() { + const { t } = useTranslation() + const navigate = useNavigate() + const { id } = useParams() + const { user } = useUserStore() + + const [detail, setDetail] = useState(null) + const [loading, setLoading] = useState(true) + const [commentVisible, setCommentVisible] = useState(false) + const [comment, setComment] = useState('') + const [actionType, setActionType] = useState<'approve' | 'reject' | 'cancel'>('approve') + + // 加载详情 + const loadDetail = async () => { + if (!id) return + + setLoading(true) + try { + const data = await approvalApi.getDetail(Number(id)) + setDetail(data) + } catch (error: any) { + Toast.show(error.message || '加载失败') + navigate(-1) + } finally { + setLoading(false) + } + } + + useEffect(() => { + loadDetail() + }, [id]) + + // 撤销申请 + const handleCancel = () => { + Dialog.confirm({ + content: '确定要撤销此申请吗?', + onConfirm: async () => { + try { + await approvalApi.cancel(Number(id)) + Toast.show('已撤销') + loadDetail() + } catch (error: any) { + Toast.show(error.message || '操作失败') + } + }, + }) + } + + // 打开审批意见框 + const handleApprove = (action: 'approve' | 'reject') => { + setActionType(action) + setComment('') + setCommentVisible(true) + } + + // 提交审批 + const handleConfirmApprove = async () => { + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur() + } + + try { + if (actionType === 'approve') { + await approvalApi.approve(Number(id), comment) + Toast.show('已同意') + } else if (actionType === 'reject') { + await approvalApi.reject(Number(id), comment) + Toast.show('已拒绝') + } else if (actionType === 'cancel') { + await approvalApi.cancel(Number(id)) + Toast.show('已撤销') + } + setCommentVisible(false) + loadDetail() + } catch (error: any) { + Toast.show(error.message || '操作失败') + } + } + + // 检查是否是当前审批人 + const isCurrentApprover = () => { + if (!detail || !user) return false + if (detail.status !== 'pending') return false + + // 检查当前用户是否在 current_nodes 对应的节点审批人列表中 + const currentNodeIds = detail.current_nodes || [] + const nodes = detail.flow?.nodes || [] + + for (const nodeId of currentNodeIds) { + const node = nodes.find((n) => n.id === nodeId) + if (node && node.approvers && node.approvers.includes(user.id)) { + // 检查是否已审批 - 使用 approver_id 而不是 approver.id + const hasApproved = detail.records.some( + (r) => r.node_id === nodeId && r.approver_id === user.id + ) + if (!hasApproved) return true + } + } + + return false + } + + if (loading || !detail) { + return
加载中...
+ } + + const config = statusConfig[detail.status] || statusConfig.draft + const isOwner = detail.applicant?.name === user?.name + const canCancel = detail.status === 'pending' && isOwner + const canApprove = isCurrentApprover() + + // 格式化时间 + const formatTime = (timeStr: string) => { + return dayjs(timeStr).format('YYYY-MM-DD HH:mm') + } + + // 获取节点类型名称 + const getNodeTypeName = (nodeType: string) => { + const typeMap: Record = { + approver: '审批人', + and_approve: '会签', + or_approve: '或签', + notify: '通知', + single_approve: '单人审批', + } + return typeMap[nodeType] || nodeType + } + + // 获取当前审批人 + const getCurrentApprovers = () => { + const currentNodeIds = detail.current_nodes || [] + const nodes = detail.flow?.nodes || [] + const approvers: any[] = [] + + for (const nodeId of currentNodeIds) { + const node = nodes.find((n) => n.id === nodeId) + if (node) { + approvers.push({ + name: node.name, + type: getNodeTypeName(node.type), + approvers: node.approvers, + }) + } + } + + return approvers + } + + const currentApprovers = getCurrentApprovers() + + return ( +
+ navigate(-1)}>审批详情 + +
+ {/* 状态卡片 */} + +
+ {detail.title} + + {config.icon} + {config.text} + +
+
+
+ 业务类型: + + {businessTypeConfig[detail.business_type] || detail.business_type} + +
+
+ 申请人: + {detail.applicant?.name} +
+
+ 申请时间: + {formatTime(detail.created_at)} +
+
+ 审批流程: + {detail.flow?.name} +
+
+
+ + {/* 当前审批节点 */} + {detail.status === 'pending' && currentApprovers.length > 0 && ( + + {currentApprovers.map((item, index) => ( +
+
{item.name}
+
{item.type}
+
+ ))} +
+ )} + + {/* 表单数据 */} + {Object.keys(detail.form_data).length > 0 && ( + +
+ {Object.entries(detail.form_data) + .filter(([key]) => !['approvers', 'cc_users'].includes(key)) + .map(([key, value]) => ( +
+ {key}: + + {Array.isArray(value) ? value.join(', ') : String(value)} + +
+ ))} +
+
+ )} + + {/* 审批流程 */} + {detail.flow?.nodes && detail.flow.nodes.length > 0 && ( + +
+ {detail.flow.nodes.map((node, index) => { + const isCurrent = detail.current_nodes?.includes(node.id) + const record = detail.records.find((r) => r.node_id === node.id) + + let status: 'pending' | 'done' = 'pending' + if (record) { + status = 'done' + } else if (!isCurrent && index > 0) { + // 检查前面的节点是否完成 + const prevNode = detail.flow.nodes[index - 1] + const prevRecord = detail.records.find((r) => r.node_id === prevNode.id) + if (prevRecord) status = 'done' + } + + return ( +
+
+ {status === 'done' ? '✓' : index + 1} +
+
+
{node.name}
+
+ {getNodeTypeName(node.type)} +
+
+
+ ) + })} +
+
+ )} + + {/* 审批记录 */} + +
+ {detail.records && detail.records.length > 0 ? ( + detail.records.map((record) => { + const recordConfig = actionConfig[record.action] || actionConfig.submit + return ( +
+
+ {recordConfig.icon} +
+
+
+ {recordConfig.text} + + {record.approver?.name} + +
+
+ {formatTime(record.created_at)} +
+ {record.comment && ( +
备注: {record.comment}
+ )} +
+
+ ) + }) + ) : ( +
暂无审批记录
+ )} +
+
+
+ + {/* 底部操作栏 */} + {(canCancel || canApprove) && ( +
+ {canCancel && ( + + )} + {canApprove && ( + <> + + + + )} +
+ )} + + {/* 审批意见弹框 */} + +