feat: expand installation detail items
This commit is contained in:
parent
fcc36551a6
commit
7cff0d6940
@ -163,6 +163,16 @@ class Pages extends Controller
|
||||
));
|
||||
}
|
||||
|
||||
public function installationDetails(string $guid)
|
||||
{
|
||||
$payload = $this->installationDetails->details($guid);
|
||||
if (! $payload['ready']) {
|
||||
return response()->json(['message' => '安装明细镜像尚未完成首次同步'], 503);
|
||||
}
|
||||
|
||||
return response()->json(['data' => $payload['data']]);
|
||||
}
|
||||
|
||||
protected function viewData(
|
||||
array $section,
|
||||
?array $page = null,
|
||||
|
||||
@ -29,17 +29,46 @@ class InstallationDetailMirrorRepository
|
||||
}
|
||||
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
// 主视图按商品行展开,同一安装单会出现多行;主表只保留每个
|
||||
// SourceGuid 的最新 Id,商品行通过 details() 单独展开。
|
||||
$latest = $connection->table($dataset->targetTable())
|
||||
->selectRaw('MAX(Id) AS id')
|
||||
->groupBy('SourceGuid');
|
||||
$query = $connection->table($dataset->targetTable() . ' AS v')
|
||||
->leftJoin($items->targetTable() . ' AS i', 'i.guid', '=', 'v.SourceGuid')
|
||||
->select(['v.*', 'i.Id AS detail_id', 'i.guid AS detail_guid', 'i.shopproid', 'i.Spu', 'i.shopskucode',
|
||||
'i.proCode', 'i.barCode', 'i.skuName', 'i.scmname', 'i.applyItemAmount', 'i.installcount',
|
||||
'i.settleItemAmount', 'i.acceptItemAmount', 'i.remark', 'i.createTime AS item_created_at', 'i.modifyTime AS item_modified_at']);
|
||||
->joinSub($latest, 'latest', static fn ($join) => $join->on('latest.id', '=', 'v.Id'))
|
||||
->select('v.*');
|
||||
$total = (clone $query)->count();
|
||||
$rows = $query->orderByDesc('v.Id')->orderByDesc('i.Id')->forPage(max($page, 1), $perPage)->get();
|
||||
$rows = $query->orderByDesc('v.Id')->forPage(max($page, 1), $perPage)->get();
|
||||
|
||||
return ['data' => $rows->map(fn (object $row): array => $this->map((array) $row))->all(), 'total' => $total, 'ready' => true];
|
||||
}
|
||||
|
||||
/** @return array{data:list<array<string,mixed>>,ready:bool} */
|
||||
public function details(string $guid): array
|
||||
{
|
||||
$dataset = $this->datasets->get(self::DATASET);
|
||||
$items = $this->datasets->get(self::ITEM_DATASET);
|
||||
$connection = $this->mirror->connection();
|
||||
if (! $this->ready($dataset, $items, $connection->getName())) {
|
||||
return ['data' => [], 'ready' => false];
|
||||
}
|
||||
|
||||
$mainExists = $connection->table($dataset->targetTable())
|
||||
->where('SourceGuid', $guid)
|
||||
->exists();
|
||||
if (! $mainExists) {
|
||||
return ['data' => [], 'ready' => true];
|
||||
}
|
||||
|
||||
$rows = $connection->table($items->targetTable())
|
||||
->where('guid', $guid)
|
||||
->orderBy('Id')
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
return ['data' => $rows->map(fn (object $row): array => $this->mapItem((array) $row))->all(), 'ready' => true];
|
||||
}
|
||||
|
||||
private function ready($dataset, $items, string $connection): bool
|
||||
{
|
||||
return $this->mirror->ready($dataset) && $this->mirror->ready($items)
|
||||
@ -55,7 +84,13 @@ class InstallationDetailMirrorRepository
|
||||
if (! $this->mirror->ready($dataset) || ! Schema::connection($connection->getName())->hasTable($dataset->targetTable())) {
|
||||
return ['rows' => 0, 'requested' => 0.0, 'settled' => 0.0, 'paid' => 0.0];
|
||||
}
|
||||
$row = $connection->table($dataset->targetTable())->selectRaw('COUNT(*) AS row_count, COALESCE(SUM(installcost),0) AS requested, COALESCE(SUM(settleAmount),0) AS settled, COALESCE(SUM(acceptAmount),0) AS paid')->first();
|
||||
$latest = $connection->table($dataset->targetTable())
|
||||
->selectRaw('MAX(Id) AS id')
|
||||
->groupBy('SourceGuid');
|
||||
$row = $connection->table($dataset->targetTable() . ' AS v')
|
||||
->joinSub($latest, 'latest', static fn ($join) => $join->on('latest.id', '=', 'v.Id'))
|
||||
->selectRaw('COUNT(*) AS row_count, COALESCE(SUM(v.installcost),0) AS requested, COALESCE(SUM(v.settleAmount),0) AS settled, COALESCE(SUM(v.acceptAmount),0) AS paid')
|
||||
->first();
|
||||
return ['rows' => (int) $row->row_count, 'requested' => (float) $row->requested, 'settled' => (float) $row->settled, 'paid' => (float) $row->paid];
|
||||
}
|
||||
|
||||
@ -104,6 +139,43 @@ class InstallationDetailMirrorRepository
|
||||
return array_replace(array_fill_keys(array_keys(\App\Models\Business\InstallationDetail::columnLabels()), null), $mapped);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $row */
|
||||
private function mapItem(array $row): array
|
||||
{
|
||||
$value = static function (array $row, string ...$names): mixed {
|
||||
foreach ($names as $name) {
|
||||
foreach ($row as $key => $value) {
|
||||
if (strcasecmp((string) $key, $name) === 0) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
return [
|
||||
'transaction_no' => $value($row, 'orderNum', 'ordernum'),
|
||||
'product_id' => $value($row, 'shopproid'),
|
||||
'id_owner' => $value($row, 'id_owner', 'idOwner'),
|
||||
'department_owner' => $value($row, 'department_owner', 'departmentOwner'),
|
||||
'auxiliary_spu' => $value($row, 'Spu'),
|
||||
'product_store_code' => $value($row, 'shopskucode'),
|
||||
'item_no' => $value($row, 'proCode'),
|
||||
'barcode' => $value($row, 'barCode'),
|
||||
'product_name' => $value($row, 'skuName'),
|
||||
'supplier_name' => $value($row, 'scmname'),
|
||||
'detail_requested_amount' => $value($row, 'applyItemAmount'),
|
||||
'detail_requested_quantity' => $value($row, 'installcount'),
|
||||
'detail_settled_amount' => $value($row, 'settleItemAmount'),
|
||||
'detail_received_amount' => $value($row, 'acceptItemAmount'),
|
||||
'installation_company' => $value($row, 'installCompanyName'),
|
||||
'available' => $value($row, 'isAvailable', 'available'),
|
||||
'quote' => $value($row, 'quote'),
|
||||
'quote_owner' => $value($row, 'quoteOwner'),
|
||||
'quote_time' => $value($row, 'quoteTime'),
|
||||
];
|
||||
}
|
||||
|
||||
private function yesNo(mixed $value): ?string
|
||||
{
|
||||
return $value === null ? null : ((int) $value === 1 ? '是' : '否');
|
||||
|
||||
@ -200,6 +200,52 @@
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.installation-expand {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: 1px solid #d8d2c7;
|
||||
border-radius: 0.375rem;
|
||||
background: #ffffff;
|
||||
color: #856632;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.installation-detail-row td {
|
||||
padding: 0.75rem 1rem 1rem 3rem;
|
||||
background: #fbfaf7;
|
||||
}
|
||||
|
||||
.installation-detail-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-detail-table th,
|
||||
.installation-detail-table td {
|
||||
padding: 0.45rem 0.65rem;
|
||||
border: 1px solid #e5e0d7;
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.installation-detail-table th {
|
||||
background: #f3efe6;
|
||||
color: #595959;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.installation-detail-empty,
|
||||
.installation-detail-error {
|
||||
color: #595959;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.installation-detail-error {
|
||||
color: #b80000;
|
||||
}
|
||||
|
||||
.biz-table .num {
|
||||
text-align: right;
|
||||
font-variant-numeric: tabular-nums;
|
||||
@ -545,8 +591,15 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
@forelse ($rows as $row)
|
||||
<tr>
|
||||
<td>{{ $row->id }}</td>
|
||||
<tr data-installation-row data-guid="{{ $row->related_guid }}">
|
||||
<td>
|
||||
@if ($page['slug'] === 'installation-details' && $row->related_guid)
|
||||
<button type="button" class="installation-expand" data-installation-expand aria-expanded="false" title="查看安装商品明细">
|
||||
<span class="material-icons-outlined" style="font-size:1rem">chevron_right</span>
|
||||
</button>
|
||||
@endif
|
||||
{{ $row->id }}
|
||||
</td>
|
||||
@foreach ($descriptor->fields as $field)
|
||||
@php
|
||||
$val = $row->{$field['name']};
|
||||
@ -601,6 +654,59 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($page['slug'] === 'installation-details')
|
||||
<script>
|
||||
(() => {
|
||||
const detailUrl = @json(route('business.installation-details.details', ['guid' => '__GUID__']));
|
||||
const columns = [
|
||||
['transaction_no', '交易单号'], ['product_id', '商品id'], ['id_owner', 'ID负责人'],
|
||||
['department_owner', '部门负责人'], ['auxiliary_spu', '辅助码Spu'], ['product_store_code', '商品店铺编码'],
|
||||
['item_no', '货号'], ['barcode', '条形码'], ['product_name', '商品名称'], ['supplier_name', '供应商名称'],
|
||||
['detail_requested_amount', '申请明细金额'], ['detail_requested_quantity', '申请明细数量'],
|
||||
['detail_settled_amount', '结算明细金额'], ['detail_received_amount', '收款明细金额'],
|
||||
['installation_company', '安装公司'], ['available', '是否可用'], ['quote', '报价'],
|
||||
['quote_owner', '报价人'], ['quote_time', '报价时间'],
|
||||
];
|
||||
const escapeHtml = (value) => String(value ?? '').replace(/[&<>"']/g, (char) => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[char]));
|
||||
const renderDetails = (items) => {
|
||||
if (!items.length) return '<div class="installation-detail-empty">暂无商品明细</div>';
|
||||
const head = columns.map(([, label]) => `<th>${escapeHtml(label)}</th>`).join('');
|
||||
const body = items.map((item) => `<tr>${columns.map(([key]) => `<td>${escapeHtml(item[key])}</td>`).join('')}</tr>`).join('');
|
||||
return `<table class="installation-detail-table"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table>`;
|
||||
};
|
||||
document.querySelectorAll('[data-installation-expand]').forEach((button) => {
|
||||
button.addEventListener('click', async () => {
|
||||
const row = button.closest('[data-installation-row]');
|
||||
const existing = row.nextElementSibling;
|
||||
if (existing?.matches('.installation-detail-row')) {
|
||||
existing.hidden = !existing.hidden;
|
||||
button.setAttribute('aria-expanded', String(!existing.hidden));
|
||||
button.querySelector('.material-icons-outlined').textContent = existing.hidden ? 'chevron_right' : 'expand_more';
|
||||
return;
|
||||
}
|
||||
const detailRow = document.createElement('tr');
|
||||
detailRow.className = 'installation-detail-row';
|
||||
detailRow.innerHTML = `<td colspan="{{ count($descriptor->fields) + 1 }}"><div>正在加载明细…</div></td>`;
|
||||
row.after(detailRow);
|
||||
button.disabled = true;
|
||||
try {
|
||||
const response = await fetch(detailUrl.replace('__GUID__', encodeURIComponent(row.dataset.guid)), {headers: {'Accept': 'application/json'}});
|
||||
if (!response.ok) throw new Error('明细加载失败');
|
||||
const payload = await response.json();
|
||||
detailRow.firstElementChild.innerHTML = renderDetails(payload.data || []);
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
button.querySelector('.material-icons-outlined').textContent = 'expand_more';
|
||||
} catch (error) {
|
||||
detailRow.firstElementChild.innerHTML = '<div class="installation-detail-error">明细加载失败,请稍后重试</div>';
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@endif
|
||||
|
||||
<div class="biz-pagination">
|
||||
<span>共 {{ $rows->total() }} 条 · 第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页</span>
|
||||
<span>
|
||||
|
||||
@ -117,6 +117,10 @@ Route::get('workspace/courier-payables/courier-information/export', 'Business\Pa
|
||||
Route::get('workspace/{section}/{page}/{tab}/export', 'Business\Pages@export')
|
||||
->name('business.pages.export');
|
||||
|
||||
Route::get('workspace/financial-operations/installation-details/details/{guid}', 'Business\Pages@installationDetails')
|
||||
->where('guid', '[A-Za-z0-9-]+')
|
||||
->name('business.installation-details.details');
|
||||
|
||||
Route::get('workspace/{section}/{page?}/{tab?}', 'Business\Pages@show')
|
||||
->name('business.pages.show');
|
||||
|
||||
|
||||
@ -29,6 +29,7 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
'ready' => true, 'total' => 1, 'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [
|
||||
'id' => 1, 'installation_no' => 'INSTALL-TEST-001', 'installation_company' => '测试安装公司',
|
||||
'store_name' => '测试店铺', 'requested_amount' => '120.50', 'settled_amount' => '100.00', 'item_quantity' => 2,
|
||||
'related_guid' => 'guid-install-test',
|
||||
])],
|
||||
]);
|
||||
$mirror->shouldReceive('summary')->once()->andReturn(['rows' => 1, 'requested' => 120.50, 'settled' => 100.00, 'paid' => 0.0]);
|
||||
@ -50,6 +51,28 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertSeeText('共 1 条');
|
||||
}
|
||||
|
||||
public function testInstallationDetailsEndpointReturnsChildItemsForMainRecord(): void
|
||||
{
|
||||
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
|
||||
$mirror->shouldReceive('details')->once()->with('guid-install-test')->andReturn([
|
||||
'ready' => true,
|
||||
'data' => [[
|
||||
'transaction_no' => 'INSTALL-TEST-001',
|
||||
'product_id' => '10035070813217',
|
||||
'product_name' => '测试商品',
|
||||
'detail_requested_amount' => '53.59',
|
||||
'detail_requested_quantity' => 1,
|
||||
]],
|
||||
]);
|
||||
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.installation-details.details', ['guid' => 'guid-install-test']))
|
||||
->assertOk()
|
||||
->assertJsonPath('data.0.transaction_no', 'INSTALL-TEST-001')
|
||||
->assertJsonPath('data.0.product_name', '测试商品');
|
||||
}
|
||||
|
||||
public function testCompanyInformationUsesWorkbookHeadersAndReadOnlyUpstreamRows(): void
|
||||
{
|
||||
$this->mockUpstreamPage((object) [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user