diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index d0b503a3..45eab080 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -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, diff --git a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php index 2c7381df..725a5acc 100644 --- a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php @@ -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>,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 $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 ? '是' : '否'); diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 98ceb45e..e4d401c5 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -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 @@ @forelse ($rows as $row) - - {{ $row->id }} + + + @if ($page['slug'] === 'installation-details' && $row->related_guid) + + @endif + {{ $row->id }} + @foreach ($descriptor->fields as $field) @php $val = $row->{$field['name']}; @@ -601,6 +654,59 @@ + @if ($page['slug'] === 'installation-details') + + @endif +
共 {{ $rows->total() }} 条 · 第 {{ $rows->currentPage() }} / {{ $rows->lastPage() }} 页 diff --git a/routes/admin.php b/routes/admin.php index 58ced581..0f90eecb 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -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'); diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php index 129825fa..e1ecfc54 100644 --- a/tests/Feature/Business/ManagementCrudTest.php +++ b/tests/Feature/Business/ManagementCrudTest.php @@ -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) [