1344 lines
57 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
try {
app(\App\Support\CourierQuoteSignatureService::class)->refreshPending();
} catch (\Throwable $exception) {
report($exception);
}
$quoteSnapshot = app(\App\Support\QuoteManagementSnapshot::class)->load();
$quotes = $quoteSnapshot['quotes'];
$quoteCompanies = collect($quotes)
->pluck('company_name')
->filter()
->unique()
->sort()
->values()
->all();
$quoteTypes = collect($quotes)->pluck('quote_type')->unique()->sort()->values();
$quoteView = in_array(request('quote_view'), ['warehouse', 'reverse-logistics'], true)
? request('quote_view')
: 'courier';
@endphp
<section class="quote-management" aria-label="报价管理" data-quote-management>
@if(isset($errors) && ($errors->has('end_date') || $errors->has('reason') || $errors->has('courier_name')))
<div role="alert" style="padding:12px;margin-bottom:12px;border:1px solid #D8D2C7;border-radius:8px;background:#F7F0DE;color:#B80000">{{ $errors->first() }}</div>
@endif
<style>
.quote-management {
--quote-accent: var(--vip-content-accent-dark, #856632);
--quote-accent-soft: var(--vip-content-accent-soft, #F7F0DE);
--quote-border: var(--vip-border, #D8D2C7);
--quote-ink: var(--vip-content-ink, #171717);
--quote-muted: var(--vip-content-muted, #595959);
display: grid;
width: 100%;
min-width: 0;
max-width: 100%;
gap: 1rem;
margin-top: 1.5rem;
color: var(--quote-ink);
font-family: JetBrains Mono, "Microsoft YaHei", "PingFang SC", monospace;
font-size: 0.8125rem;
letter-spacing: 0;
}
.quote-management *,
.quote-management *::before,
.quote-management *::after {
box-sizing: border-box;
}
.quote-local-tabs {
display: flex;
min-height: 2.75rem;
align-items: center;
gap: 1.5rem;
overflow-x: auto;
border-bottom: 1px solid var(--quote-border);
}
.quote-local-tab {
position: relative;
display: inline-flex;
min-height: 2.75rem;
flex: 0 0 auto;
align-items: center;
gap: .4rem;
border: 0;
background: transparent;
color: var(--quote-muted);
cursor: pointer;
font: inherit;
font-weight: 600;
}
.quote-local-tab .material-icons-outlined { font-size: 17px; }
.quote-local-tab.is-active { color: var(--quote-accent); }
.quote-local-tab.is-active::after { position:absolute; right:0; bottom:0; left:0; height:2px; background:var(--quote-accent); content:''; }
.quote-local-tab:focus-visible { outline:2px solid var(--quote-accent); outline-offset:2px; }
.quote-local-panel { display:grid; min-width:0; gap:1rem; }
.quote-summary,
.quote-workspace {
min-width: 0;
max-width: 100%;
width: 100%;
border: 1px solid var(--quote-border);
border-radius: 0.75rem;
background: #FFFFFF;
}
.quote-summary {
display: grid;
grid-template-columns: minmax(13rem, 1.4fr) repeat(4, minmax(7rem, 0.65fr));
align-items: stretch;
overflow: hidden;
}
.quote-source,
.quote-metric {
min-width: 0;
padding: 0.875rem 1rem;
}
.quote-source {
display: flex;
align-items: center;
gap: 0.75rem;
}
.quote-source__icon {
display: grid;
flex: 0 0 34px;
width: 34px;
height: 34px;
place-items: center;
border-radius: 8px;
background: var(--quote-accent-soft);
color: var(--quote-accent);
}
.quote-source__icon .material-icons-outlined {
font-size: 20px;
}
.quote-source__body {
min-width: 0;
}
.quote-source__file {
overflow: hidden;
font-size: 0.8125rem;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.quote-source__state,
.quote-metric__label,
.quote-table__secondary,
.quote-detail-meta dt {
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-metric {
border-left: 1px solid var(--quote-border);
}
.quote-metric__value {
margin-top: 0.2rem;
font-size: 1rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.quote-toolbar {
min-width: 0;
display: grid;
grid-template-columns: minmax(13rem, 1.2fr) repeat(3, minmax(9rem, 0.65fr)) auto;
gap: 0.75rem;
align-items: end;
padding: 1rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-field {
display: grid;
gap: 0.35rem;
min-width: 0;
}
.quote-field__label {
color: var(--quote-muted);
font-size: 0.75rem;
font-weight: 500;
}
.quote-control,
.quote-icon-button,
.quote-button {
height: 34px;
border: 1px solid var(--quote-border);
border-radius: 8px;
background: #FFFFFF;
color: var(--quote-ink);
font: inherit;
}
.quote-control {
width: 100%;
min-width: 0;
padding: 0 10px;
}
.quote-control:focus,
.quote-icon-button:focus-visible,
.quote-button:focus-visible,
.quote-action:focus-visible,
.quote-modal__close:focus-visible {
border-color: var(--quote-accent);
outline: none;
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.20);
}
.quote-toolbar__actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.quote-icon-button,
.quote-button,
.quote-action,
.quote-modal__close {
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
}
.quote-icon-button {
width: 34px;
padding: 0;
}
.quote-icon-button .material-icons-outlined,
.quote-button .material-icons-outlined {
font-size: 18px;
}
.quote-button {
gap: 0.4rem;
padding: 0 14px;
}
.quote-button--primary {
border-color: var(--quote-accent);
background: var(--quote-accent);
color: #FFFFFF;
}
.quote-button--primary:hover {
border-color: #6A5129;
background: #6A5129;
}
.quote-button[disabled] {
cursor: not-allowed;
opacity: 0.58;
}
.quote-results {
min-width: 0;
display: flex;
min-height: 2.75rem;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 1rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-results__count {
color: var(--quote-muted);
font-size: 0.75rem;
font-variant-numeric: tabular-nums;
}
.quote-results__notice {
display: inline-flex;
align-items: center;
gap: 0.35rem;
color: var(--quote-accent);
font-size: 0.75rem;
}
.quote-results__notice .material-icons-outlined {
font-size: 16px;
}
.quote-table-wrap,
.quote-detail-table-wrap {
min-width: 0;
max-width: 100%;
width: 100%;
overflow: auto;
scrollbar-color: #B9954F #F2EFE8;
scrollbar-width: thin;
}
.quote-table-wrap::-webkit-scrollbar,
.quote-detail-table-wrap::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.quote-table-wrap::-webkit-scrollbar-thumb,
.quote-detail-table-wrap::-webkit-scrollbar-thumb {
border-radius: 4px;
background: #B9954F;
}
.quote-table-wrap::-webkit-scrollbar-track,
.quote-detail-table-wrap::-webkit-scrollbar-track {
background: #F2EFE8;
}
.quote-table,
.quote-detail-table {
width: 100%;
border-collapse: collapse;
font-size: 0.78125rem;
font-variant-numeric: tabular-nums;
}
.quote-table {
min-width: 67rem;
}
.quote-table th,
.quote-table td,
.quote-detail-table th,
.quote-detail-table td {
border-bottom: 1px solid var(--quote-border);
padding: 0.65rem 0.75rem;
text-align: left;
vertical-align: middle;
}
.quote-table th,
.quote-detail-table th {
background: var(--quote-accent-soft);
color: var(--quote-muted);
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.quote-table tbody tr:hover td {
background: #FCFAF5;
}
.quote-table__name {
display: block;
max-width: 17rem;
overflow: hidden;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.quote-badge {
display: inline-flex;
align-items: center;
min-height: 24px;
border: 1px solid var(--quote-border);
border-radius: 999px;
padding: 0.15rem 0.55rem;
background: #FFFFFF;
color: var(--quote-muted);
font-size: 0.75rem;
white-space: nowrap;
}
.quote-badge--ready {
border-color: #A9C49A;
background: #F2F7EF;
color: #53793E;
}
.quote-badge--muted {
border-color: #DDD8CE;
background: #F4F2EE;
color: #8A8478;
}
.quote-badge--attention {
border-color: #D7B77D;
background: #FFF8E9;
color: #935F07;
}
.quote-signed {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin: 0.75rem 1rem 0;
padding: 0.6rem 0.8rem;
border: 1px solid #CFE3C8;
border-radius: 0.5rem;
background: #F1F8EE;
color: #2F6B2A;
}
.quote-modal__panel > .quote-detail-table-wrap {
grid-row: 5;
}
.quote-signed[hidden],
.quote-signed [hidden] {
display: none;
}
.quote-signed__text {
margin-right: auto;
font-weight: 600;
}
.quote-action {
min-width: 3.2rem;
border: 0;
padding: 0.25rem 0.4rem;
background: transparent;
color: var(--quote-accent);
font: inherit;
font-weight: 600;
}
.quote-action:hover {
background: var(--quote-accent-soft);
}
.quote-action[disabled] {
cursor: not-allowed;
opacity: 0.48;
}
.quote-action[disabled]:hover {
background: transparent;
}
.quote-empty {
display: grid;
min-height: 15rem;
place-items: center;
padding: 2rem;
text-align: center;
}
.quote-empty__body {
display: grid;
justify-items: center;
gap: 0.45rem;
max-width: 30rem;
}
.quote-empty .material-icons-outlined {
color: var(--quote-accent);
font-size: 32px;
}
.quote-empty__title {
font-weight: 600;
}
.quote-empty__text {
color: var(--quote-muted);
}
.quote-pagination {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
min-height: 3.5rem;
padding: 0.65rem 1rem;
}
.quote-pagination__label {
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-pagination__buttons {
display: flex;
gap: 0.5rem;
}
.quote-modal {
position: fixed;
z-index: 1200;
inset: 0;
display: none;
align-items: stretch;
justify-content: flex-end;
background: rgba(5, 5, 6, 0.52);
}
.quote-modal.is-open {
display: flex;
}
.quote-modal__panel {
display: grid;
grid-template-rows: auto auto auto auto minmax(0, 1fr);
width: min(78rem, calc(100vw - 3rem));
height: 100%;
background: #FFFFFF;
box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10);
}
.quote-modal__header {
display: flex;
min-width: 0;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--quote-border);
}
.quote-modal__title-wrap {
min-width: 0;
}
.quote-modal__title {
overflow-wrap: anywhere;
font-size: 0.875rem;
font-weight: 600;
}
.quote-modal__subtitle {
margin-top: 0.2rem;
color: var(--quote-muted);
font-size: 0.75rem;
}
.quote-modal__close {
flex: 0 0 34px;
width: 34px;
height: 34px;
border: 1px solid var(--quote-border);
border-radius: 8px;
background: #FFFFFF;
color: var(--quote-ink);
}
.quote-detail-meta {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
margin: 0;
border-bottom: 1px solid var(--quote-border);
}
.quote-detail-meta div {
min-width: 0;
padding: 0.75rem 1rem;
border-right: 1px solid var(--quote-border);
}
.quote-detail-meta div:last-child {
border-right: 0;
}
.quote-detail-meta dd {
margin: 0.2rem 0 0;
overflow-wrap: anywhere;
font-size: 0.78125rem;
font-weight: 600;
}
.quote-detail-table-wrap {
min-height: 0;
padding: 1rem 1.25rem 1.25rem;
}
.quote-detail-table {
min-width: max-content;
border-top: 1px solid var(--quote-border);
border-left: 1px solid var(--quote-border);
}
.quote-detail-table th,
.quote-detail-table td {
min-width: 8rem;
max-width: 24rem;
border-right: 1px solid var(--quote-border);
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.quote-detail-table .quote-detail-table__row-number {
min-width: 3.75rem;
width: 3.75rem;
background: #F8F6F1;
color: var(--quote-muted);
text-align: center;
}
.quote-detail-table__formula {
display: block;
margin-top: 0.25rem;
color: var(--quote-accent);
font-size: 0.7rem;
}
[hidden] {
display: none !important;
}
@media (max-width: 1050px) {
.quote-summary {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.quote-source {
grid-column: 1 / -1;
border-bottom: 1px solid var(--quote-border);
}
.quote-metric:first-of-type {
border-left: 0;
}
.quote-toolbar {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.quote-toolbar__actions {
grid-column: 1 / -1;
justify-content: flex-end;
}
.quote-detail-meta {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.quote-detail-meta div:nth-child(3) {
border-right: 0;
}
.quote-detail-meta div:nth-child(-n + 3) {
border-bottom: 1px solid var(--quote-border);
}
}
@media (max-width: 640px) {
.quote-management {
margin-top: 1rem;
}
.quote-summary,
.quote-toolbar,
.quote-detail-meta {
grid-template-columns: 1fr;
}
.quote-source,
.quote-metric,
.quote-detail-meta div,
.quote-detail-meta div:nth-child(3) {
grid-column: auto;
border-right: 0;
border-bottom: 1px solid var(--quote-border);
}
.quote-metric:last-child,
.quote-detail-meta div:last-child {
border-bottom: 0;
}
.quote-toolbar__actions {
grid-column: auto;
justify-content: stretch;
}
.quote-toolbar__actions .quote-button {
flex: 1;
}
.quote-results,
.quote-pagination {
align-items: flex-start;
flex-direction: column;
}
.quote-pagination__buttons {
width: 100%;
}
.quote-pagination__buttons .quote-button {
flex: 1;
}
.quote-modal__panel {
width: 100%;
}
}
</style>
<nav class="quote-local-tabs" role="tablist" aria-label="报价管理分类">
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'courier']) data-quote-view="courier" aria-controls="quote-courier-panel" aria-selected="{{ $quoteView === 'courier' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">local_shipping</span><span>快递报价</span>
</button>
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'warehouse']) data-quote-view="warehouse" aria-controls="quote-warehouse-panel" aria-selected="{{ $quoteView === 'warehouse' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">warehouse</span><span>仓储报价</span>
</button>
<button type="button" role="tab" @class(['quote-local-tab', 'is-active' => $quoteView === 'reverse-logistics']) data-quote-view="reverse-logistics" aria-controls="quote-reverse-logistics-panel" aria-selected="{{ $quoteView === 'reverse-logistics' ? 'true' : 'false' }}">
<span class="material-icons-outlined" aria-hidden="true">assignment_return</span><span>逆向物流报价</span>
</button>
</nav>
<div class="quote-local-panel" id="quote-courier-panel" role="tabpanel" data-quote-panel="courier" @if ($quoteView !== 'courier') hidden @endif>
<div class="quote-summary" aria-label="报价数据概览">
<div class="quote-source">
<span class="quote-source__icon" aria-hidden="true">
<span class="material-icons-outlined">description</span>
</span>
<div class="quote-source__body">
<div class="quote-source__file" title="{{ $quoteSnapshot['source_file'] }}">{{ $quoteSnapshot['source_file'] }}</div>
<div class="quote-source__state">本机私有报价源 · 原始工作簿未修改</div>
</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">报价版本</div>
<div class="quote-metric__value" data-summary="quotes">{{ $quoteSnapshot['summary']['quote_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">快递品牌</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['carrier_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">已标执行日期</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['dated_count'] }}</div>
</div>
<div class="quote-metric">
<div class="quote-metric__label">原表公式单元格</div>
<div class="quote-metric__value">{{ $quoteSnapshot['summary']['formula_count'] }}</div>
</div>
</div>
<div class="quote-workspace">
<div class="quote-toolbar" aria-label="报价筛选">
<label class="quote-field">
<span class="quote-field__label">报价名称</span>
<input class="quote-control" type="search" name="keyword" placeholder="搜索快递公司或版本号" autocomplete="off" data-quote-filter="keyword">
</label>
<label class="quote-field">
<span class="quote-field__label">快递公司</span>
<select class="quote-control" name="carrier" data-quote-filter="carrier">
<option value="">全部公司</option>
@foreach ($quoteCompanies as $companyName)
<option value="{{ $companyName }}">{{ $companyName }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">报价类型</span>
<select class="quote-control" name="type" data-quote-filter="type">
<option value="">全部类型</option>
@foreach ($quoteTypes as $type)
<option value="{{ $type }}">{{ $type }}</option>
@endforeach
</select>
</label>
<label class="quote-field">
<span class="quote-field__label">资料状态</span>
<select class="quote-control" name="status" data-quote-filter="status">
<option value="">全部状态</option>
<option value="已标执行日期">已标执行日期</option>
<option value="未标执行日期">未标执行日期</option>
</select>
</label>
<div class="quote-toolbar__actions">
<a class="quote-button" href="{{ route('business.courier-stock-quotes.create') }}">
<span class="material-icons-outlined" aria-hidden="true">upload_file</span><span>录入存量报价</span>
</a>
<button class="quote-icon-button" type="button" title="重置筛选" aria-label="重置筛选" data-quote-reset>
<span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span>
</button>
@if ($quoteSnapshot['available'])
<a class="quote-button" href="{{ route('business.courier-quote-signatures.export') }}" title="把当前筛选结果里已签署的回签单 PDF 和签署完成证书打包下载" data-quote-export>
<span class="material-icons-outlined" aria-hidden="true">download</span>
<span data-quote-export-label>导出回签单</span>
</a>
@endif
<a class="quote-button" href="{{ route('business.courier-quote-changes.index') }}" title="查看报价变更草稿、审核状态和新版本">
<span class="material-icons-outlined" aria-hidden="true">history</span>
<span>报价历史</span>
</a>
</div>
</div>
@if ($quoteSnapshot['available'])
<div class="quote-results">
<span class="quote-results__count" data-quote-result-count>共 {{ count($quotes) }} 个报价版本</span>
<span class="quote-results__notice">
<span class="material-icons-outlined" aria-hidden="true">verified_user</span>
<span>原始报价规则保留,当前页面不重新计算金额</span>
</span>
</div>
<div class="quote-table-wrap">
<table class="quote-table">
<thead>
<tr>
<th scope="col" data-finance-column-key="报价版本_0">报价版本</th>
<th scope="col" data-finance-column-key="快递公司_1">快递公司</th>
<th scope="col" data-finance-column-key="报价类型_2">报价类型</th>
<th scope="col" data-finance-column-key="原始范围_3">原始范围</th>
<th scope="col" data-finance-column-key="数据规模_4">数据规模</th>
<th scope="col" data-finance-column-key="执行日期_5">执行日期</th>
<th scope="col" data-finance-column-key="签章_6">签章</th>
<th scope="col" data-finance-column-key="资料状态_7">资料状态</th>
<th scope="col" data-finance-column-key="操作_8">操作</th>
</tr>
</thead>
<tbody data-quote-table-body></tbody>
</table>
</div>
<div class="quote-empty" data-quote-filter-empty hidden>
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">filter_alt_off</span>
<div class="quote-empty__title">没有符合当前条件的报价</div>
<div class="quote-empty__text">重置筛选后可查看全部已导入版本。</div>
</div>
</div>
<div class="quote-pagination" data-quote-pagination>
<span class="quote-pagination__label" data-quote-page-label></span>
<span class="quote-pagination__buttons">
<button class="quote-button" type="button" data-quote-page="previous">
<span class="material-icons-outlined" aria-hidden="true">chevron_left</span>
<span>上一页</span>
</button>
<button class="quote-button" type="button" data-quote-page="next">
<span>下一页</span>
<span class="material-icons-outlined" aria-hidden="true">chevron_right</span>
</button>
</span>
</div>
@else
<div class="quote-empty" role="status">
<div class="quote-empty__body">
<span class="material-icons-outlined" aria-hidden="true">file_present</span>
<div class="quote-empty__title">{{ $quoteSnapshot['message'] }}</div>
<div class="quote-empty__text">报价页面仍可访问,重新导入后会显示工作表列表。</div>
</div>
</div>
@endif
</div>
@if ($quoteSnapshot['available'])
<div class="quote-modal" role="dialog" aria-modal="true" aria-labelledby="quote-detail-title" data-quote-modal>
<div class="quote-modal__panel">
<header class="quote-modal__header">
<div class="quote-modal__title-wrap">
<div class="quote-modal__title" id="quote-detail-title" data-quote-detail="title"></div>
<div class="quote-modal__subtitle">原工作表内容 · 只读</div>
</div>
<div data-stock-original-links style="display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-left:auto"></div>
<button class="quote-modal__close" type="button" aria-label="关闭报价明细" title="关闭" data-quote-close>
<span class="material-icons-outlined" aria-hidden="true">close</span>
</button>
</header>
<div style="padding:10px 20px;border-bottom:1px solid var(--quote-border)"><label>报价时期 <select data-quote-version-picker class="quote-control" style="max-width:100%"></select></label></div>
<dl class="quote-detail-meta">
<div><dt>版本号</dt><dd data-quote-detail="id"></dd></div>
<div><dt>快递公司</dt><dd data-quote-detail="carrier"></dd></div>
<div><dt>报价类型</dt><dd data-quote-detail="type"></dd></div>
<div><dt>原始范围</dt><dd data-quote-detail="range"></dd></div>
<div><dt>执行日期</dt><dd data-quote-detail="date"></dd></div>
<div><dt>公式单元格</dt><dd data-quote-detail="formulas"></dd></div>
<div><dt>系统计费规则</dt><dd data-quote-detail="billing"></dd></div>
</dl>
<div class="quote-signed" data-quote-signed hidden>
<span class="material-icons-outlined" aria-hidden="true">task_alt</span>
<span class="quote-signed__text" data-quote-signed-text>已签署回传</span>
<a class="quote-button" data-quote-signed-link="signed"><span class="material-icons-outlined" aria-hidden="true">picture_as_pdf</span><span>回签单 PDF</span></a>
<a class="quote-button" data-quote-signed-link="certificate"><span class="material-icons-outlined" aria-hidden="true">verified</span><span>签署完成证书</span></a>
<a class="quote-button" data-quote-signed-link="export" title="导出报价表(xlsx,WPS / Excel 可打开),只含省份价格"><span class="material-icons-outlined" aria-hidden="true">table_view</span><span>导出电子档</span></a>
</div>
<div class="quote-detail-table-wrap">
<table class="quote-detail-table" data-quote-detail-table></table>
</div>
</div>
</div>
<dialog data-period-dialog style="max-width:480px;width:calc(100% - 32px);padding:20px;border:1px solid #D8D2C7;border-radius:12px;color:#171717;background:#fff">
<form method="POST" data-period-form>@csrf
<h3 style="font-size:14px;margin:0 0 12px">设置存量报价截止日期</h3>
<p data-period-title></p>
<label style="display:grid;gap:6px;margin-bottom:12px">截止日期(当天仍适用)<input class="quote-control" type="date" name="end_date" required></label>
<label style="display:grid;gap:6px;margin-bottom:12px">调整依据<textarea class="quote-control" name="reason" minlength="2" maxlength="1000" required></textarea></label>
<p style="font-size:12px;color:#595959">仅更新执行期间并保留操作记录,不修改报价原文或签署原件。期间不得与其他已录入报价重叠。</p>
<div style="display:flex;gap:8px;justify-content:flex-end"><button type="button" class="quote-button" data-period-cancel>取消</button><button class="quote-button" type="submit">保存截止日期</button></div>
</form>
</dialog>
<script>
(function () {
var root = document.querySelector('[data-quote-management]');
if (!root || root.dataset.initialized === 'true') {
return;
}
root.dataset.initialized = 'true';
var quotes = @json($quotes, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
var pageSize = 12;
var currentPage = 1;
var filteredQuotes = quotes.slice();
var body = root.querySelector('[data-quote-table-body]');
var empty = root.querySelector('[data-quote-filter-empty]');
var tableWrap = root.querySelector('.quote-table-wrap');
var pagination = root.querySelector('[data-quote-pagination]');
var modal = root.querySelector('[data-quote-modal]');
var exportBase = @json(route('business.courier-quote-signatures.export'));
var exportAnchor = root.querySelector('[data-quote-export]');
if (exportAnchor) {
exportAnchor.addEventListener('click', function (event) {
if (exportAnchor.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
}
});
}
var lastFocusedElement = null;
var expandedGroups = new Set();
var asOf = @json(today()->toDateString());
var dateLabel = create('label', 'quote-filter-label', '查看日期 ');
var dateInput = create('input', 'quote-control');
dateInput.type = 'date'; dateInput.value = asOf;
dateInput.setAttribute('aria-label', '按日期查看报价版本');
dateLabel.appendChild(dateInput);
root.querySelector('[data-quote-result-count]').before(dateLabel);
dateInput.addEventListener('change', function () { if (dateInput.value) { asOf = dateInput.value; filterQuotes(true); } });
function groupKey(quote) { return quote.group_key || ('sheet:' + quote.id); }
function versionsFor(quote) { return quotes.filter(function (v) { return groupKey(v) === groupKey(quote); }).sort(function (a,b) { return (b.effective_date || '').localeCompare(a.effective_date || '') || b.id.localeCompare(a.id); }); }
function effectiveIn(versions) { return versions.filter(function (v) { return v.eligible && v.effective_date && v.effective_date <= asOf && (!v.end_date || v.end_date >= asOf); }).sort(function(a,b){return b.effective_date.localeCompare(a.effective_date) || b.id.localeCompare(a.id);})[0]; }
function atDate(quote) {
if (!quote.eligible) return quote;
var copy = Object.assign({}, quote);
if (asOf !== @json(today()->toDateString())) { delete copy.status_note; delete copy.superseded_by; }
var current = effectiveIn(versionsFor(quote));
if (quote.effective_date > asOf) copy.status = '待生效';
else if (quote.end_date && quote.end_date < asOf) copy.status = '已到期';
else if (current && current.id !== quote.id) copy.status = current.end_date && !current.is_stock ? '暂停中' : '已失效';
else copy.status = quote.end_date && !quote.is_stock ? '生效中(临时)' : '生效中';
return copy;
}
var fields = {
keyword: root.querySelector('[data-quote-filter="keyword"]'),
carrier: root.querySelector('[data-quote-filter="carrier"]'),
type: root.querySelector('[data-quote-filter="type"]'),
status: root.querySelector('[data-quote-filter="status"]')
};
['生效中','待生效','已到期','已失效','已作废','暂停中','生效中(临时)'].forEach(function(status){var option=create('option','',status);option.value=status;fields.status.appendChild(option);});
function text(value, fallback) {
if (value === null || value === undefined || value === '') {
return fallback || '';
}
return String(value);
}
function cellText(value) {
if (typeof value === 'boolean') {
return value ? 'TRUE' : 'FALSE';
}
return text(value, '');
}
function create(tag, className, value) {
var element = document.createElement(tag);
if (className) {
element.className = className;
}
if (value !== undefined) {
element.textContent = value;
}
return element;
}
function updateUrl() {
var url = new URL(window.location.href);
url.searchParams.set('quote_date', asOf);
Object.keys(fields).forEach(function (key) {
var value = fields[key].value.trim();
if (value) {
url.searchParams.set(key, value);
} else {
url.searchParams.delete(key);
}
});
if (currentPage > 1) {
url.searchParams.set('quote_page', String(currentPage));
} else {
url.searchParams.delete('quote_page');
}
window.history.replaceState({}, '', url);
}
function restoreUrlFilters() {
var params = new URL(window.location.href).searchParams;
var requestedDate=params.get('quote_date');
if (/^\d{4}-\d{2}-\d{2}$/.test(requestedDate || '')) { dateInput.value=requestedDate;if(dateInput.value)asOf=dateInput.value; }
Object.keys(fields).forEach(function (key) {
var value = params.get(key);
var field = fields[key];
if (value !== null && Array.from(field.options || []).some(function (option) { return option.value === value; })) {
field.value = value;
} else if (key === 'keyword' && value !== null) {
field.value = value;
}
});
currentPage = Math.max(parseInt(params.get('quote_page') || '1', 10) || 1, 1);
}
function appendCell(row, value, className) {
var cell = create('td', className || '', value);
row.appendChild(cell);
return cell;
}
function renderRow(quote) {
var row = create('tr');
var nameCell = appendCell(row, '');
nameCell.appendChild(create('span', 'quote-table__name', quote.id));
appendCell(row, quote.company_name + (quote.courier_expcode ? ' · ' + quote.courier_expcode : ''));
appendCell(row, quote.quote_type);
appendCell(row, text(quote.source_range, '-'));
appendCell(row, quote.row_count + ' 行 × ' + quote.column_count + ' 列');
appendCell(row, text(quote.period_label || quote.effective_date, '-'));
var signCell = appendCell(row, quote.signature_status, 'quote-table__secondary');
if (quote.signed_files && quote.signed_files.signed) {
signCell.appendChild(document.createElement('br'));
var signedLink = create('a', 'quote-action', quote.signature_status === '历史签署资料已归档' ? '历史签署资料' : '回签单');
signedLink.href = quote.signed_files.signed;
signedLink.title = '下载 ' + quote.id + ' 签署资料' + (quote.signed_at ? '(' + quote.signed_at + ' 签署)' : '');
signedLink.setAttribute('aria-label', '下载回签单 ' + quote.id);
signCell.appendChild(signedLink);
}
var statusCell = appendCell(row, '');
statusCell.appendChild(create(
'span',
'quote-badge ' + (['已失效', '已作废', '已到期', '暂停中'].indexOf(quote.status) !== -1 ? 'quote-badge--muted' : (quote.effective_date ? 'quote-badge--ready' : 'quote-badge--attention')),
quote.status
));
if (quote.void_reason) {
statusCell.appendChild(create('div', 'quote-table__secondary', '作废:' + quote.void_reason));
}
if (quote.superseded_by) {
statusCell.appendChild(create('div', 'quote-table__secondary', '被 ' + quote.superseded_by + ' 替代'));
}
if (quote.status_note) {
statusCell.appendChild(create('div', 'quote-table__secondary', quote.status_note));
}
var actionCell = appendCell(row, '');
var action = create('button', 'quote-action', '查看');
action.type = 'button';
action.dataset.quoteOpen = quote.id;
action.setAttribute('aria-label', '查看 ' + quote.sheet_name);
actionCell.appendChild(action);
if (quote.period_end_url) {
var periodButton = create('button', 'quote-action', '设置截止日期');
periodButton.type = 'button'; periodButton.dataset.periodEnd = quote.id;
actionCell.appendChild(periodButton);
}
var changeAction = create('a', 'quote-action', '变价');
changeAction.href = quote.requote_url || (@json(route('business.courier-quote-changes.start')) + '?source_quote_id=' + encodeURIComponent(quote.id));
changeAction.title = '发起 ' + quote.sheet_name + ' 的变更草稿';
changeAction.setAttribute('aria-label', '变价 ' + quote.sheet_name);
if (quote.status !== '已作废') {
actionCell.appendChild(changeAction);
}
// 动态重建行时按字段键对应当前表头,保留用户列顺序与隐藏设置。
var columnKeys = ['报价版本_0', '快递公司_1', '报价类型_2', '原始范围_3', '数据规模_4', '执行日期_5', '签章_6', '资料状态_7', '操作_8'];
var cellsByKey = {};
Array.from(row.children).forEach(function (cell, index) {
cell.dataset.financeColumnKey = columnKeys[index];
cellsByKey[columnKeys[index]] = cell;
});
body.closest('table').querySelectorAll('thead th[data-finance-column-key]').forEach(function (header) {
var cell = cellsByKey[header.dataset.financeColumnKey];
if (!cell) return;
cell.classList.toggle('is-column-hidden', header.classList.contains('is-column-hidden'));
row.appendChild(cell);
});
return row;
}
function render() {
var groups = [];
filteredQuotes.forEach(function (quote) { var group = groups.find(function (g) { return g.key === groupKey(quote); }); if (!group) { group = {key:groupKey(quote), versions:[]};groups.push(group); } group.versions.push(quote); });
var pageCount = Math.max(Math.ceil(groups.length / pageSize), 1);
currentPage = Math.min(currentPage, pageCount);
var start = (currentPage - 1) * pageSize;
var visibleGroups = groups.slice(start, start + pageSize);
body.replaceChildren();
visibleGroups.forEach(function (group) {
group.versions.sort(function(a,b){return (b.effective_date || '').localeCompare(a.effective_date || '') || b.id.localeCompare(a.id);});
group.versions.forEach(function (version) { body.appendChild(renderRow(version)); });
});
root.querySelector('[data-quote-result-count]').textContent = '共 ' + groups.length + ' 组快递报价 · ' + filteredQuotes.length + ' 个版本';
var exportLink = root.querySelector('[data-quote-export]');
if (exportLink) {
var signedCodes = filteredQuotes.filter(function (quote) { return quote.signed_files && quote.signed_files.export; })
.map(function (quote) { return quote.id; });
exportLink.href = exportBase + (signedCodes.length ? '?codes=' + encodeURIComponent(signedCodes.join(',')) : '');
exportLink.setAttribute('aria-disabled', signedCodes.length ? 'false' : 'true');
exportLink.style.opacity = signedCodes.length ? '' : '0.48';
exportLink.querySelector('[data-quote-export-label]').textContent = '导出回签单' + (signedCodes.length ? '(' + signedCodes.length + ')' : '');
}
root.querySelector('[data-quote-page-label]').textContent = filteredQuotes.length
? '共 ' + groups.length + ' 组 · 共 ' + pageCount + ' 页 · 当前第 ' + currentPage + ' 页'
: '共 0 条 · 共 0 页';
root.querySelector('[data-quote-page="previous"]').disabled = currentPage <= 1;
root.querySelector('[data-quote-page="next"]').disabled = currentPage >= pageCount;
tableWrap.hidden = filteredQuotes.length === 0;
empty.hidden = filteredQuotes.length !== 0;
pagination.hidden = false;
updateUrl();
}
function filterQuotes(resetPage) {
var keyword = fields.keyword.value.trim().toLocaleLowerCase('zh-CN');
filteredQuotes = quotes.map(atDate).filter(function (quote) {
var matchesKeyword = !keyword || (quote.company_name + ' ' + quote.sheet_name + ' ' + quote.id + ' ' + quote.carrier + ' ' + (quote.courier_expcode || ''))
.toLocaleLowerCase('zh-CN')
.includes(keyword);
return matchesKeyword
&& (!fields.carrier.value || quote.company_name === fields.carrier.value)
&& (!fields.type.value || quote.quote_type === fields.type.value)
&& (!fields.status.value || quote.status === fields.status.value);
});
if (resetPage) {
currentPage = 1;
}
render();
}
function setDetail(name, value) {
root.querySelector('[data-quote-detail="' + name + '"]').textContent = value;
}
function excelColumn(index) {
var label = '';
var value = index + 1;
while (value > 0) {
value--;
label = String.fromCharCode(65 + (value % 26)) + label;
value = Math.floor(value / 26);
}
return label;
}
function openDetail(quote, trigger) {
if (trigger) lastFocusedElement = trigger;
var picker = root.querySelector('[data-quote-version-picker]');
picker.replaceChildren();
versionsFor(quote).forEach(function (version) {var option=create('option','',version.id+' · '+(version.effective_date||'未标日期')+' 至 '+(version.end_date||'长期有效'));option.value=version.id;option.selected=version.id===quote.id;picker.appendChild(option);});
setDetail('title', quote.sheet_name);
setDetail('id', quote.id);
setDetail('carrier', quote.company_name + (quote.courier_expcode ? ' · ' + quote.courier_expcode : ''));
setDetail('type', quote.quote_type);
setDetail('range', text(quote.source_range, '-'));
setDetail('date', text(quote.period_label || quote.effective_date, 'Excel 未标注'));
setDetail('formulas', String(quote.formula_count));
setDetail('billing', text(quote.billing_summary, '未设置(运费需人工核算)'));
var originals = root.querySelector('[data-stock-original-links]');
originals.replaceChildren();
(quote.stock_originals || []).forEach(function (file, index, files) {
var link = create('a', 'quote-button', (file.previewable ? '预览签署原件' : '下载签署原件') + (files.length > 1 ? ' ' + (index + 1) : ''));
link.href = file.url;
link.title = file.name;
link.target = '_blank';
link.rel = 'noopener noreferrer';
originals.appendChild(link);
});
var signedBox = root.querySelector('[data-quote-signed]');
var files = quote.signed_files || {};
signedBox.hidden = !files.signed;
root.querySelector('[data-quote-signed-text]').textContent = '已签署回传' + (quote.signed_at ? ' · ' + quote.signed_at : '');
signedBox.querySelectorAll('[data-quote-signed-link]').forEach(function (link) {
var url = files[link.getAttribute('data-quote-signed-link')];
link.hidden = !url;
if (url) { link.href = url; } else { link.removeAttribute('href'); }
});
var table = root.querySelector('[data-quote-detail-table]');
var head = create('thead');
var headerRow = create('tr');
headerRow.appendChild(create('th', 'quote-detail-table__row-number', '序号'));
for (var column = 0; column < quote.column_count; column++) {
headerRow.appendChild(create('th', '', excelColumn(column)));
}
head.appendChild(headerRow);
var tableBody = create('tbody');
quote.values.forEach(function (values, rowIndex) {
var row = create('tr');
row.appendChild(create('th', 'quote-detail-table__row-number', String(rowIndex + 1)));
for (var columnIndex = 0; columnIndex < quote.column_count; columnIndex++) {
var cell = create('td', '', cellText(values[columnIndex]));
var formula = quote.formulas[rowIndex] && quote.formulas[rowIndex][columnIndex];
if (formula) {
cell.appendChild(create('span', 'quote-detail-table__formula', '=' + String(formula).replace(/^=/, '')));
}
row.appendChild(cell);
}
tableBody.appendChild(row);
});
table.replaceChildren(head, tableBody);
modal.classList.add('is-open');
document.body.style.overflow = 'hidden';
root.querySelector('[data-quote-close]').focus();
}
function closeDetail() {
modal.classList.remove('is-open');
document.body.style.overflow = '';
if (lastFocusedElement) {
lastFocusedElement.focus();
}
}
Object.keys(fields).forEach(function (key) {
fields[key].addEventListener(key === 'keyword' ? 'input' : 'change', function () {
filterQuotes(true);
});
});
root.querySelector('[data-quote-reset]').addEventListener('click', function () {
Object.keys(fields).forEach(function (key) { fields[key].value = ''; });
filterQuotes(true);
fields.keyword.focus();
});
root.querySelector('[data-quote-page="previous"]').addEventListener('click', function () {
currentPage = Math.max(currentPage - 1, 1);
render();
});
root.querySelector('[data-quote-page="next"]').addEventListener('click', function () {
currentPage++;
render();
});
body.addEventListener('click', function (event) {
var period=event.target.closest('[data-period-end]');
if(period){var item=quotes.find(function(q){return q.id===period.dataset.periodEnd;});var form=root.querySelector('[data-period-form]');form.action=item.period_end_url;form.elements.end_date.value=item.end_date||'';form.elements.end_date.min=item.effective_date;form.elements.reason.value='';root.querySelector('[data-period-title]').textContent=item.company_name+' · '+item.id+' · '+item.effective_date+' 起';root.querySelector('[data-period-dialog]').showModal();return;}
var trigger = event.target.closest('[data-quote-open]');
if (!trigger) {
return;
}
var quote = quotes.find(function (item) { return item.id === trigger.dataset.quoteOpen; });
if (quote) {
openDetail(quote, trigger);
}
});
root.querySelector('[data-quote-close]').addEventListener('click', closeDetail);
root.querySelector('[data-quote-version-picker]').addEventListener('change',function(event){var quote=quotes.find(function(q){return q.id===event.target.value;});if(quote)openDetail(quote);});
root.querySelector('[data-period-cancel]').addEventListener('click',function(){root.querySelector('[data-period-dialog]').close();});
modal.addEventListener('click', function (event) {
if (event.target === modal) {
closeDetail();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('is-open')) {
closeDetail();
}
});
restoreUrlFilters();
filterQuotes(false);
}());
</script>
@endif
</div>
<div class="quote-local-panel" id="quote-warehouse-panel" role="tabpanel" data-quote-panel="warehouse" @if ($quoteView !== 'warehouse') hidden @endif>
<livewire:business.third-party-warehouse-quotes />
</div>
<div class="quote-local-panel" id="quote-reverse-logistics-panel" role="tabpanel" data-quote-panel="reverse-logistics" @if ($quoteView !== 'reverse-logistics') hidden @endif>
@include('business.pages.courier-payables.reverse-logistics-quotes')
</div>
<script>
(() => {
const root = document.currentScript.closest('[data-quote-management]');
if (!root || root.dataset.localTabsReady === 'true') return;
root.dataset.localTabsReady = 'true';
const tabs = [...root.querySelectorAll('[data-quote-view]')];
const panels = [...root.querySelectorAll('[data-quote-panel]')];
const activate = (view, updateUrl = true) => {
tabs.forEach((tab) => {
const active = tab.dataset.quoteView === view;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
panels.forEach((panel) => { panel.hidden = panel.dataset.quotePanel !== view; });
if (updateUrl) {
const url = new URL(window.location.href);
if (view === 'courier') url.searchParams.delete('quote_view');
else url.searchParams.set('quote_view', view);
history.replaceState({}, '', url);
}
};
tabs.forEach((tab) => tab.addEventListener('click', () => activate(tab.dataset.quoteView)));
})();
</script>
</section>