feat(报价管理): 报价变更记录页增加快递公司/状态/变更类型筛选 #783
@ -26,19 +26,46 @@ class CourierQuoteChanges extends Controller
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(): View
|
||||
public function index(Request $request): View
|
||||
{
|
||||
$filters = $this->historyFilters($request);
|
||||
$changes = CourierQuoteChange::query()
|
||||
->with(['items', 'version'])
|
||||
->when($filters['carrier'] !== '', fn ($q) => $q->where('carrier_name', 'like', '%' . addcslashes($filters['carrier'], '%_\\') . '%'))
|
||||
->when($filters['status'] !== '', fn ($q) => $q->where('status', $filters['status']))
|
||||
->when($filters['change_type'] !== '', fn ($q) => $q->where('change_type', $filters['change_type']))
|
||||
->latest('updated_at')
|
||||
->paginate(20);
|
||||
->paginate(20)
|
||||
->withQueryString();
|
||||
|
||||
return view('business.pages.courier-payables.quote-change-history', [
|
||||
'changes' => $changes,
|
||||
'canReview' => $this->canReview(),
|
||||
'filters' => $filters,
|
||||
'carrierOptions' => CourierQuoteChange::query()->whereNotNull('carrier_name')->distinct()->orderBy('carrier_name')->pluck('carrier_name')->all(),
|
||||
'changeTypeOptions' => CourierQuoteChange::query()->whereNotNull('change_type')->distinct()->orderBy('change_type')->pluck('change_type')->all(),
|
||||
'statusOptions' => [
|
||||
CourierQuoteChange::STATUS_DRAFT => '草稿',
|
||||
CourierQuoteChange::STATUS_PENDING_REVIEW => '待审核',
|
||||
CourierQuoteChange::STATUS_RETURNED => '已退回',
|
||||
CourierQuoteChange::STATUS_APPROVED => '已审核',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array{carrier:string,status:string,change_type:string} */
|
||||
private function historyFilters(Request $request): array
|
||||
{
|
||||
$status = (string) $request->input('status', '');
|
||||
$allowed = [CourierQuoteChange::STATUS_DRAFT, CourierQuoteChange::STATUS_PENDING_REVIEW, CourierQuoteChange::STATUS_RETURNED, CourierQuoteChange::STATUS_APPROVED];
|
||||
|
||||
return [
|
||||
'carrier' => mb_substr(trim((string) $request->input('carrier', '')), 0, 100),
|
||||
'status' => in_array($status, $allowed, true) ? $status : '',
|
||||
'change_type' => mb_substr(trim((string) $request->input('change_type', '')), 0, 50),
|
||||
];
|
||||
}
|
||||
|
||||
public function create(Request $request): View|RedirectResponse
|
||||
{
|
||||
$quotes = $this->snapshot->load()['quotes'] ?? [];
|
||||
@ -195,7 +222,8 @@ class CourierQuoteChanges extends Controller
|
||||
}
|
||||
}
|
||||
$passed = count(array_filter($results, static fn ($result) => $result['success']));
|
||||
return redirect()->route('business.courier-quote-changes.index', ['page' => max(1, (int) $request->input('page', 1))])
|
||||
$back = array_filter($this->historyFilters($request), static fn ($v) => $v !== '');
|
||||
return redirect()->route('business.courier-quote-changes.index', ['page' => max(1, (int) $request->input('page', 1))] + $back)
|
||||
->with('success', '批量审核完成:成功 ' . $passed . ' 条,未通过 ' . (count($results) - $passed) . ' 条。')
|
||||
->with('batch_review_results', $results);
|
||||
}
|
||||
|
||||
@ -44,13 +44,26 @@
|
||||
@endif
|
||||
<section class="quote-history__panel" aria-labelledby="quote-history-title">
|
||||
<header class="quote-history__header"><div class="quote-history__title" id="quote-history-title"><span class="material-icons-outlined" aria-hidden="true">history</span><span>报价变更记录</span></div><div class="quote-history__hint">草稿、审核和新版本按当前公司隔离保存,历史记录不可覆盖原报价。</div></header>
|
||||
@if($changes->isEmpty())
|
||||
@php($filters = $filters ?? ['carrier' => '', 'status' => '', 'change_type' => ''])
|
||||
@php($filtering = ($filters['carrier'] ?? '') !== '' || ($filters['status'] ?? '') !== '' || ($filters['change_type'] ?? '') !== '')
|
||||
<form method="get" action="{{ route('business.courier-quote-changes.index') }}" class="qh-filter" role="search" aria-label="筛选报价变更记录">
|
||||
<label class="qh-filter__field"><span>快递公司</span><input type="search" name="carrier" value="{{ $filters['carrier'] }}" list="qh-carrier-options" placeholder="输入或选择快递公司" autocomplete="off"></label>
|
||||
<datalist id="qh-carrier-options">@foreach($carrierOptions ?? [] as $option)<option value="{{ $option }}"></option>@endforeach</datalist>
|
||||
<label class="qh-filter__field"><span>状态</span><select name="status"><option value="">全部</option>@foreach($statusOptions ?? [] as $value => $label)<option value="{{ $value }}" @selected($filters['status'] === $value)>{{ $label }}</option>@endforeach</select></label>
|
||||
<label class="qh-filter__field"><span>变更类型</span><select name="change_type"><option value="">全部</option>@foreach($changeTypeOptions ?? [] as $option)<option value="{{ $option }}" @selected($filters['change_type'] === $option)>{{ $option }}</option>@endforeach</select></label>
|
||||
<div class="qh-filter__actions"><button type="submit" class="quote-history__button quote-history__button--primary">筛选</button>@if($filtering)<a class="quote-history__button" href="{{ route('business.courier-quote-changes.index') }}">清空</a>@endif</div>
|
||||
@if($filtering)<div class="quote-history__hint qh-filter__count">筛选结果共 {{ $changes->total() }} 条</div>@endif
|
||||
</form>
|
||||
@if($changes->isEmpty() && $filtering)
|
||||
<div class="quote-history__empty"><span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span><strong>没有符合条件的变更记录</strong><div>换个快递公司、状态或变更类型试试,或点「清空」查看全部。</div></div>
|
||||
@elseif($changes->isEmpty())
|
||||
<div class="quote-history__empty"><span class="material-icons-outlined" aria-hidden="true">difference</span><strong>还没有报价变更记录</strong><div>发起第一张变更单后,草稿和审核进度会显示在这里。</div></div>
|
||||
@else
|
||||
@if($canReview)
|
||||
<form method="post" action="{{ route('business.courier-quote-changes.batch-approve') }}" data-batch-review>
|
||||
@csrf
|
||||
<input type="hidden" name="page" value="{{ $changes->currentPage() }}">
|
||||
@foreach(['carrier', 'status', 'change_type'] as $keep)@if(($filters[$keep] ?? '') !== '')<input type="hidden" name="{{ $keep }}" value="{{ $filters[$keep] }}">@endif @endforeach
|
||||
<div class="qh-review-toolbar"><span>已选 <strong data-review-count>0</strong> 条</span><button type="button" class="quote-history__button quote-history__button--primary" data-review-open disabled>批量审核通过</button><span class="quote-history__hint">可全选本页待审核项;逐条校验,失败项保留待处理。</span></div>
|
||||
@endif
|
||||
<div class="quote-history__table-wrap"><table><thead><tr>@if($canReview)<th><input type="checkbox" data-review-all aria-label="全选本页可审核记录"></th>@endif<th>变更单号</th><th>原报价</th><th>快递公司</th><th>变更类型</th><th>明细</th><th>生效日期</th><th>生效方式</th><th>追溯核算</th><th>状态</th><th>新版本</th><th>更新时间</th><th>操作</th></tr></thead><tbody>
|
||||
@ -75,7 +88,7 @@
|
||||
</section>
|
||||
</main>
|
||||
<style>
|
||||
.qh-review-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:12px;padding:12px 16px;border-bottom:1px solid #D8D2C7}.qh-review-toolbar strong{color:#856632}.quote-history input[type=checkbox]{width:16px;height:16px;accent-color:#856632;vertical-align:middle}.quote-history__button:disabled{opacity:.55;cursor:not-allowed}.qh-review-dialog{width:min(680px,calc(100vw - 32px));padding:24px;border:1px solid #D8D2C7;border-radius:12px;color:#171717;background:#fff;font:inherit}.qh-review-dialog::backdrop{background:#17171766}.qh-review-dialog h3{margin:0 0 12px;font-size:14px;font-weight:600}.qh-review-dialog p{font-size:12px;line-height:1.7;color:#595959}.qh-review-dialog ul{max-height:35vh;overflow:auto;padding-left:20px;line-height:1.8}.qh-review-confirm{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.7}.qh-review-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.qh-review-results{padding:12px 16px;margin-bottom:16px;border:1px solid #D8D2C7;border-radius:8px;background:#fff;font-size:12px;line-height:1.7}.qh-review-results ul{max-height:260px;overflow:auto}.qh-review-error{padding:12px;margin-bottom:16px;border:1px solid #E6B7B7;border-radius:8px;background:#FFF4F4;color:#B80000}.quote-history__table-wrap{max-height:60vh;overflow:auto}.quote-history th{position:sticky;top:0;z-index:1}.quote-history :focus-visible{outline:2px solid #856632;outline-offset:2px}
|
||||
.qh-review-toolbar{display:flex;align-items:center;flex-wrap:wrap;gap:12px;padding:12px 16px;border-bottom:1px solid #D8D2C7}.qh-review-toolbar strong{color:#856632}.quote-history input[type=checkbox]{width:16px;height:16px;accent-color:#856632;vertical-align:middle}.quote-history__button:disabled{opacity:.55;cursor:not-allowed}.qh-review-dialog{width:min(680px,calc(100vw - 32px));padding:24px;border:1px solid #D8D2C7;border-radius:12px;color:#171717;background:#fff;font:inherit}.qh-review-dialog::backdrop{background:#17171766}.qh-review-dialog h3{margin:0 0 12px;font-size:14px;font-weight:600}.qh-review-dialog p{font-size:12px;line-height:1.7;color:#595959}.qh-review-dialog ul{max-height:35vh;overflow:auto;padding-left:20px;line-height:1.8}.qh-review-confirm{display:flex;gap:8px;align-items:flex-start;font-size:12px;line-height:1.7}.qh-review-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:20px}.qh-review-results{padding:12px 16px;margin-bottom:16px;border:1px solid #D8D2C7;border-radius:8px;background:#fff;font-size:12px;line-height:1.7}.qh-review-results ul{max-height:260px;overflow:auto}.qh-review-error{padding:12px;margin-bottom:16px;border:1px solid #E6B7B7;border-radius:8px;background:#FFF4F4;color:#B80000}.quote-history__table-wrap{max-height:60vh;overflow:auto}.quote-history th{position:sticky;top:0;z-index:1}.quote-history :focus-visible{outline:2px solid #856632;outline-offset:2px}.qh-filter{display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px;padding:12px 16px;border-bottom:1px solid #D8D2C7}.qh-filter__field{display:flex;flex-direction:column;gap:4px;font-size:12px;color:#595959}.qh-filter__field input,.qh-filter__field select{height:34px;min-width:180px;border:1px solid #D8D2C7;border-radius:8px;padding:0 10px;background:#fff;color:#171717;font:inherit}.qh-filter__actions{display:flex;gap:8px}.qh-filter__count{width:100%;margin-top:0}.quote-history__pagination nav{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:8px}.quote-history__pagination ul{display:flex;flex-wrap:wrap;gap:6px;margin:0;padding:0;list-style:none}.quote-history__pagination li>a,.quote-history__pagination li>span{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:30px;padding:0 8px;border:1px solid #D8D2C7;border-radius:6px;background:#fff;color:#171717}.quote-history__pagination li.active>span,.quote-history__pagination [aria-current=page]>span{border-color:#856632;background:#856632;color:#fff}.quote-history__pagination li.disabled>span{opacity:.45}@media(max-width:800px){.qh-filter__field{flex:1 1 100%}.qh-filter__field input,.qh-filter__field select{min-width:0;width:100%}}
|
||||
</style>
|
||||
<script>
|
||||
(() => {
|
||||
|
||||
82
tests/Feature/Business/QuoteChangeHistoryFilterTest.php
Normal file
82
tests/Feature/Business/QuoteChangeHistoryFilterTest.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use App\Models\Auth\Role;
|
||||
use App\Models\Business\CourierQuoteChange;
|
||||
use App\Support\CourierQuoteChangeService;
|
||||
use App\Support\CourierStockQuoteEntryService;
|
||||
use App\Support\QuoteManagementSnapshot;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class QuoteChangeHistoryFilterTest extends FeatureTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Http::preventStrayRequests();
|
||||
app()->instance(QuoteManagementSnapshot::class, new QuoteManagementSnapshot('/nonexistent-history-filter-test.json'));
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
config(['courier_onboarding.allow_same_reviewer' => false]);
|
||||
}
|
||||
|
||||
private function reviewer()
|
||||
{
|
||||
$reviewer = user_model_class()::create(['name' => '筛选审核员', 'email' => 'filter-' . uniqid() . '@example.com', 'password' => 'local-test-password', 'locale' => 'zh-CN', 'enabled' => 1, 'landing_page' => 'dashboard', 'created_from' => 'core::test']);
|
||||
$reviewer->companies()->attach($this->company->id);
|
||||
$reviewer->attachRole(Role::where('name', 'admin')->firstOrFail());
|
||||
$reviewer->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
|
||||
return $reviewer;
|
||||
}
|
||||
|
||||
private function pending(string $name): CourierQuoteChange
|
||||
{
|
||||
$entry = app(CourierStockQuoteEntryService::class)->createPrepared(
|
||||
['quote_type' => '标准快递报价', 'effective_date' => '2026-08-01', 'summary' => '筛选测试', 'billing_rules' => '保留原报价规则'],
|
||||
$this->user->id, ['code' => 'FLT-' . $name, 'name' => $name],
|
||||
['sheet_name' => '原报价', 'source_range' => 'A1:B2', 'values' => [['省份', '运费'], ['江苏', 5]], 'formulas' => [[], []]]
|
||||
);
|
||||
|
||||
return app(CourierQuoteChangeService::class)->submit($entry->change, $this->user->id);
|
||||
}
|
||||
|
||||
public function test_history_filters_by_carrier_status_and_change_type(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
$jia = $this->pending('北京韵达');
|
||||
$yi = $this->pending('沐野竹木极兔');
|
||||
$yi->forceFill(['status' => CourierQuoteChange::STATUS_APPROVED])->save();
|
||||
|
||||
$this->get(route('business.courier-quote-changes.index'))
|
||||
->assertOk()->assertSee('快递公司')->assertSee('变更类型')->assertSee($jia->number)->assertSee($yi->number);
|
||||
|
||||
$this->get(route('business.courier-quote-changes.index', ['carrier' => '韵达']))
|
||||
->assertOk()->assertSee($jia->number)->assertDontSee($yi->number)->assertSee('筛选结果共 1 条');
|
||||
|
||||
$this->get(route('business.courier-quote-changes.index', ['status' => 'approved']))
|
||||
->assertOk()->assertSee($yi->number)->assertDontSee($jia->number);
|
||||
|
||||
$this->get(route('business.courier-quote-changes.index', ['change_type' => '存量报价', 'status' => 'pending_review']))
|
||||
->assertOk()->assertSee($jia->number)->assertDontSee($yi->number);
|
||||
|
||||
$this->get(route('business.courier-quote-changes.index', ['change_type' => '费率调整']))
|
||||
->assertOk()->assertSee('没有符合条件的变更记录')->assertDontSee($jia->number);
|
||||
|
||||
// 非法状态值忽略,不报错
|
||||
$this->get(route('business.courier-quote-changes.index', ['status' => 'bogus']))
|
||||
->assertOk()->assertSee($jia->number)->assertSee($yi->number);
|
||||
}
|
||||
|
||||
public function test_batch_approve_returns_to_the_same_filtered_list(): void
|
||||
{
|
||||
$this->loginAs();
|
||||
$jia = $this->pending('北京韵达');
|
||||
$this->loginAs($this->reviewer())->post(route('business.courier-quote-changes.batch-approve'), [
|
||||
'ids' => [$jia->id], 'stock_rules_confirmed' => 1, 'page' => 1, 'carrier' => '韵达', 'status' => 'pending_review',
|
||||
])->assertRedirect(route('business.courier-quote-changes.index', ['page' => 1, 'carrier' => '韵达', 'status' => 'pending_review']));
|
||||
$this->assertSame('approved', $jia->fresh()->status);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user