货款对账:差异支持勾选多条推送 (#379)
This commit is contained in:
parent
41f6946a2b
commit
3638a8cee9
@ -130,10 +130,12 @@ class SupplierReconciliation extends Controller
|
||||
'supplier' => ['required', 'string', 'max:191'],
|
||||
'month' => ['nullable', 'string', 'regex:/^\d{4}-\d{2}$/'],
|
||||
'reconciler' => ['nullable', 'string', 'max:191'],
|
||||
'scope' => ['required', 'in:all,row'],
|
||||
'scope' => ['required', 'in:all,row,selected'],
|
||||
'barcode' => ['nullable', 'string', 'max:191'],
|
||||
'difference_type' => ['nullable', 'string', 'max:32'],
|
||||
'unit_price' => ['nullable', 'numeric'],
|
||||
'selected_rows' => ['nullable', 'array', 'max:600'],
|
||||
'selected_rows.*' => ['string', 'max:2048'],
|
||||
]);
|
||||
[$supplier, $companyId] = $this->returnTarget($request);
|
||||
$detail = $this->pageData->financeDetailForFilters($supplier, $companyId, $request->all());
|
||||
@ -152,6 +154,18 @@ class SupplierReconciliation extends Controller
|
||||
&& (string) ($row['type'] ?? '') === $type
|
||||
&& abs((float) ($row['unitPrice'] ?? 0) - $unitPrice) < 0.005,
|
||||
));
|
||||
} elseif ($request->input('scope') === 'selected') {
|
||||
$selected = [];
|
||||
foreach ((array) $request->input('selected_rows', []) as $token) {
|
||||
$row = json_decode((string) $token, true);
|
||||
if (is_array($row)) {
|
||||
$selected[$this->differenceRowKey($row)] = true;
|
||||
}
|
||||
}
|
||||
$differences = array_values(array_filter(
|
||||
$differences,
|
||||
fn (array $row): bool => isset($selected[$this->differenceRowKey($row)]),
|
||||
));
|
||||
}
|
||||
|
||||
if ($differences === []) {
|
||||
@ -196,4 +210,14 @@ class SupplierReconciliation extends Controller
|
||||
return [trim((string) $request->input('supplier')), (int) (company_id() ?: 1)];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $row */
|
||||
private function differenceRowKey(array $row): string
|
||||
{
|
||||
return implode("\0", [
|
||||
trim((string) ($row['barcode'] ?? '')),
|
||||
trim((string) ($row['type'] ?? '')),
|
||||
number_format((float) ($row['unitPrice'] ?? 0), 4, '.', ''),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@
|
||||
.supplier-reconciliation-page .sr-robot-config summary { cursor: pointer; color: #5B5343; font-size: .8125rem; font-weight: 600; }
|
||||
.supplier-reconciliation-page .sr-robot-form { display: grid; grid-template-columns: minmax(260px, 2fr) minmax(180px, 1fr) auto; gap: 10px; align-items: end; margin-top: 12px; }
|
||||
.supplier-reconciliation-page .sr-robot-check { display: flex; align-items: center; gap: 6px; min-height: 36px; color: var(--vip-content-muted, #595959); font-size: .75rem; }
|
||||
.supplier-reconciliation-page .sr-select-cell { width: 34px; text-align: center; }
|
||||
.supplier-reconciliation-page .sr-select-cell input { width: 15px; height: 15px; accent-color: var(--vip-content-accent-dark, #856632); }
|
||||
.supplier-reconciliation-page .sr-robot-form input[type="url"],
|
||||
.supplier-reconciliation-page .sr-robot-form input[type="password"] { width: 100%; min-width: 0; }
|
||||
@media (max-width: 900px) {
|
||||
@ -138,6 +140,22 @@
|
||||
推送全部差异({{ $differenceCount }} 条)
|
||||
</button>
|
||||
</form>
|
||||
<form id="push-selected-form"
|
||||
method="POST"
|
||||
action="{{ route('business.supplier-reconciliation.push-differences') }}">
|
||||
@csrf
|
||||
<input type="hidden" name="supplier" value="{{ $selectedSupplier }}">
|
||||
<input type="hidden" name="month" value="{{ $month }}">
|
||||
<input type="hidden" name="reconciler" value="{{ $reconciler }}">
|
||||
<input type="hidden" name="scope" value="selected">
|
||||
<button id="push-selected-button"
|
||||
type="submit"
|
||||
class="sr-button is-primary is-disabled"
|
||||
disabled
|
||||
title="先在表格中勾选需要推送的差异">
|
||||
推送选中(<span id="push-selected-count">0</span>)
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
</div>
|
||||
<details class="sr-robot-config" @if (! $robot['configured']) open @endif>
|
||||
@ -231,6 +249,9 @@
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" class="sr-select-cell">
|
||||
<input id="select-all-differences" type="checkbox" aria-label="选择全部差异">
|
||||
</th>
|
||||
<th scope="col">比对结果</th>
|
||||
<th scope="col">条码</th>
|
||||
<th scope="col">单价</th>
|
||||
@ -254,6 +275,18 @@
|
||||
};
|
||||
@endphp
|
||||
<tr class="{{ $tagClass }}">
|
||||
<td class="sr-select-cell">
|
||||
@if ($row['type'] !== '一致')
|
||||
<input class="js-difference-checkbox"
|
||||
form="push-selected-form"
|
||||
type="checkbox"
|
||||
name="selected_rows[]"
|
||||
value="{{ json_encode(['barcode' => $row['barcode'], 'type' => $row['type'], 'unitPrice' => $row['unitPrice']], JSON_UNESCAPED_UNICODE) }}"
|
||||
aria-label="选择差异 {{ $row['barcode'] }}">
|
||||
@else
|
||||
<span class="sr-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
<td><span class="sr-tag {{ $tagClass }}">{{ $row['type'] }}</span></td>
|
||||
<td>
|
||||
{{ $row['barcode'] }}
|
||||
@ -294,7 +327,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="10"><div class="sr-empty">该厂家还没有上传对账单,或我方还没有这家供应商的结算数据。</div></td></tr>
|
||||
<tr><td colspan="11"><div class="sr-empty">该厂家还没有上传对账单,或我方还没有这家供应商的结算数据。</div></td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
@ -302,3 +335,45 @@
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($detail && $differenceCount > 0)
|
||||
<script>
|
||||
(() => {
|
||||
const form = document.getElementById('push-selected-form');
|
||||
const button = document.getElementById('push-selected-button');
|
||||
const countLabel = document.getElementById('push-selected-count');
|
||||
const selectAll = document.getElementById('select-all-differences');
|
||||
const checkboxes = Array.from(document.querySelectorAll('.js-difference-checkbox'));
|
||||
const robotConfigured = @json($robot['configured']);
|
||||
|
||||
if (!form || !button || !countLabel || !selectAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateSelection = () => {
|
||||
const selected = checkboxes.filter((checkbox) => checkbox.checked).length;
|
||||
countLabel.textContent = String(selected);
|
||||
button.disabled = !robotConfigured || selected === 0;
|
||||
button.classList.toggle('is-disabled', button.disabled);
|
||||
selectAll.checked = selected > 0 && selected === checkboxes.length;
|
||||
selectAll.indeterminate = selected > 0 && selected < checkboxes.length;
|
||||
};
|
||||
|
||||
checkboxes.forEach((checkbox) => checkbox.addEventListener('change', updateSelection));
|
||||
selectAll.addEventListener('change', () => {
|
||||
checkboxes.forEach((checkbox) => {
|
||||
checkbox.checked = selectAll.checked;
|
||||
});
|
||||
updateSelection();
|
||||
});
|
||||
form.addEventListener('submit', (event) => {
|
||||
const selected = checkboxes.filter((checkbox) => checkbox.checked).length;
|
||||
if (selected === 0 || !window.confirm(`确认将这 ${selected} 条差异推送到该厂家钉钉群?`)) {
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
updateSelection();
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
@ -29,6 +29,8 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase
|
||||
$this->loginAs()->get($this->pageUrl())
|
||||
->assertOk()
|
||||
->assertSeeText('推送全部差异(3 条)')
|
||||
->assertSeeText('推送选中(')
|
||||
->assertSee('aria-label="选择全部差异"', false)
|
||||
->assertSeeText('推送本条')
|
||||
->assertSeeText('厂家钉钉群机器人')
|
||||
->assertSeeText('未配置,配置后才能推送差异')
|
||||
@ -140,6 +142,35 @@ class SupplierReconciliationDingTalkTest extends FeatureTestCase
|
||||
});
|
||||
}
|
||||
|
||||
public function test_push_selected_differences_only_sends_checked_rows(): void
|
||||
{
|
||||
$this->seedReconciliation();
|
||||
$this->saveRobot();
|
||||
Http::fake(['*' => Http::response(['errcode' => 0, 'errmsg' => 'ok'])]);
|
||||
|
||||
$this->loginAs()
|
||||
->post(route('business.supplier-reconciliation.push-differences'), [
|
||||
'supplier' => '晨丰',
|
||||
'month' => '2026-09',
|
||||
'reconciler' => '张三',
|
||||
'scope' => 'selected',
|
||||
'selected_rows' => [
|
||||
json_encode(['barcode' => 'B-1', 'type' => '金额不一致', 'unitPrice' => 50]),
|
||||
json_encode(['barcode' => 'D-1', 'type' => '厂家多', 'unitPrice' => 80]),
|
||||
],
|
||||
])
|
||||
->assertStatus(302)
|
||||
->assertSessionHas('success', '已推送 2 条差异到 晨丰 钉钉群(1 条消息)。');
|
||||
|
||||
Http::assertSent(function ($request): bool {
|
||||
$text = (string) ($request['markdown']['text'] ?? '');
|
||||
|
||||
return str_contains($text, 'B-1')
|
||||
&& str_contains($text, 'D-1')
|
||||
&& ! str_contains($text, '`C-1`');
|
||||
});
|
||||
}
|
||||
|
||||
public function test_push_without_robot_configuration_is_rejected_without_http_call(): void
|
||||
{
|
||||
$this->seedReconciliation();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user