增加签署任务查询筛选 #232

Merged
limeng merged 2 commits from fix/limeng-agreement-task-filter into main 2026-09-21 16:51:48 +08:00
3 changed files with 282 additions and 30 deletions

View File

@ -623,9 +623,38 @@ class Pages extends Controller
}
try {
return CourierAgreementSignature::query()
$type = $this->agreementFilterType();
if ($type === 'quote-confirmation') {
return collect();
}
$query = CourierAgreementSignature::query();
$search = $this->agreementFilterSearch();
$status = $this->agreementFilterStatus();
if ($search !== '') {
$like = '%' . addcslashes($search, '%_\\') . '%';
$query->where(function ($builder) use ($like): void {
$builder
->where('business_no', 'like', $like)
->orWhere('courier_name', 'like', $like)
->orWhere('organization_name', 'like', $like)
->orWhere('operator_name', 'like', $like)
->orWhere('company_full_name', 'like', $like);
});
}
if ($status !== null) {
$query->where('status', $status);
}
if ($type !== null) {
$query->where('document_type', $type);
}
return $query
->latest('id')
->paginate(50, ['*'], 'agreement_page')->withQueryString();
->paginate(50, ['*'], 'agreement_task_page')->withQueryString();
} catch (QueryException $exception) {
// 迁移尚未执行时仍允许打开页面;发送接口会明确提示数据库未就绪。
report($exception);
@ -633,6 +662,38 @@ class Pages extends Controller
}
}
protected function agreementFilterSearch(): string
{
return mb_substr(trim((string) request('agreement_search', '')), 0, 191);
}
protected function agreementFilterStatus(): ?string
{
$status = trim((string) request('agreement_status', ''));
$allowed = [
CourierAgreementSignature::STATUS_PREPARED,
CourierAgreementSignature::STATUS_SENDING,
CourierAgreementSignature::STATUS_SIGNING,
CourierAgreementSignature::STATUS_COMPLETED,
CourierAgreementSignature::STATUS_ARCHIVED,
CourierAgreementSignature::STATUS_REVOKED,
CourierAgreementSignature::STATUS_EXPIRED,
CourierAgreementSignature::STATUS_REJECTED,
CourierAgreementSignature::STATUS_FAILED,
CourierAgreementSignature::STATUS_UNKNOWN,
];
return in_array($status, $allowed, true) ? $status : null;
}
protected function agreementFilterType(): ?string
{
$type = trim((string) request('agreement_type', ''));
$allowed = array_keys(CourierAgreementSignature::DOCUMENTS);
return in_array($type, $allowed, true) ? $type : null;
}
protected function agreementSubjects(?string $page): Collection
{
if ($page !== 'agreement-signing') {
@ -658,10 +719,36 @@ class Pages extends Controller
}
try {
return CourierQuoteSignature::query()
->with('version')
$type = $this->agreementFilterType();
if ($type !== null && $type !== 'quote-confirmation') {
return collect();
}
$query = CourierQuoteSignature::query()->with('version');
$search = $this->agreementFilterSearch();
$status = $this->agreementFilterStatus();
if ($search !== '') {
$like = '%' . addcslashes($search, '%_\\') . '%';
$query->where(function ($builder) use ($like): void {
$builder
->where('organization_name', 'like', $like)
->orWhere('operator_name', 'like', $like)
->orWhereHas('version', function ($versionQuery) use ($like): void {
$versionQuery
->where('version_code', 'like', $like)
->orWhere('carrier_name', 'like', $like);
});
});
}
if ($status !== null) {
$query->where('status', $status);
}
return $query
->latest('id')
->paginate(50, ['*'], 'agreement_page')->withQueryString();
->paginate(50, ['*'], 'agreement_quote_page')->withQueryString();
} catch (QueryException $exception) {
// 报价签署迁移尚未执行时仍允许打开协议签署页。
report($exception);

View File

@ -9,6 +9,10 @@
@php($agreementCompanyName = $agreementDefaultSubject?->name ?: company()?->name ?: '当前公司')
@php($agreementSubjectPayload = $agreementSubjects->map(fn ($subject) => ['id' => $subject->id, 'name' => $subject->name, 'unified_social_credit_code' => $subject->unified_social_credit_code, 'is_default' => (bool) $subject->is_default])->values())
@php($esignConfigured = \App\Services\Esign\EsignClient::configured())
@php($agreementSearch = trim((string) request('agreement_search', '')))
@php($agreementStatus = trim((string) request('agreement_status', '')))
@php($agreementType = trim((string) request('agreement_type', '')))
@php($agreementFiltersActive = $agreementSearch !== '' || $agreementStatus !== '' || $agreementType !== '')
<section class="agreement-workspace" aria-label="协议签署" data-agreement-signing>
<style>
@ -1169,43 +1173,53 @@
</div>
<section class="agreement-panel" id="agreement-panel-tasks" role="tabpanel" aria-labelledby="agreement-tab-tasks" data-agreement-panel="tasks">
<div class="agreement-filter-bar">
<form class="agreement-filter-bar" method="GET" action="{{ url()->current() }}" data-task-filter-form>
<input type="hidden" name="agreement_view" value="tasks">
<div class="agreement-field">
<label for="agreement-task-search">搜索任务</label>
<div class="agreement-search">
<span class="material-icons-outlined" aria-hidden="true">search</span>
<input class="agreement-control" id="agreement-task-search" type="search" placeholder="任务编号、快递公司或签署人" autocomplete="off" data-task-filter>
<input class="agreement-control" id="agreement-task-search" name="agreement_search" type="search" value="{{ $agreementSearch }}" placeholder="任务编号、快递公司或签署人" autocomplete="off" data-task-filter>
</div>
</div>
<div class="agreement-field">
<label for="agreement-task-status">签署状态</label>
<select class="agreement-control" id="agreement-task-status" data-task-filter>
<select class="agreement-control" id="agreement-task-status" name="agreement_status" data-task-filter>
<option value="">全部状态</option>
<option value="draft">草稿</option>
<option value="signing">签署中</option>
<option value="completed">已完成</option>
<option value="rejected">已拒签</option>
<option value="expired">已过期</option>
<option value="prepared" @selected($agreementStatus === 'prepared')>待推送</option>
<option value="sending" @selected($agreementStatus === 'sending')>推送中</option>
<option value="signing" @selected($agreementStatus === 'signing')>待签署</option>
<option value="completed" @selected($agreementStatus === 'completed')>签署完成,待归档</option>
<option value="archived" @selected($agreementStatus === 'archived')>已签署并归档</option>
<option value="revoked" @selected($agreementStatus === 'revoked')>已撤销</option>
<option value="rejected" @selected($agreementStatus === 'rejected')>已拒签</option>
<option value="expired" @selected($agreementStatus === 'expired')>已过期</option>
<option value="failed" @selected($agreementStatus === 'failed')>处理失败</option>
<option value="unknown" @selected($agreementStatus === 'unknown')>状态未知</option>
</select>
</div>
<div class="agreement-field">
<label for="agreement-task-type">业务类型</label>
<select class="agreement-control" id="agreement-task-type" data-task-filter>
<select class="agreement-control" id="agreement-task-type" name="agreement_type" data-task-filter>
<option value="">全部类型</option>
<option value="account-change">收款账户变更</option>
<option value="account-proof">收款账户证明</option>
<option value="entity-change">公司主体变更</option>
<option value="shared-contract">共用合同证明</option>
<option value="cooperation-contract">合作合同</option>
<option value="authorization">授权委托书</option>
<option value="quote-confirmation">报价确认单</option>
<option value="account-change" @selected($agreementType === 'account-change')>收款账户变更</option>
<option value="account-proof" @selected($agreementType === 'account-proof')>收款账户证明</option>
<option value="entity-change" @selected($agreementType === 'entity-change')>公司主体变更</option>
<option value="shared-contract" @selected($agreementType === 'shared-contract')>共用合同证明</option>
<option value="cooperation-contract" @selected($agreementType === 'cooperation-contract')>合作合同</option>
<option value="authorization" @selected($agreementType === 'authorization')>授权委托书</option>
<option value="quote-confirmation" @selected($agreementType === 'quote-confirmation')>报价确认单</option>
</select>
</div>
<button class="agreement-button agreement-button--primary" type="submit">
<span class="material-icons-outlined" aria-hidden="true">search</span>
查询
</button>
<button class="agreement-button" type="button" data-task-reset>
<span class="material-icons-outlined" aria-hidden="true">restart_alt</span>
重置
</button>
</div>
</form>
<div class="agreement-table-wrap">
<table class="agreement-table">
@ -1299,8 +1313,13 @@
<span class="agreement-empty__icon">
<span class="material-icons-outlined" aria-hidden="true">assignment_late</span>
</span>
<strong>还没有协议签署任务</strong>
<p>完成E签宝接入后,发起、签署、拒签和归档记录会在这里统一追踪。</p>
@if ($agreementFiltersActive)
<strong>没有匹配的签署任务</strong>
<p>请调整筛选条件后重新查询。</p>
@else
<strong>还没有协议签署任务</strong>
<p>完成E签宝接入后,发起、签署、拒签和归档记录会在这里统一追踪。</p>
@endif
<button class="agreement-button agreement-button--primary" type="button" data-start-agreement>
<span class="material-icons-outlined" aria-hidden="true">add</span>
新建签署任务
@ -1317,10 +1336,14 @@
@php($quoteTotal = method_exists($agreementQuoteSignatures, 'total') ? $agreementQuoteSignatures->total() : $agreementQuoteSignatures->count())
@php($taskPages = method_exists($agreementTasks, 'lastPage') ? $agreementTasks->lastPage() : 0)
@php($quotePages = method_exists($agreementQuoteSignatures, 'lastPage') ? $agreementQuoteSignatures->lastPage() : 0)
@php($pageSource = $taskPages >= $quotePages ? $agreementTasks : $agreementQuoteSignatures)
<div class="agreement-panel-footnote">
<span>共 {{ $taskTotal + $quoteTotal }} 条 · 共 {{ $taskTotal + $quoteTotal > 0 ? max($taskPages, $quotePages) : 0 }} 页(每页两类签署记录各最多 50 条)</span>
@if (method_exists($pageSource, 'links')) {{ $pageSource->links() }} @endif
<span>共 {{ $taskTotal + $quoteTotal }} 条 · 协议任务 {{ $taskTotal }} 条,报价确认单 {{ $quoteTotal }} 条</span>
@if (method_exists($agreementTasks, 'links') && $taskPages > 1)
<span>协议任务分页:</span>{{ $agreementTasks->links() }}
@endif
@if (method_exists($agreementQuoteSignatures, 'links') && $quotePages > 1)
<span>报价确认单分页:</span>{{ $agreementQuoteSignatures->links() }}
@endif
</div>
<div class="agreement-panel-footnote">
<span class="material-icons-outlined" aria-hidden="true">shield</span>
@ -2708,10 +2731,12 @@
});
root.querySelector('[data-task-reset]').addEventListener('click', function () {
root.querySelectorAll('[data-task-filter]').forEach(function (control) {
control.value = '';
var url = new URL(window.location.href);
['agreement_search', 'agreement_status', 'agreement_type', 'agreement_task_page', 'agreement_quote_page', 'agreement_page'].forEach(function (key) {
url.searchParams.delete(key);
});
announce('筛选条件已重置');
url.searchParams.set('agreement_view', 'tasks');
window.location.assign(url.toString());
});
window.addEventListener('popstate', function () {

View File

@ -4,6 +4,9 @@ namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use App\Models\Business\CourierAgreementSignature;
use App\Models\Business\CourierQuoteChange;
use App\Models\Business\CourierQuoteSignature;
use App\Models\Business\CourierQuoteVersion;
use App\Services\Esign\EsignClient;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\HttpException;
@ -120,6 +123,63 @@ class AgreementSigningPageTest extends FeatureTestCase
->assertDontSee('<td>张经理</td>', false);
}
public function testAgreementSigningPageFiltersAgreementTasksBySearchStatusAndType(): void
{
$this->grantWorkspaceAccess();
$this->createAgreementTask([
'business_no' => 'AGR-FILTER-MATCH',
'document_type' => 'account-proof',
'status' => CourierAgreementSignature::STATUS_ARCHIVED,
'courier_name' => '筛选快递',
'operator_name' => '筛选签署人',
]);
$this->createAgreementTask([
'business_no' => 'AGR-FILTER-OTHER',
'document_type' => 'account-change',
'status' => CourierAgreementSignature::STATUS_SIGNING,
'courier_name' => '另一家快递',
'operator_name' => '另一位签署人',
]);
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'agreement-signing',
'agreement_search' => 'AGR-FILTER-MATCH',
'agreement_status' => CourierAgreementSignature::STATUS_ARCHIVED,
'agreement_type' => 'account-proof',
]))
->assertOk()
->assertSeeText('AGR-FILTER-MATCH')
->assertDontSeeText('AGR-FILTER-OTHER')
->assertSee('name="agreement_search"', false)
->assertSee('value="AGR-FILTER-MATCH"', false)
->assertSee('value="archived" selected', false)
->assertSee('value="account-proof" selected', false);
}
public function testAgreementSigningPageFiltersQuoteConfirmationsByVersionCarrierAndStatus(): void
{
$this->grantWorkspaceAccess();
$this->createQuoteSignature('QB-FILTER-MATCH', CourierQuoteSignature::STATUS_ARCHIVED, '筛选快递');
$this->createQuoteSignature('QB-FILTER-OTHER', CourierQuoteSignature::STATUS_SIGNING, '另一家快递');
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'courier-payables',
'page' => 'agreement-signing',
'agreement_search' => 'QB-FILTER-MATCH',
'agreement_status' => CourierQuoteSignature::STATUS_ARCHIVED,
'agreement_type' => 'quote-confirmation',
]))
->assertOk()
->assertSeeText('QB-FILTER-MATCH')
->assertDontSee('<td>QB-FILTER-OTHER</td>', false)
->assertDontSeeText('没有匹配的签署任务');
}
public function testAgreementSigningPageDistinguishesDefinitiveFailureFromUnknownResult(): void
{
$this->grantWorkspaceAccess();
@ -293,4 +353,84 @@ class AgreementSigningPageTest extends FeatureTestCase
$this->assertSame(404, $exception->getStatusCode());
}
}
private function createAgreementTask(array $overrides = []): CourierAgreementSignature
{
return CourierAgreementSignature::create(array_merge([
'company_id' => $this->company->id,
'business_no' => 'AGR-FILTER-' . uniqid(),
'idempotency_key' => 'filter-' . uniqid(),
'document_type' => 'account-proof',
'template_code' => 'TPL-ACCOUNT-CERT-V2',
'template_version' => '2',
'source_file_name' => '收款账户证明文件.doc',
'status' => CourierAgreementSignature::STATUS_SIGNING,
'courier_name' => '新北中通',
'company_full_name' => $this->company->name,
'organization_name' => '新北中通',
'operator_name' => '张经理',
'operator_account' => '13800138000',
'payload' => ['signing_subject' => $this->company->name],
'environment' => 'sandbox',
'source_pdf_path' => 'courier-agreements/filter/source.pdf',
'source_pdf_sha256' => str_repeat('a', 64),
'created_by' => $this->user->id,
'updated_by' => $this->user->id,
], $overrides));
}
private function createQuoteSignature(string $versionCode, string $status, string $carrierName): CourierQuoteSignature
{
$change = CourierQuoteChange::create([
'company_id' => $this->company->id,
'number' => 'BG-' . $versionCode,
'source_quote_id' => 'QB-SOURCE-' . $versionCode,
'source_sheet_name' => $carrierName . '报价',
'carrier_name' => $carrierName,
'quote_type' => '标准报价',
'change_type' => '面单调整',
'status' => CourierQuoteChange::STATUS_APPROVED,
'effective_date' => now()->toDateString(),
'is_retrospective' => false,
'recalculation_status' => CourierQuoteChange::RECALCULATION_NOT_REQUIRED,
'summary' => '筛选测试报价。',
'created_by' => $this->user->id,
'approved_by' => $this->user->id,
'approved_at' => now(),
]);
$version = CourierQuoteVersion::create([
'company_id' => $this->company->id,
'version_code' => $versionCode,
'source_quote_id' => 'QB-SOURCE-' . $versionCode,
'source_change_id' => $change->id,
'sheet_name' => $carrierName . '报价',
'company_name' => $this->company->name,
'carrier_name' => $carrierName,
'quote_type' => '标准报价',
'effective_date' => now()->toDateString(),
'source_range' => 'A1:B2',
'values_json' => json_encode([['地区', '面单'], ['广东省', 6.5]], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'formulas_json' => json_encode([[], []], JSON_THROW_ON_ERROR),
'status' => 'approved',
'signature_status' => '未签署',
'created_by' => $this->user->id,
'approved_by' => $this->user->id,
]);
return CourierQuoteSignature::create([
'company_id' => $this->company->id,
'quote_version_id' => $version->id,
'provider' => 'esign',
'environment' => 'sandbox',
'status' => $status,
'organization_name' => $carrierName,
'operator_name' => '筛选签署人',
'source_pdf_path' => 'courier-quotes/filter/' . $versionCode . '.pdf',
'source_pdf_sha256' => str_repeat('b', 64),
'source_pdf_generated_at' => now(),
'created_by' => $this->user->id,
'updated_by' => $this->user->id,
]);
}
}