Merge remote-tracking branch 'origin/main' into feature/92451-withdrawal-reserve-standards
This commit is contained in:
commit
e99fa7eec8
@ -20,6 +20,7 @@ class CourierSurchargeAuditLogRepository
|
||||
'courier' => '快递名称',
|
||||
'scope' => '快递归属',
|
||||
'reconciler' => '对账人',
|
||||
'supplier' => '供应商',
|
||||
'types' => '附加费类型',
|
||||
'mode' => '结算方式',
|
||||
'standard' => '计价标准',
|
||||
@ -98,6 +99,8 @@ class CourierSurchargeAuditLogRepository
|
||||
'actor_name' => $log->actor?->name ?: '系统',
|
||||
'action' => $log->action,
|
||||
'action_label' => self::ACTION_LABELS[$log->action] ?? $log->action,
|
||||
'target_type' => $log->target_type,
|
||||
'target_id' => $log->target_id,
|
||||
'target_type_label' => self::TARGET_LABELS[$log->target_type] ?? $log->target_type,
|
||||
'target_label' => $log->target_label,
|
||||
'changes_label' => $this->changesLabel($log),
|
||||
|
||||
@ -40,7 +40,11 @@ class CourierSurchargeQuoteRepository
|
||||
public function ready(): bool
|
||||
{
|
||||
try {
|
||||
return Schema::hasTable((new CourierSurchargeQuote())->getTable()) && $this->types->ready();
|
||||
$table = (new CourierSurchargeQuote())->getTable();
|
||||
|
||||
return Schema::hasTable($table)
|
||||
&& Schema::hasColumn($table, 'supplier')
|
||||
&& $this->types->ready();
|
||||
} catch (QueryException) {
|
||||
return false;
|
||||
}
|
||||
@ -63,8 +67,7 @@ class CourierSurchargeQuoteRepository
|
||||
|
||||
try {
|
||||
$all = CourierSurchargeQuote::query()
|
||||
->orderByDesc('effective_at')
|
||||
->orderByDesc('id')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
} catch (QueryException) {
|
||||
return [
|
||||
@ -76,7 +79,7 @@ class CourierSurchargeQuoteRepository
|
||||
}
|
||||
|
||||
$filtered = $all->filter(function (CourierSurchargeQuote $quote) use ($filters): bool {
|
||||
foreach (['courier', 'scope', 'reconciler', 'mode'] as $key) {
|
||||
foreach (['courier', 'scope', 'reconciler', 'supplier', 'mode'] as $key) {
|
||||
if (($filters[$key] ?? '') !== '' && (string) $quote->{$key} !== $filters[$key]) {
|
||||
return false;
|
||||
}
|
||||
@ -96,6 +99,7 @@ class CourierSurchargeQuoteRepository
|
||||
$quote->courier,
|
||||
$quote->scope,
|
||||
$quote->reconciler,
|
||||
(string) $quote->supplier,
|
||||
$quote->typeLabel(),
|
||||
$quote->mode,
|
||||
$quote->standard,
|
||||
@ -139,6 +143,7 @@ class CourierSurchargeQuoteRepository
|
||||
'modes' => collect(self::MODES),
|
||||
'scopes' => self::SCOPES,
|
||||
'reconcilers' => $all->pluck('reconciler')->filter()->unique()->sort()->values(),
|
||||
'suppliers' => $all->pluck('supplier')->filter()->unique()->sort()->values(),
|
||||
'statuses' => ['已启用', '已停用'],
|
||||
],
|
||||
];
|
||||
@ -168,6 +173,9 @@ class CourierSurchargeQuoteRepository
|
||||
->where('condition', trim((string) $data['condition']))
|
||||
->whereDate('effective_at', (string) $data['effective_at']);
|
||||
|
||||
$supplier = trim((string) ($data['supplier'] ?? ''));
|
||||
$supplier === '' ? $query->whereNull('supplier') : $query->where('supplier', $supplier);
|
||||
|
||||
if ($lockForUpdate) {
|
||||
$query->lockForUpdate();
|
||||
}
|
||||
@ -186,6 +194,7 @@ class CourierSurchargeQuoteRepository
|
||||
trim((string) ($data['courier'] ?? '')),
|
||||
trim((string) ($data['scope'] ?? '')),
|
||||
trim((string) ($data['reconciler'] ?? '')),
|
||||
trim((string) ($data['supplier'] ?? '')),
|
||||
$this->normalizedTypes((array) ($data['types'] ?? [])),
|
||||
trim((string) ($data['mode'] ?? '')),
|
||||
trim((string) ($data['effective_at'] ?? '')),
|
||||
@ -279,6 +288,7 @@ class CourierSurchargeQuoteRepository
|
||||
'courier' => $quote->courier,
|
||||
'scope' => $quote->scope,
|
||||
'reconciler' => $quote->reconciler,
|
||||
'supplier' => $quote->supplier,
|
||||
'types' => array_values((array) $quote->types),
|
||||
'mode' => $quote->mode,
|
||||
'standard' => $quote->standard,
|
||||
@ -302,6 +312,7 @@ class CourierSurchargeQuoteRepository
|
||||
'courier' => trim((string) $data['courier']),
|
||||
'scope' => trim((string) $data['scope']),
|
||||
'reconciler' => trim((string) $data['reconciler']),
|
||||
'supplier' => trim((string) ($data['supplier'] ?? '')) ?: null,
|
||||
'types' => array_values(array_unique(array_map('strval', $data['types']))),
|
||||
'mode' => trim((string) $data['mode']),
|
||||
'standard' => trim((string) $data['standard']),
|
||||
@ -333,6 +344,7 @@ class CourierSurchargeQuoteRepository
|
||||
'courier' => $quote->courier,
|
||||
'scope' => $quote->scope,
|
||||
'reconciler' => $quote->reconciler,
|
||||
'supplier' => $quote->supplier,
|
||||
'types' => array_values((array) $quote->types),
|
||||
'type' => $quote->typeLabel(),
|
||||
'mode' => $quote->mode,
|
||||
@ -356,6 +368,7 @@ class CourierSurchargeQuoteRepository
|
||||
'modes' => collect(self::MODES),
|
||||
'scopes' => self::SCOPES,
|
||||
'reconcilers' => collect(),
|
||||
'suppliers' => collect(),
|
||||
'statuses' => ['已启用', '已停用'],
|
||||
];
|
||||
}
|
||||
|
||||
@ -29,6 +29,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
'快递名称',
|
||||
'快递归属',
|
||||
'对账人',
|
||||
'供应商',
|
||||
'附加费类型',
|
||||
'结算方式',
|
||||
'计价标准',
|
||||
@ -64,7 +65,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
$sheet->freezePane('A2');
|
||||
$sheet->setAutoFilter($headerRange);
|
||||
|
||||
$widths = [18, 14, 14, 24, 18, 30, 32, 14, 12, 30];
|
||||
$widths = [18, 14, 14, 20, 24, 18, 30, 32, 14, 12, 30];
|
||||
foreach ($widths as $index => $width) {
|
||||
$column = Coordinate::stringFromColumnIndex($index + 1);
|
||||
$sheet->getColumnDimension($column)->setWidth($width);
|
||||
@ -78,6 +79,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
$instructions->fromArray([
|
||||
['项目', '填写要求'],
|
||||
['必填字段', '快递名称、快递归属、对账人、附加费类型、结算方式、计价标准、生效条件、生效日期、状态。'],
|
||||
['供应商', '选填;没有供应商时保持空白。'],
|
||||
['附加费类型', '多种类型使用“、”分隔;只能填写当前已启用类型。当前可选:' . implode('、', $types)],
|
||||
['快递归属', '自有快递 或 代发仓。'],
|
||||
['结算方式', implode('、', CourierSurchargeQuoteRepository::MODES)],
|
||||
@ -92,8 +94,8 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
]);
|
||||
$instructions->getColumnDimension('A')->setWidth(18);
|
||||
$instructions->getColumnDimension('B')->setWidth(90);
|
||||
$instructions->getStyle('A1:B9')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
|
||||
foreach (range(1, 9) as $row) {
|
||||
$instructions->getStyle('A1:B10')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
|
||||
foreach (range(1, 10) as $row) {
|
||||
$instructions->getRowDimension($row)->setRowHeight($row === 1 ? 24 : 34);
|
||||
}
|
||||
|
||||
@ -144,7 +146,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
}
|
||||
if ($headers !== self::HEADERS) {
|
||||
throw ValidationException::withMessages([
|
||||
'import' => '表头不正确。请下载最新模板,并保持 10 个字段名称及顺序不变。',
|
||||
'import' => '表头不正确。请下载最新模板,并保持 11 个字段名称及顺序不变。',
|
||||
]);
|
||||
}
|
||||
|
||||
@ -273,7 +275,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
}
|
||||
|
||||
$errors = [];
|
||||
foreach (array_slice(self::HEADERS, 0, 9) as $header) {
|
||||
foreach (['快递名称', '快递归属', '对账人', '附加费类型', '结算方式', '计价标准', '生效条件', '生效日期', '状态'] as $header) {
|
||||
if ($display[$header] === '') {
|
||||
$errors[] = $header . '不能为空。';
|
||||
}
|
||||
@ -281,6 +283,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
|
||||
$this->checkLength($display, '快递名称', 191, $errors);
|
||||
$this->checkLength($display, '对账人', 100, $errors);
|
||||
$this->checkLength($display, '供应商', 191, $errors);
|
||||
$this->checkLength($display, '计价标准', 2000, $errors);
|
||||
$this->checkLength($display, '生效条件', 2000, $errors);
|
||||
$this->checkLength($display, '备注', 2000, $errors);
|
||||
@ -333,6 +336,7 @@ class CourierSurchargeQuoteWorkbookImporter
|
||||
'courier' => $display['快递名称'],
|
||||
'scope' => $display['快递归属'],
|
||||
'reconciler' => $display['对账人'],
|
||||
'supplier' => $display['供应商'] !== '' ? $display['供应商'] : null,
|
||||
'types' => $types,
|
||||
'mode' => $display['结算方式'],
|
||||
'standard' => $display['计价标准'],
|
||||
|
||||
@ -134,6 +134,7 @@ class CourierSurchargeQuotes extends Controller
|
||||
'courier' => ['required', 'string', 'max:191'],
|
||||
'scope' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::SCOPES)],
|
||||
'reconciler' => ['required', 'string', 'max:100'],
|
||||
'supplier' => ['nullable', 'string', 'max:191'],
|
||||
'types' => ['required', 'array', 'min:1', 'max:50'],
|
||||
'types.*' => ['string', 'in:' . implode(',', $this->quotes->activeTypeNames())],
|
||||
'mode' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::MODES)],
|
||||
@ -145,6 +146,7 @@ class CourierSurchargeQuotes extends Controller
|
||||
|
||||
$data['courier'] = trim($data['courier']);
|
||||
$data['reconciler'] = trim($data['reconciler']);
|
||||
$data['supplier'] = trim((string) ($data['supplier'] ?? '')) ?: null;
|
||||
$data['types'] = array_values(array_unique($data['types']));
|
||||
|
||||
return $data;
|
||||
|
||||
@ -1138,6 +1138,7 @@ class Pages extends Controller
|
||||
'mode' => $clean(request('mode', ''), 40),
|
||||
'scope' => $clean(request('scope', ''), 20),
|
||||
'reconciler' => $clean(request('reconciler', ''), 40),
|
||||
'supplier' => $clean(request('supplier', ''), 191),
|
||||
'status' => $clean(request('status', ''), 20),
|
||||
];
|
||||
$data = $this->courierSurchargeQuotes->pageData($filters);
|
||||
|
||||
@ -17,6 +17,7 @@ class CourierSurchargeQuote extends Model
|
||||
'courier',
|
||||
'scope',
|
||||
'reconciler',
|
||||
'supplier',
|
||||
'types',
|
||||
'mode',
|
||||
'standard',
|
||||
|
||||
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! Schema::hasTable('business_courier_surcharge_quotes')
|
||||
|| Schema::hasColumn('business_courier_surcharge_quotes', 'supplier')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('business_courier_surcharge_quotes', function (Blueprint $table): void {
|
||||
$table->string('supplier', 191)->nullable()->after('reconciler');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! Schema::hasTable('business_courier_surcharge_quotes')
|
||||
|| ! Schema::hasColumn('business_courier_surcharge_quotes', 'supplier')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::table('business_courier_surcharge_quotes', function (Blueprint $table): void {
|
||||
$table->dropColumn('supplier');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -17,6 +17,7 @@
|
||||
['key' => 'courier', 'label' => '快递名称', 'exportable' => true],
|
||||
['key' => 'scope', 'label' => '快递归属', 'exportable' => true],
|
||||
['key' => 'reconciler', 'label' => '对账人', 'exportable' => true],
|
||||
['key' => 'supplier', 'label' => '供应商', 'exportable' => true],
|
||||
['key' => 'type', 'label' => '附加费类型', 'exportable' => true],
|
||||
['key' => 'mode', 'label' => '结算方式', 'exportable' => true],
|
||||
['key' => 'standard', 'label' => '计价标准', 'exportable' => true],
|
||||
@ -62,6 +63,16 @@
|
||||
.sm-audit-table td:nth-child(5) { min-width:24rem; white-space:normal; line-height:1.5; }
|
||||
.sm-audit-action { display:inline-flex; align-items:center; padding:.15rem .45rem; border-radius:999px; background:var(--sm-soft); color:var(--sm-accent); font-size:.7rem; font-weight:700; }
|
||||
.sm-audit-empty { padding:1.8rem 1rem; text-align:center; color:var(--sm-muted); }
|
||||
.sm-audit-empty[hidden],.sm-audit-table-wrap[hidden],.sm-audit-table tr[hidden] { display:none; }
|
||||
.sm-table tr[data-surcharge-row] { cursor:pointer; }
|
||||
.sm-table tr[data-surcharge-row]:focus-visible { outline:2px solid var(--sm-accent); outline-offset:-2px; }
|
||||
.sm-table tr.is-log-selected td,.sm-table tr.is-log-selected:hover td,.sm-table tr.is-log-selected [data-column-key="id"],.sm-table tr.is-log-selected [data-column-key="actions"] { background:var(--sm-soft); }
|
||||
.sm-table td { overflow:hidden; text-overflow:ellipsis; }
|
||||
.sm-table th .sm-table__resize { position:absolute; top:0; right:-.3rem; z-index:7; width:.6rem; height:100%; cursor:col-resize; touch-action:none; }
|
||||
.sm-table th .sm-table__resize::after { position:absolute; top:20%; bottom:20%; left:calc(50% - .5px); width:1px; background:var(--sm-border); content:""; }
|
||||
.sm-table th .sm-table__resize:hover::after,.sm-table th .sm-table__resize:focus-visible::after { width:2px; background:var(--sm-accent); }
|
||||
.sm-table th .sm-table__resize:focus-visible { outline:2px solid var(--sm-accent); outline-offset:-2px; }
|
||||
.sm-table.is-resizing,.sm-table.is-resizing * { cursor:col-resize !important; user-select:none; }
|
||||
.sm-column-picker { position:relative; } .sm-column-picker__panel { position:absolute; right:0; z-index:20; display:grid; width:min(22rem,calc(100vw - 2rem)); gap:.65rem; margin-top:.35rem; padding:.8rem; border:1px solid var(--sm-border); border-radius:.55rem; background:#fff; box-shadow:0 4px 8px rgba(23,23,23,.1); } .sm-column-picker__panel[hidden] { display:none; } .sm-column-picker__intro { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.7rem; line-height:1.45; } .sm-column-picker__intro strong { color:var(--sm-ink); font-size:.78rem; } .sm-visually-hidden { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip:rect(0,0,0,0); white-space:nowrap; border:0; } .sm-column-picker__list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.35rem; max-height:15rem; overflow:auto; } .sm-column-picker__option { display:flex; align-items:center; gap:.35rem; min-width:0; padding:.35rem .4rem; border:1px solid transparent; border-radius:.35rem; color:var(--sm-ink); font-size:.7rem; cursor:grab; } .sm-column-picker__option:hover,.sm-column-picker__option.is-drop-target { border-color:var(--sm-border); background:#fffdf8; } .sm-column-picker__option.is-fixed { cursor:default; } .sm-column-picker__option.is-dragging { opacity:.55; } .sm-column-picker__option input { flex:none; accent-color:var(--sm-accent); } .sm-column-picker__option .material-icons-outlined { color:var(--sm-muted); font-size:.95rem; } .sm-column-picker__foot { display:flex; justify-content:flex-end; padding-top:.35rem; border-top:1px solid var(--sm-border); }
|
||||
.sm-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; } .sm-badge.is-active { background:#edf5e8; color:#53793e; } .sm-badge.is-inactive { background:#fff4df; color:#935f07; }
|
||||
.sm-empty { padding:3rem 1rem; text-align:center; color:var(--sm-muted); } .sm-rule-note { padding:1rem 1.15rem; color:var(--sm-muted); font-size:.75rem; line-height:1.65; background:#fffdf8; }
|
||||
@ -114,10 +125,11 @@
|
||||
<div class="sm-card">
|
||||
<div class="sm-card__head"><div class="sm-card__title"><span class="material-icons-outlined" aria-hidden="true">tune</span>筛选报价规则</div><div class="sm-actions"><button class="sm-button" type="button" data-type-manage @disabled(!$ready)><span class="material-icons-outlined" aria-hidden="true">settings</span>管理附加费类型</button><button class="sm-button sm-button--primary" type="button" data-surcharge-open @disabled(!$ready)><span class="material-icons-outlined" aria-hidden="true">add</span>新增报价规则</button></div></div>
|
||||
<form method="GET" class="sm-toolbar" aria-label="报价规则筛选">
|
||||
<div class="sm-field"><label for="surcharge-keyword">快递名称 / 标准</label><input id="surcharge-keyword" name="keyword" value="{{ $filters['keyword'] ?? '' }}" placeholder="输入关键字"></div>
|
||||
<div class="sm-field"><label for="surcharge-keyword">快递名称 / 供应商 / 标准</label><input id="surcharge-keyword" name="keyword" value="{{ $filters['keyword'] ?? '' }}" placeholder="输入关键字"></div>
|
||||
<div class="sm-field"><label for="surcharge-courier">快递名称</label><select id="surcharge-courier" name="courier"><option value="">全部快递</option>@foreach (($options['couriers'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['courier'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-scope">快递归属</label><select id="surcharge-scope" name="scope"><option value="">全部归属</option>@foreach (($options['scopes'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['scope'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-reconciler">对账人</label><select id="surcharge-reconciler" name="reconciler"><option value="">全部对账人</option>@foreach (($options['reconcilers'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['reconciler'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-supplier">供应商</label><select id="surcharge-supplier" name="supplier"><option value="">全部供应商</option>@foreach (($options['suppliers'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['supplier'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-type">附加费类型</label><select id="surcharge-type" name="type"><option value="">全部类型</option>@foreach (($options['filter_types'] ?? $options['types'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['type'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-mode">结算方式</label><select id="surcharge-mode" name="mode"><option value="">全部方式</option>@foreach (($options['modes'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['mode'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-field"><label for="surcharge-status">状态</label><select id="surcharge-status" name="status"><option value="">全部状态</option>@foreach (($options['statuses'] ?? []) as $option)<option value="{{ $option }}" @selected(($filters['status'] ?? '') === $option)>{{ $option }}</option>@endforeach</select></div>
|
||||
@ -139,33 +151,33 @@
|
||||
<div class="sm-table-meta"><span>当前筛选结果(全部):<strong data-surcharge-row-count>{{ $rows->count() }}</strong> 条 · 共 {{ $rows->isEmpty() ? 0 : 1 }} 页</span><span>规则编号与操作列固定显示;列设置仅保存于当前用户</span></div>
|
||||
<div class="sm-table-wrap" data-surcharge-table-wrap><table class="sm-table" data-finance-table-native data-surcharge-table><thead><tr>
|
||||
@foreach ($columns as $column)
|
||||
<th scope="col" draggable="{{ empty($column['fixed']) ? 'true' : 'false' }}" data-column-header data-column-key="{{ $column['key'] }}" data-column-label="{{ $column['label'] }}" data-fixed="{{ !empty($column['fixed']) ? 'true' : 'false' }}"><span class="sm-table__header">@if (empty($column['fixed']))<span class="sm-table__drag material-icons-outlined" aria-hidden="true">drag_handle</span>@endif<span>{{ $column['label'] }}</span></span></th>
|
||||
<th scope="col" draggable="{{ empty($column['fixed']) ? 'true' : 'false' }}" data-column-header data-column-key="{{ $column['key'] }}" data-column-label="{{ $column['label'] }}" data-fixed="{{ !empty($column['fixed']) ? 'true' : 'false' }}"><span class="sm-table__header">@if (empty($column['fixed']))<span class="sm-table__drag material-icons-outlined" aria-hidden="true">drag_handle</span>@endif<span>{{ $column['label'] }}</span></span><span class="sm-table__resize" role="separator" aria-label="调整{{ $column['label'] }}列宽" aria-orientation="vertical" tabindex="0" draggable="false" data-column-resize></span></th>
|
||||
@endforeach
|
||||
</tr></thead><tbody>
|
||||
@forelse ($rows as $row)
|
||||
<tr data-surcharge-row data-rule='@json($row, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)' data-update-url="{{ route('business.courier-surcharge.update', ['quote' => $row['db_id']]) }}">
|
||||
<td data-column-key="id" data-export-value="{{ $row['id'] }}">{{ $row['id'] }}</td><td data-column-key="courier" data-export-value="{{ $row['courier'] }}">{{ $row['courier'] }}</td><td data-column-key="scope" data-export-value="{{ $row['scope'] }}">{{ $row['scope'] }}</td><td data-column-key="reconciler" data-export-value="{{ $row['reconciler'] }}">{{ $row['reconciler'] }}</td><td data-column-key="type" data-export-value="{{ $row['type'] }}">{{ $row['type'] }}</td><td data-column-key="mode" data-export-value="{{ $row['mode'] }}">{{ $row['mode'] }}</td><td data-column-key="standard" data-export-value="{{ $row['standard'] }}" class="wrap">{{ $row['standard'] }}</td><td data-column-key="condition" data-export-value="{{ $row['condition'] }}" class="wrap">{{ $row['condition'] }}</td><td data-column-key="effective_at" data-export-value="{{ $row['effective_at'] }}">{{ $row['effective_at'] }}</td><td data-column-key="status" data-export-value="{{ $row['status'] }}"><span class="sm-badge {{ $statusClass($row['status']) }}">{{ $row['status'] }}</span></td><td data-column-key="remark" data-export-value="{{ $row['remark'] ?: '—' }}" class="wrap">{{ $row['remark'] ?: '—' }}</td><td data-column-key="actions" data-fixed="true"><div class="sm-actions"><button class="sm-button" type="button" data-surcharge-edit @disabled(!$ready)>编辑</button><form method="POST" action="{{ route('business.courier-surcharge.status', ['quote' => $row['db_id']]) }}">@csrf<input type="hidden" name="status" value="{{ $row['status_code'] === 'active' ? 'inactive' : 'active' }}"><button class="sm-button" type="submit" @disabled(!$ready)>{{ $row['status'] === '已启用' ? '停用' : '启用' }}</button></form></div></td>
|
||||
<tr tabindex="0" aria-selected="false" aria-controls="surcharge-audit-results" aria-label="查看 {{ $row['courier'] }} {{ $row['type'] }} 的操作日志" data-surcharge-row data-audit-quote-id="{{ $row['db_id'] }}" data-audit-quote-label="{{ $row['courier'] }} · {{ $row['type'] }}" data-rule='@json($row, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT)' data-update-url="{{ route('business.courier-surcharge.update', ['quote' => $row['db_id']]) }}">
|
||||
<td data-column-key="id" data-export-value="{{ $row['id'] }}">{{ $row['id'] }}</td><td data-column-key="courier" data-export-value="{{ $row['courier'] }}">{{ $row['courier'] }}</td><td data-column-key="scope" data-export-value="{{ $row['scope'] }}">{{ $row['scope'] }}</td><td data-column-key="reconciler" data-export-value="{{ $row['reconciler'] }}">{{ $row['reconciler'] }}</td><td data-column-key="supplier" data-export-value="{{ $row['supplier'] ?: '' }}">{{ $row['supplier'] ?: '—' }}</td><td data-column-key="type" data-export-value="{{ $row['type'] }}">{{ $row['type'] }}</td><td data-column-key="mode" data-export-value="{{ $row['mode'] }}">{{ $row['mode'] }}</td><td data-column-key="standard" data-export-value="{{ $row['standard'] }}" class="wrap">{{ $row['standard'] }}</td><td data-column-key="condition" data-export-value="{{ $row['condition'] }}" class="wrap">{{ $row['condition'] }}</td><td data-column-key="effective_at" data-export-value="{{ $row['effective_at'] }}">{{ $row['effective_at'] }}</td><td data-column-key="status" data-export-value="{{ $row['status'] }}"><span class="sm-badge {{ $statusClass($row['status']) }}">{{ $row['status'] }}</span></td><td data-column-key="remark" data-export-value="{{ $row['remark'] ?: '—' }}" class="wrap">{{ $row['remark'] ?: '—' }}</td><td data-column-key="actions" data-fixed="true"><div class="sm-actions"><button class="sm-button" type="button" data-surcharge-edit @disabled(!$ready)>编辑</button><form method="POST" action="{{ route('business.courier-surcharge.status', ['quote' => $row['db_id']]) }}">@csrf<input type="hidden" name="status" value="{{ $row['status_code'] === 'active' ? 'inactive' : 'active' }}"><button class="sm-button" type="submit" @disabled(!$ready)>{{ $row['status'] === '已启用' ? '停用' : '启用' }}</button></form></div></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr data-surcharge-empty><td data-empty-cell colspan="12"><div class="sm-empty">没有符合条件的报价规则,请点击“新增报价规则”。</div></td></tr>
|
||||
<tr data-surcharge-empty><td data-empty-cell colspan="{{ count($columns) }}"><div class="sm-empty">没有符合条件的报价规则,请点击“新增报价规则”。</div></td></tr>
|
||||
@endforelse
|
||||
</tbody></table></div>
|
||||
<div class="sm-rule-note"><strong>维护建议:</strong>固定单价、按重量、按体积、按尺寸/最长边等方式直接维护标准;聚工单客服核实、取发出运费和按账单等方式只维护触发条件,具体金额由后续结算链路读取业务凭证。批量导入必须先预览校验,已有规则需明确选择跳过或更新。</div>
|
||||
</div>
|
||||
|
||||
<div class="sm-card" aria-label="附加费维护操作日志">
|
||||
<div class="sm-card__head"><div class="sm-card__title"><span class="material-icons-outlined" aria-hidden="true">history</span>操作日志</div><span class="sm-card__hint">记录当前公司最近 50 条新增、更改、启用和停用操作</span></div>
|
||||
<div class="sm-card" id="surcharge-audit-results" aria-label="附加费维护操作日志" data-surcharge-audit-panel>
|
||||
<div class="sm-card__head"><div class="sm-card__title"><span class="material-icons-outlined" aria-hidden="true">history</span><span data-surcharge-audit-title>操作日志</span></div><span class="sm-card__hint" data-surcharge-audit-hint>请选择报价</span></div>
|
||||
@if (!$auditReady)
|
||||
<div class="sm-audit-empty">操作日志表尚未就绪,完成迁移后将从首次新操作开始记录。</div>
|
||||
@elseif ($auditLogs->isEmpty())
|
||||
<div class="sm-audit-empty">暂无操作日志。新增、编辑或切换报价规则/附加费类型状态后,记录会显示在这里。</div>
|
||||
@else
|
||||
<div class="sm-audit-table-wrap">
|
||||
<div class="sm-audit-empty" data-surcharge-audit-placeholder>请选择上方一条报价</div>
|
||||
<div class="sm-audit-empty" data-surcharge-audit-empty hidden>该报价暂无操作日志。</div>
|
||||
<div class="sm-audit-table-wrap" data-surcharge-audit-table-wrap hidden>
|
||||
<table class="sm-audit-table">
|
||||
<thead><tr><th scope="col">操作时间</th><th scope="col">操作人</th><th scope="col">动作</th><th scope="col">对象</th><th scope="col">变更内容</th></tr></thead>
|
||||
<tbody>
|
||||
@foreach ($auditLogs as $log)
|
||||
<tr>
|
||||
<tr data-audit-log data-target-type="{{ $log['target_type'] }}" data-target-id="{{ $log['target_id'] }}" hidden>
|
||||
<td>{{ $log['occurred_at'] }}</td>
|
||||
<td>{{ $log['actor_name'] }}</td>
|
||||
<td><span class="sm-audit-action">{{ $log['action_label'] }}</span></td>
|
||||
@ -242,6 +254,7 @@
|
||||
<div class="sm-form__field"><label for="surcharge-form-courier">快递名称</label><input id="surcharge-form-courier" name="courier" required placeholder="例如:曹县德邦"></div>
|
||||
<div class="sm-form__field"><label for="surcharge-form-scope">快递归属</label><select id="surcharge-form-scope" name="scope" required><option value="">请选择</option>@foreach (($options['scopes'] ?? []) as $option)<option value="{{ $option }}">{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-form__field"><label for="surcharge-form-reconciler">对账人</label><input id="surcharge-form-reconciler" name="reconciler" required placeholder="例如:张会计"></div>
|
||||
<div class="sm-form__field"><label for="surcharge-form-supplier">供应商(可选)</label><input id="surcharge-form-supplier" name="supplier" maxlength="191" placeholder="没有供应商可留空"></div>
|
||||
<fieldset class="sm-form__field sm-form__field--full"><legend>附加费类型(可多选)</legend><div class="sm-form__types" id="surcharge-form-types">@foreach (($options['types'] ?? []) as $option)<label class="sm-type-option"><input type="checkbox" name="types[]" value="{{ $option }}"><span>{{ $option }}</span></label>@endforeach</div></fieldset>
|
||||
<div class="sm-form__field"><label for="surcharge-form-mode">结算方式</label><select id="surcharge-form-mode" name="mode" required><option value="">请选择</option>@foreach (($options['modes'] ?? []) as $option)<option value="{{ $option }}">{{ $option }}</option>@endforeach</select></div>
|
||||
<div class="sm-form__field"><label for="surcharge-form-standard">计价标准</label><input id="surcharge-form-standard" name="standard" required placeholder="例如:2 元/kg"></div>
|
||||
@ -278,6 +291,13 @@
|
||||
if (!root) return;
|
||||
const table = root.querySelector('[data-surcharge-table]');
|
||||
const tableWrap = root.querySelector('[data-surcharge-table-wrap]');
|
||||
const auditPanel = root.querySelector('[data-surcharge-audit-panel]');
|
||||
const auditTitle = root.querySelector('[data-surcharge-audit-title]');
|
||||
const auditHint = root.querySelector('[data-surcharge-audit-hint]');
|
||||
const auditPlaceholder = root.querySelector('[data-surcharge-audit-placeholder]');
|
||||
const auditEmpty = root.querySelector('[data-surcharge-audit-empty]');
|
||||
const auditTableWrap = root.querySelector('[data-surcharge-audit-table-wrap]');
|
||||
const auditRows = [...root.querySelectorAll('[data-audit-log]')];
|
||||
const picker = root.querySelector('[data-column-picker]');
|
||||
const columnToggle = picker?.querySelector('[data-column-toggle]');
|
||||
const columnPanel = picker?.querySelector('[data-column-options]') || picker?.querySelector('.sm-column-picker__panel');
|
||||
@ -289,7 +309,10 @@
|
||||
const mutableKeys = defaultOrder.filter((key) => !fixedKeys.has(key));
|
||||
const storageKey = root.dataset.columnStorageKey || 'caiwu:table-columns:courier-surcharge:quotes';
|
||||
const emptyCell = table?.querySelector('[data-empty-cell]');
|
||||
let state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
|
||||
const minColumnWidth = 72;
|
||||
const maxColumnWidth = 720;
|
||||
const clampColumnWidth = (width) => Math.min(maxColumnWidth, Math.max(minColumnWidth, Math.round(Number(width) || minColumnWidth)));
|
||||
let state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
|
||||
|
||||
const readState = () => {
|
||||
try {
|
||||
@ -301,16 +324,19 @@
|
||||
const visible = Array.isArray(saved.visible)
|
||||
? new Set(saved.visible.filter((key) => defaultOrder.includes(key)))
|
||||
: new Set(defaultOrder);
|
||||
const widths = Object.fromEntries(Object.entries(saved.widths || {})
|
||||
.filter(([key, width]) => defaultOrder.includes(key) && Number.isFinite(Number(width)))
|
||||
.map(([key, width]) => [key, clampColumnWidth(width)]));
|
||||
fixedKeys.forEach((key) => visible.add(key));
|
||||
state = { order, visible };
|
||||
state = { order, visible, widths };
|
||||
} catch (_) {
|
||||
state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
|
||||
state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
|
||||
}
|
||||
};
|
||||
|
||||
const writeState = () => {
|
||||
try {
|
||||
window.localStorage.setItem(storageKey, JSON.stringify({ order: state.order, visible: [...state.visible] }));
|
||||
window.localStorage.setItem(storageKey, JSON.stringify({ order: state.order, visible: [...state.visible], widths: state.widths }));
|
||||
} catch (_) {}
|
||||
};
|
||||
|
||||
@ -331,6 +357,22 @@
|
||||
keys.forEach((key) => { if (cells.has(key)) row.appendChild(cells.get(key)); });
|
||||
};
|
||||
|
||||
const applyColumnWidths = () => {
|
||||
if (!table) return;
|
||||
table.querySelectorAll('[data-column-key]').forEach((cell) => {
|
||||
const width = state.widths[cell.dataset.columnKey];
|
||||
if (Number.isFinite(width)) {
|
||||
cell.style.width = `${width}px`;
|
||||
cell.style.minWidth = `${width}px`;
|
||||
cell.style.maxWidth = `${width}px`;
|
||||
} else {
|
||||
cell.style.removeProperty('width');
|
||||
cell.style.removeProperty('min-width');
|
||||
cell.style.removeProperty('max-width');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const applyColumns = (save = true) => {
|
||||
if (!table) return;
|
||||
const keys = orderedKeys();
|
||||
@ -348,6 +390,7 @@
|
||||
const options = new Map([...columnList.querySelectorAll('[data-column-option]')].map((option) => [option.dataset.columnKey, option]));
|
||||
keys.forEach((key) => { if (options.has(key)) columnList.appendChild(options.get(key)); });
|
||||
}
|
||||
applyColumnWidths();
|
||||
if (save) writeState();
|
||||
};
|
||||
|
||||
@ -402,12 +445,13 @@
|
||||
applyColumns();
|
||||
}));
|
||||
picker?.querySelector('[data-column-reset]')?.addEventListener('click', () => {
|
||||
state = { order: [...defaultOrder], visible: new Set(defaultOrder) };
|
||||
state = { order: [...defaultOrder], visible: new Set(defaultOrder), widths: {} };
|
||||
applyColumns();
|
||||
});
|
||||
|
||||
const dragSource = (element, sourceKey) => {
|
||||
element.addEventListener('dragstart', (event) => {
|
||||
if (event.target.closest?.('[data-column-resize]')) { event.preventDefault(); return; }
|
||||
if (fixedKeys.has(sourceKey)) { event.preventDefault(); return; }
|
||||
event.dataTransfer.effectAllowed = 'move';
|
||||
event.dataTransfer.setData('text/plain', sourceKey);
|
||||
@ -431,6 +475,51 @@
|
||||
moveMutableKey(event.dataTransfer.getData('text/plain'), key, event.offsetX < header.offsetWidth / 2);
|
||||
});
|
||||
});
|
||||
headers.forEach((header) => {
|
||||
const handle = header.querySelector('[data-column-resize]');
|
||||
const key = header.dataset.columnKey;
|
||||
if (!handle || !key) return;
|
||||
const setWidth = (width, save = false) => {
|
||||
state.widths[key] = clampColumnWidth(width);
|
||||
applyColumnWidths();
|
||||
if (save) writeState();
|
||||
};
|
||||
handle.addEventListener('pointerdown', (event) => {
|
||||
if (event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const startX = event.clientX;
|
||||
const startWidth = header.getBoundingClientRect().width;
|
||||
table?.classList.add('is-resizing');
|
||||
handle.setPointerCapture?.(event.pointerId);
|
||||
const move = (moveEvent) => setWidth(startWidth + moveEvent.clientX - startX);
|
||||
const finish = (finishEvent) => {
|
||||
handle.removeEventListener('pointermove', move);
|
||||
handle.removeEventListener('pointerup', finish);
|
||||
handle.removeEventListener('pointercancel', finish);
|
||||
if (handle.hasPointerCapture?.(finishEvent.pointerId)) handle.releasePointerCapture(finishEvent.pointerId);
|
||||
table?.classList.remove('is-resizing');
|
||||
writeState();
|
||||
};
|
||||
handle.addEventListener('pointermove', move);
|
||||
handle.addEventListener('pointerup', finish);
|
||||
handle.addEventListener('pointercancel', finish);
|
||||
});
|
||||
handle.addEventListener('keydown', (event) => {
|
||||
if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const current = state.widths[key] || header.getBoundingClientRect().width;
|
||||
setWidth(current + (event.key === 'ArrowRight' ? 10 : -10), true);
|
||||
});
|
||||
handle.addEventListener('dblclick', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
delete state.widths[key];
|
||||
applyColumnWidths();
|
||||
writeState();
|
||||
});
|
||||
});
|
||||
columnList?.querySelectorAll('[data-column-option]').forEach((option) => {
|
||||
if (option.dataset.columnKey === undefined || fixedKeys.has(option.dataset.columnKey)) return;
|
||||
const key = option.dataset.columnKey;
|
||||
@ -453,6 +542,39 @@
|
||||
tableWrap.classList.toggle('is-scrolled-x', tableWrap.scrollLeft > 0);
|
||||
}, { passive: true });
|
||||
|
||||
const selectQuoteLog = (row) => {
|
||||
const quoteId = String(row.dataset.auditQuoteId || '');
|
||||
const quoteLabel = row.dataset.auditQuoteLabel || '当前报价';
|
||||
root.querySelectorAll('[data-surcharge-row]').forEach((candidate) => {
|
||||
const selected = candidate === row;
|
||||
candidate.classList.toggle('is-log-selected', selected);
|
||||
candidate.setAttribute('aria-selected', String(selected));
|
||||
});
|
||||
let visibleCount = 0;
|
||||
auditRows.forEach((auditRow) => {
|
||||
const visible = auditRow.dataset.targetType === 'quote' && auditRow.dataset.targetId === quoteId;
|
||||
auditRow.hidden = !visible;
|
||||
if (visible) visibleCount++;
|
||||
});
|
||||
if (auditTitle) auditTitle.textContent = `操作日志 · ${quoteLabel}`;
|
||||
if (auditHint) auditHint.textContent = visibleCount ? `${visibleCount} 条记录` : '暂无记录';
|
||||
if (auditPlaceholder) auditPlaceholder.hidden = true;
|
||||
if (auditEmpty) auditEmpty.hidden = visibleCount > 0;
|
||||
if (auditTableWrap) auditTableWrap.hidden = visibleCount === 0;
|
||||
auditPanel?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||
};
|
||||
root.querySelectorAll('[data-surcharge-row]').forEach((row) => {
|
||||
row.addEventListener('click', (event) => {
|
||||
if (event.target.closest('button,a,input,select,textarea,form,label')) return;
|
||||
selectQuoteLog(row);
|
||||
});
|
||||
row.addEventListener('keydown', (event) => {
|
||||
if (!['Enter', ' '].includes(event.key)) return;
|
||||
event.preventDefault();
|
||||
selectQuoteLog(row);
|
||||
});
|
||||
});
|
||||
|
||||
const modal = root.querySelector('[data-surcharge-modal]');
|
||||
const typeModal = root.querySelector('[data-type-modal]');
|
||||
const importModal = root.querySelector('[data-surcharge-import-modal]');
|
||||
@ -482,7 +604,7 @@
|
||||
});
|
||||
root.querySelectorAll('[data-surcharge-edit]').forEach((button) => button.addEventListener('click', () => {
|
||||
const row = button.closest('[data-surcharge-row]'); const data = JSON.parse(row.dataset.rule); form.action = row.dataset.updateUrl; methodField.value = 'PATCH'; title.textContent = '编辑报价规则';
|
||||
['courier', 'scope', 'mode', 'standard', 'condition', 'effective_at', 'reconciler', 'remark'].forEach((key) => { const field = form.elements.namedItem(key); if (field) field.value = data[key] || ''; });
|
||||
['courier', 'scope', 'mode', 'standard', 'condition', 'effective_at', 'reconciler', 'supplier', 'remark'].forEach((key) => { const field = form.elements.namedItem(key); if (field) field.value = data[key] || ''; });
|
||||
const selectedTypes = Array.isArray(data.types) ? data.types : (data.type || '').split('、').map((item) => item.trim()).filter(Boolean);
|
||||
form.querySelectorAll('input[name="types[]"]').forEach((checkbox) => { checkbox.checked = selectedTypes.includes(checkbox.value); });
|
||||
modal.classList.add('is-open');
|
||||
|
||||
@ -25,7 +25,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$this->createQuote();
|
||||
|
||||
$this->loginAs()
|
||||
$response = $this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
'section' => 'courier-payables',
|
||||
'page' => 'courier-surcharge',
|
||||
@ -46,6 +46,8 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
->assertSeeText('代发仓')
|
||||
->assertSeeText('对账人')
|
||||
->assertSeeText('张会计')
|
||||
->assertSeeText('供应商')
|
||||
->assertSeeText('曹县物流供应商')
|
||||
->assertSeeText('附加费类型(可多选)')
|
||||
->assertSeeText('列设置')
|
||||
->assertSeeText('导出当前列')
|
||||
@ -56,18 +58,27 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
->assertSeeText('规则编号与操作列固定显示')
|
||||
->assertSee('data-column-storage-key="caiwu:table-columns:', false)
|
||||
->assertSee('data-surcharge-table', false)
|
||||
->assertSee('data-surcharge-audit-panel', false)
|
||||
->assertSee('data-surcharge-audit-placeholder', false)
|
||||
->assertSee('data-surcharge-audit-table-wrap hidden', false)
|
||||
->assertSee('data-column-check', false)
|
||||
->assertSee('data-column-resize', false)
|
||||
->assertSee('aria-orientation="vertical"', false)
|
||||
->assertSee('data-surcharge-export-current', false)
|
||||
->assertSee('data-surcharge-export-all', false)
|
||||
->assertSee('data-column-reset', false)
|
||||
->assertSee('name="types[]"', false)
|
||||
->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"'], false)
|
||||
->assertSeeInOrder(['data-column-key="courier"', 'data-column-key="scope"', 'data-column-key="reconciler"', 'data-column-key="supplier"'], false)
|
||||
->assertSee('data-audit-quote-id="', false)
|
||||
->assertSeeText('曹县德邦')
|
||||
->assertSeeText('超长费')
|
||||
->assertSeeText('按尺寸/最长边')
|
||||
->assertSeeText('8 元/单')
|
||||
->assertSeeText('已启用')
|
||||
->assertSeeText('后续结算页面将按启用中的规则计算');
|
||||
|
||||
$this->assertSame(13, substr_count($response->getContent(), 'data-column-resize></span>'));
|
||||
$this->assertStringContainsString('widths: state.widths', $response->getContent());
|
||||
}
|
||||
|
||||
public function test_courier_surcharge_type_can_be_created_and_duplicate_is_rejected(): void
|
||||
@ -151,6 +162,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'courier' => '常州松宇货运',
|
||||
'scope' => '代发仓',
|
||||
'reconciler' => '李会计',
|
||||
'supplier' => '常州物流供应商',
|
||||
'types' => ['送货费'],
|
||||
'mode' => '按体积',
|
||||
'standard' => '体积 ≤ 10 m³:50 元/单',
|
||||
@ -167,6 +179,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'courier' => '常州松宇货运',
|
||||
'scope' => '代发仓',
|
||||
'reconciler' => '李会计',
|
||||
'supplier' => '常州物流供应商',
|
||||
'status' => '已启用',
|
||||
]))
|
||||
->assertOk()
|
||||
@ -174,11 +187,49 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
->assertSeeText('送货费')
|
||||
->assertSeeText('代发仓')
|
||||
->assertSeeText('李会计')
|
||||
->assertSeeText('常州物流供应商')
|
||||
->assertSeeText('当前规则')
|
||||
->assertDontSee('<td>曹县德邦</td>', false)
|
||||
->assertDontSee('<td>霸州百世快运-雅柏</td>', false);
|
||||
}
|
||||
|
||||
public function test_courier_surcharge_page_keeps_creation_order_instead_of_effective_date_order(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$first = CourierSurchargeQuote::query()->create([
|
||||
'company_id' => company_id(),
|
||||
'courier' => '顺序测试第一条',
|
||||
'scope' => '自有快递',
|
||||
'reconciler' => '苏木',
|
||||
'supplier' => null,
|
||||
'types' => ['送货费'],
|
||||
'mode' => '固定单价',
|
||||
'standard' => '1 元/单',
|
||||
'condition' => '第一条',
|
||||
'effective_at' => '2027-12-31',
|
||||
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
|
||||
]);
|
||||
$second = CourierSurchargeQuote::query()->create([
|
||||
'company_id' => company_id(),
|
||||
'courier' => '顺序测试第二条',
|
||||
'scope' => '自有快递',
|
||||
'reconciler' => '苏木',
|
||||
'supplier' => null,
|
||||
'types' => ['进仓费'],
|
||||
'mode' => '固定单价',
|
||||
'standard' => '2 元/单',
|
||||
'condition' => '第二条',
|
||||
'effective_at' => '2026-01-01',
|
||||
'status' => CourierSurchargeQuote::STATUS_ACTIVE,
|
||||
]);
|
||||
|
||||
$this->assertLessThan($second->id, $first->id);
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['顺序测试第一条', '顺序测试第二条']);
|
||||
}
|
||||
|
||||
public function test_courier_surcharge_store_update_and_status_are_persistent(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
@ -188,6 +239,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'courier' => '测试快递',
|
||||
'scope' => '代发仓',
|
||||
'reconciler' => '李会计',
|
||||
'supplier' => '',
|
||||
'types' => ['拦截费', '改地址'],
|
||||
'mode' => '固定单价',
|
||||
'standard' => '12 元/单',
|
||||
@ -203,6 +255,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$quote = CourierSurchargeQuote::query()->where('courier', '测试快递')->firstOrFail();
|
||||
$this->assertSame(['拦截费', '改地址'], $quote->types);
|
||||
$this->assertNull($quote->supplier);
|
||||
$this->assertSame(CourierSurchargeQuote::STATUS_ACTIVE, $quote->status);
|
||||
$this->assertSame((int) user_id(), (int) $quote->created_by);
|
||||
$this->assertSame((int) user_id(), (int) $quote->updated_by);
|
||||
@ -210,11 +263,13 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
|
||||
...$payload,
|
||||
'courier' => '测试快递(已修改)',
|
||||
'supplier' => '测试供应商',
|
||||
'types' => ['保费'],
|
||||
])->assertRedirect();
|
||||
|
||||
$quote->refresh();
|
||||
$this->assertSame('测试快递(已修改)', $quote->courier);
|
||||
$this->assertSame('测试供应商', $quote->supplier);
|
||||
$this->assertSame(['保费'], $quote->types);
|
||||
|
||||
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
|
||||
@ -233,6 +288,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'courier' => '日志测试快递',
|
||||
'scope' => '自有快递',
|
||||
'reconciler' => '周会计',
|
||||
'supplier' => '原供应商',
|
||||
'types' => ['进仓费'],
|
||||
'mode' => '固定单价',
|
||||
'standard' => '10 元/单',
|
||||
@ -255,6 +311,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$this->patch(route('business.courier-surcharge.update', ['quote' => $quote->id]), [
|
||||
...$payload,
|
||||
'supplier' => '新供应商',
|
||||
'standard' => '12 元/单',
|
||||
'remark' => '调整后标准',
|
||||
])->assertRedirect();
|
||||
@ -268,6 +325,8 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
$this->assertSame('12 元/单', data_get($updated->changes, 'after.standard'));
|
||||
$this->assertSame('初始标准', data_get($updated->changes, 'before.remark'));
|
||||
$this->assertSame('调整后标准', data_get($updated->changes, 'after.remark'));
|
||||
$this->assertSame('原供应商', data_get($updated->changes, 'before.supplier'));
|
||||
$this->assertSame('新供应商', data_get($updated->changes, 'after.supplier'));
|
||||
|
||||
$this->post(route('business.courier-surcharge.status', ['quote' => $quote->id]), [
|
||||
'status' => CourierSurchargeQuote::STATUS_INACTIVE,
|
||||
@ -289,15 +348,22 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'action' => CourierSurchargeAuditLog::ACTION_ACTIVATED,
|
||||
]);
|
||||
|
||||
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
|
||||
$response = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
|
||||
->assertOk()
|
||||
->assertSeeText('操作日志')
|
||||
->assertSeeText('请选择上方一条报价')
|
||||
->assertSeeText('新增')
|
||||
->assertSeeText('更改')
|
||||
->assertSeeText('停用')
|
||||
->assertSeeText('启用')
|
||||
->assertSeeText($this->user->name)
|
||||
->assertSeeText('供应商:原供应商 → 新供应商')
|
||||
->assertSeeText('计价标准:10 元/单 → 12 元/单');
|
||||
|
||||
$response
|
||||
->assertSee('data-audit-quote-id="' . $quote->id . '"', false)
|
||||
->assertSee('data-audit-log data-target-type="quote" data-target-id="' . $quote->id . '" hidden', false)
|
||||
->assertSee('aria-selected="false"', false);
|
||||
}
|
||||
|
||||
public function test_type_operations_are_recorded_and_other_company_logs_are_hidden(): void
|
||||
@ -339,7 +405,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge']))
|
||||
->assertOk()
|
||||
->assertSeeText('附加费类型 · 日志专用类型')
|
||||
->assertSee('data-audit-log data-target-type="type" data-target-id="' . $type->id . '" hidden', false)
|
||||
->assertDontSeeText('其他公司的私有类型');
|
||||
}
|
||||
|
||||
@ -383,10 +449,10 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
try {
|
||||
$this->assertSame(
|
||||
CourierSurchargeQuoteWorkbookImporter::HEADERS,
|
||||
$spreadsheet->getSheet(0)->rangeToArray('A1:J1')[0],
|
||||
$spreadsheet->getSheet(0)->rangeToArray('A1:K1')[0],
|
||||
);
|
||||
$this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle());
|
||||
$this->assertStringContainsString('不会静默覆盖', (string) $spreadsheet->getSheet(1)->getCell('B8')->getValue());
|
||||
$this->assertStringContainsString('不会静默覆盖', (string) $spreadsheet->getSheet(1)->getCell('B9')->getValue());
|
||||
} finally {
|
||||
$spreadsheet->disconnectWorksheets();
|
||||
}
|
||||
@ -397,7 +463,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$this->loginAs();
|
||||
$file = $this->workbook([[
|
||||
'导入测试快递', '代发仓', '李会计', '进仓费、超长费', '固定单价',
|
||||
'导入测试快递', '代发仓', '李会计', '导入供应商', '进仓费、超长费', '固定单价',
|
||||
'12.50 元/单', '入仓即收取', '2026-09-22', '已停用', '批量导入测试',
|
||||
]]);
|
||||
|
||||
@ -416,6 +482,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$quote = CourierSurchargeQuote::query()->where('courier', '导入测试快递')->firstOrFail();
|
||||
$this->assertSame(['进仓费', '超长费'], $quote->types);
|
||||
$this->assertSame('导入供应商', $quote->supplier);
|
||||
$this->assertSame(CourierSurchargeQuote::STATUS_INACTIVE, $quote->status);
|
||||
$this->assertSame('批量导入测试', $quote->remark);
|
||||
$this->assertDatabaseHas('business_courier_surcharge_audit_logs', [
|
||||
@ -441,7 +508,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$previewResponse = $this->post(route('business.courier-surcharge.import.preview'), [
|
||||
'import_file' => $this->workbook([[
|
||||
'', '未知归属', '李会计', '超长费、虚构费', '未知方式', '',
|
||||
'', '未知归属', '李会计', '', '超长费、虚构费', '未知方式', '',
|
||||
'测试条件', '2026-02-31', '未知状态', '',
|
||||
]]),
|
||||
]);
|
||||
@ -476,7 +543,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
])->assertSessionHasErrors('import');
|
||||
|
||||
$rows = array_fill(0, CourierSurchargeQuoteWorkbookImporter::MAX_ROWS + 1, [
|
||||
'测试快递', '自有快递', '周会计', '进仓费', '固定单价',
|
||||
'测试快递', '自有快递', '周会计', '', '进仓费', '固定单价',
|
||||
'1 元/单', '测试条件', '2026-09-22', '已启用', '',
|
||||
]);
|
||||
$this->withExceptionHandling()->post(route('business.courier-surcharge.import.preview'), [
|
||||
@ -489,7 +556,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$this->loginAs();
|
||||
$duplicateRow = [
|
||||
'文件内重复快递', '自有快递', '周会计', '进仓费', '固定单价',
|
||||
'文件内重复快递', '自有快递', '周会计', '重复测试供应商', '进仓费', '固定单价',
|
||||
'5 元/单', '重复条件', '2026-09-22', '已启用', '',
|
||||
];
|
||||
$response = $this->post(route('business.courier-surcharge.import.preview'), [
|
||||
@ -503,7 +570,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
|
||||
$existing = $this->createQuote();
|
||||
$existingRow = [
|
||||
$existing->courier, $existing->scope, $existing->reconciler, $existing->typeLabel(), $existing->mode,
|
||||
$existing->courier, $existing->scope, $existing->reconciler, $existing->supplier, $existing->typeLabel(), $existing->mode,
|
||||
'新计价标准', $existing->condition, $existing->effective_at->format('Y-m-d'), '已停用', '更新后的备注',
|
||||
];
|
||||
$skipPreview = $this->post(route('business.courier-surcharge.import.preview'), [
|
||||
@ -534,6 +601,24 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_import_duplicate_identity_includes_optional_supplier(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
$this->loginAs();
|
||||
$base = [
|
||||
'供应商区分快递', '自有快递', '周会计', '供应商甲', '进仓费', '固定单价',
|
||||
'5 元/单', '相同条件', '2026-09-22', '已启用', '',
|
||||
];
|
||||
$otherSupplier = $base;
|
||||
$otherSupplier[3] = '供应商乙';
|
||||
|
||||
$this->post(route('business.courier-surcharge.import.preview'), [
|
||||
'import_file' => $this->workbook([$base, $otherSupplier]),
|
||||
])->assertSessionHas('courierSurchargeImportPreview', function (array $preview): bool {
|
||||
return $preview['summary'] === ['total' => 2, 'valid' => 2, 'new' => 2, 'duplicate' => 0, 'invalid' => 0];
|
||||
});
|
||||
}
|
||||
|
||||
public function test_import_commit_rejects_expired_or_other_context_preview(): void
|
||||
{
|
||||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||||
@ -600,6 +685,7 @@ class CourierSurchargePageTest extends FeatureTestCase
|
||||
'courier' => '曹县德邦',
|
||||
'scope' => '自有快递',
|
||||
'reconciler' => '张会计',
|
||||
'supplier' => '曹县物流供应商',
|
||||
'types' => ['超长费'],
|
||||
'mode' => '按尺寸/最长边',
|
||||
'standard' => '最长边 > 160 cm:8 元/单',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user