diff --git a/modules/EcommerceFinance/Http/Controllers/ProcessDetails.php b/modules/EcommerceFinance/Http/Controllers/ProcessDetails.php index 36023ce6..96b289e4 100644 --- a/modules/EcommerceFinance/Http/Controllers/ProcessDetails.php +++ b/modules/EcommerceFinance/Http/Controllers/ProcessDetails.php @@ -63,6 +63,7 @@ class ProcessDetails extends Controller 'statuses' => $options['statuses'], 'paymentStates' => $options['payment_states'], 'currentApprovers' => $options['current_approvers'] ?? [], + 'waitingBuckets' => DingProcessDetailMirrorRepository::WAITING_BUCKET_LABELS, 'mirrorStatus' => $result['status'], 'mirrorSyncedAt' => $result['last_synced_at'], 'mirrorStale' => $result['sync_stale'] ?? false, @@ -91,6 +92,7 @@ class ProcessDetails extends Controller 'statuses' => [], 'paymentStates' => [], 'currentApprovers' => [], + 'waitingBuckets' => DingProcessDetailMirrorRepository::WAITING_BUCKET_LABELS, 'mirrorStatus' => 'unavailable', 'mirrorSyncedAt' => null, 'mirrorStale' => false, diff --git a/modules/EcommerceFinance/Repositories/DingProcessDetailMirrorRepository.php b/modules/EcommerceFinance/Repositories/DingProcessDetailMirrorRepository.php index 43d5f6c5..acef3558 100644 --- a/modules/EcommerceFinance/Repositories/DingProcessDetailMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/DingProcessDetailMirrorRepository.php @@ -22,6 +22,14 @@ class DingProcessDetailMirrorRepository '不涉及付款', ]; + public const WAITING_BUCKET_LABELS = [ + 'lt_1h' => '不足1小时', + '1_12h' => '1–12小时', + '12_24h' => '12–24小时', + '1_2d' => '1–2天', + 'gte_2d' => '2天以上', + ]; + public const LEGACY_PAYMENT_STATE = '未确认'; public const LEGACY_PAYMENT_STATE_MAP = [ @@ -78,6 +86,13 @@ class DingProcessDetailMirrorRepository return self::LEGACY_STATUS_MAP[$status] ?? $status; } + public static function normalizeWaitingBucketFilter(mixed $value): string + { + $bucket = trim((string) $value); + + return array_key_exists($bucket, self::WAITING_BUCKET_LABELS) ? $bucket : ''; + } + public function datasetKey(): string { return self::DATASET_PREFIX . $this->scope(); @@ -384,26 +399,31 @@ class DingProcessDetailMirrorRepository ); } - $entryStart = $this->filterDate($filters['entry_start'] ?? ''); - $entryEnd = $this->filterDate($filters['entry_end'] ?? ''); - if ($entryStart !== '' && $entryEnd !== '' && $entryStart > $entryEnd) { - [$entryStart, $entryEnd] = [$entryEnd, $entryStart]; - } - if ($entryStart !== '') { - $query->where( - 'ding_process_detail_mirror.current_task_started_at', - '>=', - $entryStart . ' 00:00:00', - ); - } - if ($entryEnd !== '') { - $query->where( - 'ding_process_detail_mirror.current_task_started_at', - '<', - Carbon::createFromFormat('Y-m-d', $entryEnd) - ->addDay() - ->format('Y-m-d 00:00:00'), - ); + $waitingBucket = self::normalizeWaitingBucketFilter($filters['waiting_bucket'] ?? ''); + if ($waitingBucket !== '') { + $now = OaProcessTime::now(); + $column = 'ding_process_detail_mirror.current_task_started_at'; + $query + ->whereNotNull($column) + ->where($column, '<=', $now->format('Y-m-d H:i:s')); + + if ($waitingBucket === 'lt_1h') { + $query->where($column, '>', $now->subHour()->format('Y-m-d H:i:s')); + } elseif ($waitingBucket === '1_12h') { + $query + ->where($column, '>', $now->subHours(12)->format('Y-m-d H:i:s')) + ->where($column, '<=', $now->subHour()->format('Y-m-d H:i:s')); + } elseif ($waitingBucket === '12_24h') { + $query + ->where($column, '>', $now->subHours(24)->format('Y-m-d H:i:s')) + ->where($column, '<=', $now->subHours(12)->format('Y-m-d H:i:s')); + } elseif ($waitingBucket === '1_2d') { + $query + ->where($column, '>', $now->subHours(48)->format('Y-m-d H:i:s')) + ->where($column, '<=', $now->subHours(24)->format('Y-m-d H:i:s')); + } else { + $query->where($column, '<=', $now->subHours(48)->format('Y-m-d H:i:s')); + } } foreach ([ diff --git a/modules/EcommerceFinance/View/ProcessDetailsPageData.php b/modules/EcommerceFinance/View/ProcessDetailsPageData.php index e16d8fab..70c6219d 100644 --- a/modules/EcommerceFinance/View/ProcessDetailsPageData.php +++ b/modules/EcommerceFinance/View/ProcessDetailsPageData.php @@ -89,6 +89,7 @@ class ProcessDetailsPageData 'statuses' => $options['statuses'], 'paymentStates' => $options['payment_states'], 'currentApprovers' => $options['current_approvers'] ?? [], + 'waitingBuckets' => DingProcessDetailMirrorRepository::WAITING_BUCKET_LABELS, 'mirrorStatus' => $result['status'], 'mirrorSyncedAt' => $result['last_synced_at'], 'mirrorStale' => $result['sync_stale'] ?? false, @@ -115,6 +116,7 @@ class ProcessDetailsPageData 'statuses' => [], 'paymentStates' => [], 'currentApprovers' => [], + 'waitingBuckets' => DingProcessDetailMirrorRepository::WAITING_BUCKET_LABELS, 'mirrorStatus' => 'unavailable', 'mirrorSyncedAt' => null, 'mirrorStale' => false, @@ -136,13 +138,12 @@ class ProcessDetailsPageData $request->query('paid_end'), $request->query('paid_date'), ); - [$entryStart, $entryEnd] = self::normalizeDateRange( - $request->query('entry_start'), - $request->query('entry_end'), - ); $paymentState = DingProcessDetailMirrorRepository::normalizePaymentStateFilter( $request->query('payment_state'), ); + $waitingBucket = DingProcessDetailMirrorRepository::normalizeWaitingBucketFilter( + $request->query('waiting_bucket'), + ); return [ 'paid_start' => $paidStart, @@ -153,8 +154,7 @@ class ProcessDetailsPageData 'business_id' => mb_substr(trim((string) $request->query('business_id', '')), 0, 32), 'originator_name' => mb_substr(trim((string) $request->query('originator_name', '')), 0, 100), 'current_approver_name' => mb_substr(trim((string) $request->query('current_approver_name', '')), 0, 100), - 'entry_start' => $entryStart, - 'entry_end' => $entryEnd, + 'waiting_bucket' => $waitingBucket, 'keyword' => mb_substr(trim((string) $request->query('keyword', '')), 0, 100), ]; } diff --git a/resources/views/business/pages/financial-operations/process-details-content.blade.php b/resources/views/business/pages/financial-operations/process-details-content.blade.php index 984bdd70..28344e02 100644 --- a/resources/views/business/pages/financial-operations/process-details-content.blade.php +++ b/resources/views/business/pages/financial-operations/process-details-content.blade.php @@ -4,6 +4,7 @@ $documentTypes ??= []; $statuses ??= []; $currentApprovers ??= []; + $waitingBuckets ??= []; $error ??= null; $paymentStates = $paymentStates ?? ($pageData['paymentStates'] ?? []); $mirrorStale = $mirrorStale ?? ($pageData['mirrorStale'] ?? false); @@ -203,6 +204,10 @@ grid-column: span 5; } + .process-details-field.is-waiting { + grid-column: span 5; + } + .process-details-field.is-business { grid-column: span 3; } @@ -799,6 +804,7 @@ } .process-details-field.is-date-range, + .process-details-field.is-waiting, .process-details-field.is-keyword, .process-details-actions { grid-column: span 6; @@ -848,6 +854,7 @@ .process-details-field.is-business, .process-details-field.is-approver, .process-details-field.is-date-range, + .process-details-field.is-waiting, .process-details-field.is-keyword, .process-details-actions { grid-column: 1; @@ -1063,51 +1070,16 @@ -
- -
- - - - - - - 至 - - - - - - -
+
+ +
diff --git a/tests/Feature/Business/ProcessDetailsPageTest.php b/tests/Feature/Business/ProcessDetailsPageTest.php index 878774e7..b05a4de7 100644 --- a/tests/Feature/Business/ProcessDetailsPageTest.php +++ b/tests/Feature/Business/ProcessDetailsPageTest.php @@ -89,8 +89,7 @@ class ProcessDetailsPageTest extends FeatureTestCase 'per_page' => 100, 'payment_state' => '待付款', 'current_approver_name' => '王五', - 'entry_start' => '2026-10-20', - 'entry_end' => '2026-10-30', + 'waiting_bucket' => '1_2d', ]))->assertOk() ->assertSee('流程数据') ->assertSee('OA流程') @@ -106,15 +105,15 @@ class ProcessDetailsPageTest extends FeatureTestCase ->assertSee('role="combobox"', false) ->assertSee('placeholder="输入姓名或展开选择"', false) ->assertSee('data-value="赵六"', false) - ->assertSee('name="entry_start"', false) - ->assertSee('name="entry_end"', false) + ->assertSee('name="waiting_bucket"', false) + ->assertSee('value="1_2d" selected', false) + ->assertSee('2天以上', false) + ->assertDontSee('name="entry_start"', false) + ->assertDontSee('name="entry_end"', false) ->assertSee('placeholder="开始日期"', false) ->assertSee('placeholder="结算日期"', false) - ->assertSee('placeholder="结束日期"', false) ->assertSee('aria-label="选择打款开始日期"', false) ->assertSee('aria-label="选择打款结算日期"', false) - ->assertSee('aria-label="选择审批进入开始日期"', false) - ->assertSee('aria-label="选择审批进入结束日期"', false) ->assertSee('CaiWuDatePicker.setup', false) ->assertSee('class="process-details-filter-grid"', false) ->assertSee('class="process-details-note-disclosure"', false) diff --git a/tests/Unit/EcommerceFinance/DingProcessDetailMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/DingProcessDetailMirrorRepositoryTest.php index 6f75d67f..5a827970 100644 --- a/tests/Unit/EcommerceFinance/DingProcessDetailMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/DingProcessDetailMirrorRepositoryTest.php @@ -197,20 +197,43 @@ class DingProcessDetailMirrorRepositoryTest extends TestCase 2, $this->repository->paginate(['current_approver_name' => '王五'], 1, 50)['total'], ); - $this->assertSame( - 2, - $this->repository->paginate([ - 'entry_start' => '2026-02-01', - 'entry_end' => '2026-02-28', - ], 1, 50)['total'], - ); - $this->assertSame( - 2, - $this->repository->paginate([ - 'entry_start' => '2026-02-28', - 'entry_end' => '2026-02-01', - ], 1, 50)['total'], - ); + } + + public function test_waiting_bucket_filter_uses_real_elapsed_hours(): void + { + try { + Carbon::setTestNow(Carbon::parse('2026-02-03 10:00:00', 'Asia/Shanghai')); + $this->assertSame( + 2, + $this->repository->paginate(['waiting_bucket' => '1_12h'], 1, 50)['total'], + ); + $this->assertSame( + 2, + $this->repository->paginate(['waiting_bucket' => 'gte_2d'], 1, 50)['total'], + ); + + Carbon::setTestNow(Carbon::parse('2026-01-02 21:00:00', 'Asia/Shanghai')); + $this->assertSame( + 2, + $this->repository->paginate(['waiting_bucket' => '12_24h'], 1, 50)['total'], + ); + $this->assertSame( + 0, + $this->repository->paginate(['waiting_bucket' => '1_12h'], 1, 50)['total'], + ); + + Carbon::setTestNow(Carbon::parse('2026-01-02 09:30:00', 'Asia/Shanghai')); + $this->assertSame( + 2, + $this->repository->paginate(['waiting_bucket' => 'lt_1h'], 1, 50)['total'], + ); + $this->assertSame( + '', + DingProcessDetailMirrorRepository::normalizeWaitingBucketFilter('invalid'), + ); + } finally { + Carbon::setTestNow(); + } } public function test_pending_cashier_status_without_payment_facts_is_waiting_for_payment(): void diff --git a/tests/Unit/EcommerceFinance/ProcessDetailsPageDataTest.php b/tests/Unit/EcommerceFinance/ProcessDetailsPageDataTest.php index e43f1db3..bba2adb0 100644 --- a/tests/Unit/EcommerceFinance/ProcessDetailsPageDataTest.php +++ b/tests/Unit/EcommerceFinance/ProcessDetailsPageDataTest.php @@ -53,8 +53,7 @@ class ProcessDetailsPageDataTest extends TestCase && $filters['paid_end'] === '2026-10-24' && $filters['payment_state'] === '已付款' && $filters['current_approver_name'] === '王五' - && $filters['entry_start'] === '2026-10-21' - && $filters['entry_end'] === '2026-10-25' + && $filters['waiting_bucket'] === '12_24h' ), 2, 50) ->willReturn([ 'data' => [$row], @@ -92,8 +91,7 @@ class ProcessDetailsPageDataTest extends TestCase 'paid_end' => '2026-10-24', 'payment_state' => '出纳已打款', 'current_approver_name' => '王五', - 'entry_start' => '2026-10-21', - 'entry_end' => '2026-10-25', + 'waiting_bucket' => '12_24h', 'page' => 2, ], )); @@ -109,8 +107,7 @@ class ProcessDetailsPageDataTest extends TestCase $this->assertSame('2026-10-24', $data['filters']['paid_end']); $this->assertSame('已付款', $data['filters']['payment_state']); $this->assertSame('王五', $data['filters']['current_approver_name']); - $this->assertSame('2026-10-21', $data['filters']['entry_start']); - $this->assertSame('2026-10-25', $data['filters']['entry_end']); + $this->assertSame('12_24h', $data['filters']['waiting_bucket']); $this->assertSame( [ '待付款', @@ -120,6 +117,10 @@ class ProcessDetailsPageDataTest extends TestCase $data['paymentStates'], ); $this->assertSame(['王五', '赵六'], $data['currentApprovers']); + $this->assertSame( + DingProcessDetailMirrorRepository::WAITING_BUCKET_LABELS, + $data['waitingBuckets'], + ); $this->assertTrue($data['mirrorStale']); $this->assertSame(15, $data['mirrorStaleAfterMinutes']); $this->assertArrayNotHasKey('months', $data);