219 lines
23 KiB
PHP
219 lines
23 KiB
PHP
<x-layouts.admin>
|
|
<x-slot name="title">{{ $change ? '编辑报价变更' : '发起报价变更' }}</x-slot>
|
|
|
|
<x-slot name="content">
|
|
<style>
|
|
.quote-change-workspace {
|
|
--qc-ink: var(--vip-content-ink, #171717);
|
|
--qc-muted: var(--vip-content-muted, #595959);
|
|
--qc-gold: var(--vip-content-accent-dark, #856632);
|
|
--qc-gold-soft: var(--vip-content-accent-soft, #F7F0DE);
|
|
--qc-border: var(--vip-border, #D8D2C7);
|
|
margin: 24px 40px;
|
|
color: var(--qc-ink);
|
|
font-family: "JetBrains Mono", "Microsoft YaHei", "PingFang SC", monospace;
|
|
font-size: 13px;
|
|
}
|
|
.quote-change-workspace *, .quote-change-workspace *::before, .quote-change-workspace *::after { box-sizing: border-box; }
|
|
.quote-change-breadcrumb { display: flex; align-items: center; gap: 6px; margin-bottom: 16px; color: var(--qc-muted); font-size: 12px; }
|
|
.quote-change-breadcrumb a { color: var(--qc-gold); }
|
|
.quote-change-panel { border: 1px solid var(--qc-border); border-radius: 12px; background: #fff; }
|
|
.quote-change-panel + .quote-change-panel { margin-top: 16px; }
|
|
.quote-change-panel__header { display: flex; align-items: center; justify-content: space-between; gap: 16px; min-height: 58px; padding: 12px 16px; border-bottom: 1px solid var(--qc-border); }
|
|
.quote-change-panel__title { display: flex; align-items: center; gap: 8px; font-size: 14px; font-weight: 600; }
|
|
.quote-change-panel__title .material-icons-outlined { color: var(--qc-gold); font-size: 19px; }
|
|
.quote-change-panel__hint { margin-top: 3px; color: var(--qc-muted); font-size: 12px; }
|
|
.quote-change-panel__body { padding: 16px; }
|
|
.quote-change-alert { display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px; padding: 10px 12px; border: 1px solid var(--qc-border); border-radius: 8px; background: var(--qc-gold-soft); color: #513E21; font-size: 12px; line-height: 1.5; }
|
|
.quote-change-alert--error { border-color: #E6B7B7; background: #FFF4F4; color: #8A1515; }
|
|
.quote-change-alert--retrospective { margin: 14px 0 0; border-color: #D9BD88; background: #FFF8E8; color: #704800; }
|
|
.quote-change-alert[hidden] { display: none; }
|
|
.quote-change-alert .material-icons-outlined { flex: 0 0 auto; font-size: 18px; }
|
|
.quote-change-source { display: grid; grid-template-columns: minmax(220px, 1fr) auto; gap: 8px; align-items: end; }
|
|
.quote-change-form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; }
|
|
.quote-change-field { display: grid; gap: 6px; }
|
|
.quote-change-field--full { grid-column: 1 / -1; }
|
|
.quote-change-field label { color: var(--qc-ink); font-size: 12px; font-weight: 500; }
|
|
.quote-change-required { color: #B80000; }
|
|
.quote-change-control { width: 100%; height: 34px; border: 1px solid var(--qc-border); border-radius: 8px; background: #fff; padding: 0 10px; color: var(--qc-ink); font: inherit; }
|
|
textarea.quote-change-control { min-height: 88px; height: auto; padding: 9px 10px; resize: vertical; }
|
|
.quote-change-control:focus { border-color: var(--qc-gold); outline: 0; box-shadow: 0 0 0 3px rgba(133, 102, 50, .2); }
|
|
.quote-change-control[readonly] { background: #F8F6F1; color: var(--qc-muted); }
|
|
.quote-change-button { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 34px; border: 1px solid var(--qc-border); border-radius: 8px; background: #fff; padding: 0 14px; color: var(--qc-ink); font: inherit; font-weight: 500; white-space: nowrap; cursor: pointer; }
|
|
.quote-change-button:hover { border-color: var(--qc-gold); }
|
|
.quote-change-button:focus-visible { outline: 0; box-shadow: 0 0 0 3px rgba(133, 102, 50, .2); }
|
|
.quote-change-button--primary { border-color: var(--qc-gold); background: var(--qc-gold); color: #fff; }
|
|
.quote-change-button--primary:hover { border-color: #6A5129; background: #6A5129; }
|
|
.quote-change-button .material-icons-outlined { font-size: 18px; }
|
|
.quote-change-source-meta { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; margin-top: 14px; padding: 12px; border: 1px solid var(--qc-border); border-radius: 8px; background: #F8F6F1; }
|
|
.quote-change-source-meta dt { color: var(--qc-muted); font-size: 11px; }
|
|
.quote-change-source-meta dd { margin: 3px 0 0; font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
|
.quote-batch-tools { display: grid; grid-template-columns: minmax(160px, .8fr) minmax(130px, .6fr) auto; gap: 8px; align-items: end; }
|
|
.quote-change-table-wrap { overflow: auto; max-height: 540px; border: 1px solid var(--qc-border); border-radius: 8px; }
|
|
.quote-change-table { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
|
.quote-change-table th { position: sticky; top: 0; z-index: 1; padding: 9px 10px; border-bottom: 1px solid var(--qc-border); background: var(--qc-gold-soft); color: var(--qc-ink); font-size: 12px; font-weight: 600; text-align: left; white-space: nowrap; }
|
|
.quote-change-table td { padding: 7px 10px; border-bottom: 1px solid #ECE8E0; vertical-align: middle; }
|
|
.quote-change-table tbody tr:last-child td { border-bottom: 0; }
|
|
.quote-change-table tbody tr.is-changed td { background: #FFFDF7; }
|
|
.quote-change-table__number { text-align: right !important; font-variant-numeric: tabular-nums; }
|
|
.quote-change-price-input { width: 108px; height: 30px; border: 1px solid var(--qc-border); border-radius: 6px; padding: 0 8px; text-align: right; font: inherit; font-variant-numeric: tabular-nums; }
|
|
.quote-change-price-input:focus { border-color: var(--qc-gold); outline: 0; box-shadow: 0 0 0 2px rgba(133, 102, 50, .16); }
|
|
.quote-change-delta { font-weight: 600; font-variant-numeric: tabular-nums; }
|
|
.quote-change-delta--up { color: #935F07; }
|
|
.quote-change-delta--down { color: #53793E; }
|
|
.quote-change-note-input { width: 180px; height: 30px; border: 1px solid var(--qc-border); border-radius: 6px; padding: 0 8px; font: inherit; }
|
|
.quote-change-footer { position: sticky; bottom: 0; display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 16px; padding: 12px 16px; border: 1px solid var(--qc-border); border-radius: 12px; background: rgba(255, 255, 255, .96); }
|
|
.quote-change-footer__summary { color: var(--qc-muted); font-size: 12px; }
|
|
.quote-change-footer__summary strong { color: var(--qc-ink); font-variant-numeric: tabular-nums; }
|
|
.quote-change-footer__actions { display: flex; gap: 8px; }
|
|
@media (max-width: 900px) {
|
|
.quote-change-workspace { margin: 16px; }
|
|
.quote-change-form-grid, .quote-change-source-meta { grid-template-columns: 1fr; }
|
|
.quote-change-field--full { grid-column: auto; }
|
|
.quote-batch-tools { grid-template-columns: 1fr; }
|
|
.quote-change-footer { align-items: stretch; flex-direction: column; }
|
|
.quote-change-footer__actions { justify-content: flex-end; }
|
|
}
|
|
</style>
|
|
|
|
<main class="quote-change-workspace" data-quote-change-editor data-today="{{ now()->toDateString() }}">
|
|
<nav class="quote-change-breadcrumb" aria-label="当前位置">
|
|
<a href="{{ route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management']) }}">报价管理</a>
|
|
<span class="material-icons-outlined" aria-hidden="true" style="font-size:15px">chevron_right</span>
|
|
<span>{{ $change ? $change->number : '新建变更单' }}</span>
|
|
</nav>
|
|
|
|
@if (is_string(session('success')))
|
|
<div class="quote-change-alert" role="status"><span class="material-icons-outlined" aria-hidden="true">check_circle</span><span>{{ session('success') }}</span></div>
|
|
@endif
|
|
@if ($errors->any())
|
|
<div class="quote-change-alert quote-change-alert--error" role="alert">
|
|
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
|
<div><strong>草稿未保存,请检查:</strong><ul>@foreach ($errors->all() as $error)<li>{{ $error }}</li>@endforeach</ul></div>
|
|
</div>
|
|
@endif
|
|
|
|
@unless ($change)
|
|
<section class="quote-change-panel" aria-labelledby="quote-source-title">
|
|
<header class="quote-change-panel__header">
|
|
<div><div class="quote-change-panel__title" id="quote-source-title"><span class="material-icons-outlined" aria-hidden="true">source</span><span>第一步:选择原报价</span></div><div class="quote-change-panel__hint">原报价保持只读,系统只保存发生变化的单元格。</div></div>
|
|
</header>
|
|
<div class="quote-change-panel__body">
|
|
<form class="quote-change-source" method="GET" action="{{ route('business.courier-quote-changes.create') }}">
|
|
<div class="quote-change-field"><label for="source_quote_id">报价版本</label><select class="quote-change-control" id="source_quote_id" name="source_quote_id">@foreach ($quotes as $quote)<option value="{{ $quote['id'] }}" @selected($quote['id'] === $source['id'])>{{ $quote['id'] }} · {{ $quote['company_name'] }} · {{ $quote['quote_type'] }}</option>@endforeach</select></div>
|
|
<button class="quote-change-button" type="submit"><span class="material-icons-outlined" aria-hidden="true">sync_alt</span><span>载入报价</span></button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
@endunless
|
|
|
|
<form method="POST" action="{{ $change ? route('business.courier-quote-changes.update', $change) : route('business.courier-quote-changes.store') }}" data-quote-change-form>
|
|
@csrf
|
|
@if ($change) @method('PATCH') @else <input type="hidden" name="source_quote_id" value="{{ $source['id'] }}"> @endif
|
|
|
|
<section class="quote-change-panel" aria-labelledby="quote-base-title">
|
|
<header class="quote-change-panel__header"><div><div class="quote-change-panel__title" id="quote-base-title"><span class="material-icons-outlined" aria-hidden="true">edit_note</span><span>{{ $change ? '变更单基本信息' : '第二步:填写变更信息' }}</span></div><div class="quote-change-panel__hint">生效日期可以早于今天;历史日期会进入追溯核算流程。</div></div></header>
|
|
<div class="quote-change-panel__body">
|
|
<div class="quote-change-source-meta">
|
|
<div><dt>原报价版本</dt><dd>{{ $source['id'] }}</dd></div><div><dt>快递公司</dt><dd>{{ $source['company_name'] }}</dd></div><div><dt>报价类型</dt><dd>{{ $source['quote_type'] }}</dd></div><div><dt>原执行日期</dt><dd>{{ $source['effective_date'] ?? '未标注' }}</dd></div>
|
|
</div>
|
|
<div class="quote-change-form-grid" style="margin-top:16px">
|
|
<div class="quote-change-field"><label for="change_type">变更类型 <span class="quote-change-required">*</span></label><select class="quote-change-control" id="change_type" name="change_type" required>@foreach (['费率调整', '面单调整', '规则备注', '执行日期', '其他'] as $type)<option value="{{ $type }}" @selected(old('change_type', $change?->change_type ?? '费率调整') === $type)>{{ $type }}</option>@endforeach</select></div>
|
|
<div class="quote-change-field"><label for="effective_date">生效日期 <span class="quote-change-required">*</span></label><input class="quote-change-control" id="effective_date" name="effective_date" type="date" value="{{ old('effective_date', $change?->effective_date?->toDateString() ?? now()->toDateString()) }}" data-effective-date required></div>
|
|
<div class="quote-change-field quote-change-field--full"><label for="summary">变更依据与说明 <span class="quote-change-required">*</span></label><textarea class="quote-change-control" id="summary" name="summary" maxlength="2000" required placeholder="请说明调整区域、重量段、金额及业务依据">{{ old('summary', $change?->summary) }}</textarea></div>
|
|
</div>
|
|
<div class="quote-change-alert quote-change-alert--retrospective" data-retrospective-alert hidden role="status"><span class="material-icons-outlined" aria-hidden="true">history</span><span>这是历史生效日期。审核通过后应从 <strong data-retrospective-date></strong> 起追溯核算,只新增补款或扣款明细,不覆盖原运费。当前报价缺少可执行计算方式和逐票报价版本关联,在这些条件完整前系统不会自动写入可能错误的补差金额。</span></div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="quote-change-panel" aria-labelledby="quote-detail-edit-title">
|
|
<header class="quote-change-panel__header"><div><div class="quote-change-panel__title" id="quote-detail-edit-title"><span class="material-icons-outlined" aria-hidden="true">difference</span><span>{{ $change ? '变价明细' : '第三步:编辑新价格' }}</span></div><div class="quote-change-panel__hint">仅新价格可编辑;系统会再次核对原价格,防止过期数据覆盖。</div></div></header>
|
|
<div class="quote-change-panel__body">
|
|
<div class="quote-change-alert"><span class="material-icons-outlined" aria-hidden="true">verified_user</span><span>保存草稿不会让报价生效。提交审核后草稿将锁定,由另一位管理员审核并生成新版本。</span></div>
|
|
<div class="quote-batch-tools" aria-label="批量调整">
|
|
<div class="quote-change-field"><label for="batch_mode">批量方式</label><select class="quote-change-control" id="batch_mode" data-batch-mode><option value="add">统一加减金额</option><option value="percent">按比例调整</option><option value="set">统一设为金额</option></select></div>
|
|
<div class="quote-change-field"><label for="batch_value">调整值</label><input class="quote-change-control" id="batch_value" type="number" step="0.0001" data-batch-value placeholder="例如 1 或 -0.5"></div>
|
|
<button class="quote-change-button" type="button" data-batch-apply><span class="material-icons-outlined" aria-hidden="true">calculate</span><span>应用到勾选行</span></button>
|
|
</div>
|
|
<div class="quote-change-table-wrap" style="margin-top:12px">
|
|
<table class="quote-change-table">
|
|
<thead><tr><th scope="col"><input type="checkbox" data-select-all aria-label="全选可调整行"></th><th scope="col">单元格</th><th scope="col">地区/规则</th><th scope="col">重量段/计费项</th><th scope="col" class="quote-change-table__number">原价格</th><th scope="col" class="quote-change-table__number">新价格</th><th scope="col" class="quote-change-table__number">差额</th><th scope="col">备注</th></tr></thead>
|
|
<tbody>
|
|
@foreach ($editableCells as $index => $cell)
|
|
<tr data-price-row data-original="{{ $cell['original_amount'] }}">
|
|
<td><input type="checkbox" data-row-select aria-label="选择 {{ $cell['source_cell'] }}" @checked($cell['selected'])></td>
|
|
<td><strong>{{ $cell['source_cell'] }}</strong><input type="hidden" name="items[{{ $index }}][source_cell]" value="{{ $cell['source_cell'] }}"><input type="hidden" name="items[{{ $index }}][source_row]" value="{{ $cell['source_row'] }}"><input type="hidden" name="items[{{ $index }}][source_column]" value="{{ $cell['source_column'] }}"></td>
|
|
<td>{{ $cell['region'] ?: '—' }}<input type="hidden" name="items[{{ $index }}][region]" value="{{ $cell['region'] }}"></td>
|
|
<td>{{ $cell['weight_band'] ?: ($cell['charge_item'] ?: '—') }}<input type="hidden" name="items[{{ $index }}][weight_band]" value="{{ $cell['weight_band'] }}"><input type="hidden" name="items[{{ $index }}][charge_item]" value="{{ $cell['charge_item'] }}"></td>
|
|
<td class="quote-change-table__number">{{ rtrim(rtrim($cell['original_amount'], '0'), '.') }}<input type="hidden" name="items[{{ $index }}][original_amount]" value="{{ $cell['original_amount'] }}"></td>
|
|
<td class="quote-change-table__number"><input class="quote-change-price-input" type="number" name="items[{{ $index }}][new_amount]" min="0" step="0.0001" value="{{ old("items.$index.new_amount", $cell['new_amount']) }}" data-new-price required></td>
|
|
<td class="quote-change-table__number"><span class="quote-change-delta" data-price-delta>0.0000</span></td>
|
|
<td><input class="quote-change-note-input" name="items[{{ $index }}][note]" maxlength="500" value="{{ old("items.$index.note", $cell['note']) }}" placeholder="选填"><input type="hidden" name="items[{{ $index }}][unit]" value="{{ $cell['unit'] }}"></td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<footer class="quote-change-footer">
|
|
<div class="quote-change-footer__summary">当前已修改 <strong data-changed-count>0</strong> 个价格单元格;原始工作簿不会被修改。</div>
|
|
<div class="quote-change-footer__actions"><a class="quote-change-button" href="{{ $change ? route('business.courier-quote-changes.show', $change) : route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management']) }}">取消</a><button class="quote-change-button quote-change-button--primary" type="submit"><span class="material-icons-outlined" aria-hidden="true">save</span><span>保存草稿</span></button></div>
|
|
</footer>
|
|
</form>
|
|
|
|
@if ($change)
|
|
<form method="POST" action="{{ route('business.courier-quote-changes.submit', $change) }}" style="display:flex;justify-content:flex-end;margin-top:12px" onsubmit="return confirm('提交后草稿将锁定并进入审核,确认继续吗?')">@csrf<button class="quote-change-button" type="submit"><span class="material-icons-outlined" aria-hidden="true">send</span><span>提交审核</span></button></form>
|
|
@endif
|
|
</main>
|
|
|
|
<script>
|
|
(function () {
|
|
var root = document.querySelector('[data-quote-change-editor]');
|
|
if (!root || root.dataset.initialized === 'true') return;
|
|
root.dataset.initialized = 'true';
|
|
var rows = Array.from(root.querySelectorAll('[data-price-row]'));
|
|
var count = root.querySelector('[data-changed-count]');
|
|
var effectiveDate = root.querySelector('[data-effective-date]');
|
|
var retrospectiveAlert = root.querySelector('[data-retrospective-alert]');
|
|
function refreshRetrospective() {
|
|
if (!effectiveDate || !retrospectiveAlert) return;
|
|
var isRetrospective = effectiveDate.value !== '' && effectiveDate.value < root.dataset.today;
|
|
retrospectiveAlert.hidden = !isRetrospective;
|
|
if (isRetrospective) retrospectiveAlert.querySelector('[data-retrospective-date]').textContent = effectiveDate.value;
|
|
}
|
|
function refreshRow(row) {
|
|
var original = Number(row.dataset.original);
|
|
var input = row.querySelector('[data-new-price]');
|
|
var next = Number(input.value);
|
|
var delta = Number.isFinite(next) ? next - original : 0;
|
|
var output = row.querySelector('[data-price-delta]');
|
|
output.textContent = (delta > 0 ? '+' : '') + delta.toFixed(4);
|
|
output.className = 'quote-change-delta' + (delta > 0 ? ' quote-change-delta--up' : delta < 0 ? ' quote-change-delta--down' : '');
|
|
row.classList.toggle('is-changed', Math.abs(delta) > 0.00005);
|
|
if (Math.abs(delta) > 0.00005) row.querySelector('[data-row-select]').checked = true;
|
|
}
|
|
function refreshAll() { rows.forEach(refreshRow); count.textContent = String(rows.filter(function (row) { return row.classList.contains('is-changed'); }).length); }
|
|
rows.forEach(function (row) { row.querySelector('[data-new-price]').addEventListener('input', refreshAll); });
|
|
if (effectiveDate) effectiveDate.addEventListener('change', refreshRetrospective);
|
|
root.querySelector('[data-select-all]').addEventListener('change', function (event) { rows.forEach(function (row) { row.querySelector('[data-row-select]').checked = event.target.checked; }); });
|
|
root.querySelector('[data-batch-apply]').addEventListener('click', function () {
|
|
var value = Number(root.querySelector('[data-batch-value]').value);
|
|
if (!Number.isFinite(value)) { root.querySelector('[data-batch-value]').focus(); return; }
|
|
var mode = root.querySelector('[data-batch-mode]').value;
|
|
rows.filter(function (row) { return row.querySelector('[data-row-select]').checked; }).forEach(function (row) {
|
|
var input = row.querySelector('[data-new-price]');
|
|
var current = Number(input.value);
|
|
var result = mode === 'add' ? current + value : mode === 'percent' ? current * (1 + value / 100) : value;
|
|
input.value = Math.max(0, result).toFixed(4);
|
|
});
|
|
refreshAll();
|
|
});
|
|
refreshAll();
|
|
refreshRetrospective();
|
|
}());
|
|
</script>
|
|
</x-slot>
|
|
</x-layouts.admin>
|