diff --git a/app/Business/BusinessSummaryCards.php b/app/Business/BusinessSummaryCards.php new file mode 100644 index 00000000..dbbaac72 --- /dev/null +++ b/app/Business/BusinessSummaryCards.php @@ -0,0 +1,182 @@ + $summary + * @return list,tone:string}> + */ + public function forPage(string $key, array $summary = []): array + { + return match ($key) { + 'store-management/store-report' => $summary === [] ? [] : $this->storeReportCards($summary), + 'company-management/subject-operating-report' => $summary === [] ? [] : $this->subjectOperatingReportCards($summary), + 'store-management/internal-flow-report' => $summary === [] ? [] : $this->internalFlowReportCards($summary), + 'company-management/company-cost-arrangement' => $this->companyCostArrangementCards(), + 'company-management/tax-forecast' => $this->taxForecastCards(), + 'company-management/personnel-inventory' => $this->personnelInventoryCards(), + default => [], + }; + } + + /** @param array $summary */ + private function subjectOperatingReportCards(array $summary): array + { + $ready = (bool) ($summary['company_status_ready'] ?? false); + $previousLabel = (string) ($summary['previous_month_label'] ?? '上月'); + $currentLabel = (string) ($summary['current_month_label'] ?? '当月'); + $previousStores = (int) ($summary['previous_income_entities'] ?? 0); + $currentStores = (int) ($summary['current_income_entities'] ?? 0); + + if (! $ready) { + return [ + $this->card('正常存续主体', '—', '公司资料镜像未就绪,暂时无法剔除注销主体', [], 'text', 'warning'), + $this->card('上月收入', '—', '公司资料镜像未就绪', [], 'text', 'warning'), + $this->card('当月收入', '—', '公司资料镜像未就绪', [], 'text', 'warning'), + $this->card('小规模纳税人', '—', '公司资料镜像未就绪', [], 'text'), + $this->card('一般纳税人', '—', '公司资料镜像未就绪', [], 'text'), + ]; + } + + return [ + $this->card('正常存续主体', $summary['normal'] ?? 0, '只统计公司台账标签为保留的主体', ['company_status' => '正常'], 'integer', 'accent'), + $this->card($previousLabel . '收入', $summary['previous_income'] ?? 0, $previousStores . ' 个存续主体有收入', [ + 'company_status' => '正常', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card($currentLabel . '收入', $summary['current_month_income'] ?? 0, $currentStores . ' 个存续主体有收入', [ + 'company_status' => '正常', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card('小规模纳税人', $summary['small_taxpayer'] ?? 0, '正常存续主体中的小规模纳税人', [ + 'company_status' => '正常', + 'tax_type' => '小规模纳税人', + ]), + $this->card('一般纳税人', $summary['general_taxpayer'] ?? 0, '正常存续主体中的一般纳税人', [ + 'company_status' => '正常', + 'tax_type' => '一般纳税人', + ]), + ]; + } + + /** @param array $summary */ + private function storeReportCards(array $summary): array + { + $previousLabel = (string) ($summary['previous_month_label'] ?? '上月'); + $currentLabel = (string) ($summary['current_month_label'] ?? '当月'); + $normalStores = (int) ($summary['normal_stores'] ?? 0); + $previousStores = (int) ($summary['previous_month_stores'] ?? 0); + $currentStores = (int) ($summary['current_month_stores'] ?? 0); + $previousShare = $normalStores > 0 + ? number_format($previousStores / $normalStores * 100, 1) . '%' + : '0.0%'; + + return [ + $this->card('正常存续店铺', $normalStores, '只统计开店中店铺,不含关店和已注销', ['status' => '0'], 'integer', 'accent'), + $this->card($previousLabel . '销量', $summary['previous_month_sales'] ?? 0, $previousStores . ' 家存续店铺有销量', [ + 'status' => '0', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card($currentLabel . '销量', $summary['current_month_sales'] ?? 0, $currentStores . ' 家存续店铺有销量', [ + 'status' => '0', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card($previousLabel . '有销量店铺', $previousStores, '占正常存续店铺 ' . $previousShare, [ + 'status' => '0', + 'sort' => $summary['previous_month_key'] ?? null, + 'direction' => 'desc', + ]), + $this->card($currentLabel . '有销量店铺', $currentStores, $currentStores === 0 ? '当前还未同步当月销量' : '正常存续店铺中当月已有销量', [ + 'status' => '0', + 'sort' => $summary['current_month_key'] ?? null, + 'direction' => 'desc', + ]), + ]; + } + + /** @param array $summary */ + private function internalFlowReportCards(array $summary): array + { + return [ + $this->card('公司数', $summary['companies'] ?? 0, '当前筛选内内部流转公司', [], 'integer', 'accent'), + $this->card('预计销量合计', $summary['projected_sales'] ?? 0, '当前筛选内预计销量合计', [ + 'sort' => 'projected_sales', + 'direction' => 'desc', + ], 'amount'), + $this->card('已收票合计', $summary['receipt_total'] ?? 0, '供应商、关联公司与内部流转收票合计', [ + 'sort' => 'receipt_total', + 'direction' => 'desc', + ], 'amount', 'success'), + $this->card('剩余缺口合计', $summary['remaining_gap'] ?? 0, '金额越大越应优先处理', [ + 'sort' => 'remaining_gap', + 'direction' => 'desc', + ], 'amount', 'warning'), + $this->card('平均开票比例', $summary['average_invoicing_ratio'] ?? 0, '按有比例的公司简单平均', [], 'percent'), + ]; + } + + private function companyCostArrangementCards(): array + { + return [ + $this->card('数据状态', '待接入', '当前表尚无业务数据', [], 'text', 'warning'), + $this->card('完整表头', '11 列', '包含序号,收入、成本、税金、人工等字段全部保留', [], 'text'), + $this->card('核心结果', '预计结余 / 缺口', '当月收入预测减去成本与费用', [], 'text'), + $this->card('缺口提示', '负值标红', '成本或费用未覆盖收入时提醒', [], 'text', 'danger'), + $this->card('配平提示', '零或正结余标绿', '表示当月预算已覆盖', [], 'text', 'success'), + ]; + } + + private function taxForecastCards(): array + { + return [ + $this->card('数据状态', '待取数', '规则已定义,业务数据尚未接入', [], 'text', 'warning'), + $this->card('完整表头', '22 列', '包含序号、征管口径、收入、税额与备注', [], 'text'), + $this->card('计算税种', '4 类', '增值税、附加税、印花税、所得税', [], 'text'), + $this->card('必补口径', '2 项', '工商类型与征收方式', [], 'text', 'warning'), + $this->card('当前记录', '0 条', '数据接入后按完整字段展示', [], 'text'), + ]; + } + + private function personnelInventoryCards(): array + { + return [ + $this->card('数据状态', '待接入', '当前表尚无人员数据', [], 'text', 'warning'), + $this->card('完整表头', '17 列', '包含序号和全部人员、容量字段', [], 'text'), + $this->card('敏感字段', '4 项', '密码、身份证、实名手机和身份证地址只显示维护状态', [], 'text', 'danger'), + $this->card('容量规则', '3 档', '可担任公司数量按风险档位提示', [], 'text'), + $this->card('当前记录', '0 条', '接入后按正常在职人员汇总', [], 'text'), + ]; + } + + /** + * @return array{label:string,value:mixed,hint:string,format:string,query:array,tone:string} + */ + private function card( + string $label, + mixed $value, + string $hint, + array $query = [], + string $format = 'integer', + string $tone = 'neutral', + ): array { + return [ + 'label' => $label, + 'value' => $value, + 'hint' => $hint, + 'format' => $format, + 'query' => ['page' => null, ...$query], + 'tone' => $tone, + ]; + } +} diff --git a/app/Business/UpstreamBusinessInformation.php b/app/Business/UpstreamBusinessInformation.php index 9481db3a..67b39aa1 100644 --- a/app/Business/UpstreamBusinessInformation.php +++ b/app/Business/UpstreamBusinessInformation.php @@ -45,7 +45,7 @@ class UpstreamBusinessInformation }; request()->attributes->set( 'business-page-summary-cards', - $this->businessSummaryCards($key, $payload['summary'] ?? []), + app(BusinessSummaryCards::class)->forPage($key, $payload['summary'] ?? []), ); return new LengthAwarePaginator( @@ -60,97 +60,6 @@ class UpstreamBusinessInformation ); } - private function businessSummaryCards(string $key, array $summary): array - { - if ($summary === []) { - return []; - } - - return match ($key) { - 'store-management/store-report' => [ - $this->summaryCard('店铺总数', $summary['stores'] ?? 0, '当前筛选内唯一店铺,明细按期间拆分'), - $this->summaryCard('正常店铺', $summary['normal'] ?? 0, '店铺状态为正常(开店中)', ['status' => '0']), - $this->summaryCard('关店中', $summary['closing'] ?? 0, '店铺状态为关闭中', ['status' => '3']), - $this->summaryCard('已关闭', $summary['closed'] ?? 0, '店铺状态为已关闭', ['status' => '6']), - $this->summaryCard('期间销量', $summary['period_sales'] ?? 0, '收入 + 优惠 + 冲减 + 退款', [ - 'sort' => 'period_sales', - 'direction' => 'desc', - ], 'money'), - ], - 'company-management/subject-operating-report' => [ - $this->summaryCard('主体总数', $summary['total'] ?? 0, '当前筛选内唯一主体'), - ($summary['company_status_ready'] ?? false) - ? $this->summaryCard('有效主体', $summary['normal'] ?? 0, '经营状态为正常', ['company_status' => '正常']) - : $this->summaryCard('有效主体', '—', '公司资料镜像未就绪', [], 'text'), - ($summary['company_status_ready'] ?? false) - ? $this->summaryCard('已注销主体', $summary['deregistered'] ?? 0, '最新公司资料状态为已注销', ['company_status' => '已注销']) - : $this->summaryCard('已注销主体', '—', '公司资料镜像未就绪', [], 'text'), - $this->summaryCard('最近12个月收入', $summary['rolling_income'] ?? 0, '最近 12 个完整月账单收入', [ - 'sort' => 'rolling_12m_income', - 'direction' => 'desc', - ], 'money'), - $this->summaryCard('本月收入', $summary['current_month_income'] ?? 0, (string) ($summary['current_month_label'] ?? '本月') . '账单收入', [ - 'sort' => 'income_' . ($summary['current_month_number'] ?? now()->format('m')), - 'direction' => 'desc', - ], 'money'), - ], - 'store-management/basic-information' => [ - $this->summaryCard('店铺总数', $summary['total'] ?? 0, '当前筛选内店铺档案'), - $this->summaryCard('正常店铺', $summary['normal'] ?? 0, '店铺状态为正常', ['store_status' => '正常']), - $this->summaryCard('关闭中', $summary['closing'] ?? 0, '店铺状态为关闭中', ['store_status' => '关闭中']), - $this->summaryCard('已关闭', $summary['closed'] ?? 0, '店铺状态为已关闭', ['store_status' => '已关闭']), - $this->summaryCard('平台数', $summary['platforms'] ?? 0, '按店铺档案平台去重', [ - 'sort' => 'platform', - 'direction' => 'asc', - ]), - ], - 'store-management/store-accounts' => [ - $this->summaryCard('账号总数', $summary['total'] ?? 0, '当前筛选内账号记录'), - $this->summaryCard('主账号', $summary['main_accounts'] ?? 0, '账号类型为 MAIN', ['account_type' => '主账号']), - $this->summaryCard('子账号', $summary['sub_accounts'] ?? 0, '账号类型为 SUB', ['account_type' => '子账号']), - $this->summaryCard('密码已设置', $summary['password_set'] ?? 0, '仅统计是否设置,不展示密码', ['password_configured' => '已设置']), - $this->summaryCard('密码未设置', $summary['password_missing'] ?? 0, '需店铺负责人补充', ['password_configured' => '未设置']), - ], - 'company-management/basic-information' => [ - $this->summaryCard('公司总数', $summary['total'] ?? 0, '当前筛选内公司档案'), - $this->summaryCard('正常公司', $summary['normal'] ?? 0, '公司状态为正常', ['company_status' => '正常']), - $this->summaryCard('已注销公司', $summary['deregistered'] ?? 0, '公司状态为已注销', ['company_status' => '已注销']), - $this->summaryCard('一般纳税人', $summary['general_taxpayer'] ?? 0, '纳税人类型为一般纳税人', ['taxpayer_type' => '一般纳税人']), - $this->summaryCard('小规模纳税人', $summary['small_taxpayer'] ?? 0, '纳税人类型为小规模纳税人', ['taxpayer_type' => '小规模纳税人']), - ], - 'trademark-management/information' => [ - $this->summaryCard('商标总数', $summary['total'] ?? 0, '当前筛选内商标档案'), - $this->summaryCard('有效商标', $summary['enabled'] ?? 0, '商标状态为有效', ['trademark_status' => '有效']), - $this->summaryCard('未启用', $summary['disabled'] ?? 0, '商标状态为未启用', ['trademark_status' => '未启用']), - $this->summaryCard('最终持有人', $summary['holders'] ?? 0, '按非空最终持有人去重', [ - 'sort' => 'final_holder', - 'direction' => 'asc', - ]), - $this->summaryCard('保管部门', $summary['departments'] ?? 0, '按非空保管部门去重', [ - 'sort' => 'custody_department', - 'direction' => 'asc', - ]), - ], - default => [], - }; - } - - private function summaryCard( - string $label, - mixed $value, - string $hint, - array $query = [], - string $format = 'integer', - ): array { - return [ - 'label' => $label, - 'value' => $value, - 'hint' => $hint, - 'format' => $format, - 'query' => ['page' => null, ...$query], - ]; - } - /** * 财务经营风险管控 › 流程统计:店铺申请流程(开店 / 变更 / 关店)只读镜像。 * @@ -410,12 +319,6 @@ SQL); $filters = StoreReportRepository::requestFilters(); $rows = $this->storeReportRows($filters); - // 所有店铺该月都没有业绩的月份列默认隐藏;「筛选字段」中仍保留勾选项,可手动选出来。 - request()->attributes->set( - 'store-report.default_hidden_columns', - $this->columnsWithoutData($rows, 'sales_', zeroCountsAsData: false), - ); - $offset = ($page - 1) * $perPage; return [ @@ -428,9 +331,21 @@ SQL); /** @param list> $rows */ private function storeReportSummary(array $rows): array { - $stores = []; - $periodSales = 0.0; + $normalStores = []; + $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); + $currentMonth = now()->startOfMonth(); + $previousMonthKey = 'sales_' . $previousMonth->format('m'); + $currentMonthKey = 'sales_' . $currentMonth->format('m'); + $previousSales = 0.0; + $currentSales = 0.0; + $previousStores = []; + $currentStores = []; + foreach ($rows as $row) { + if (trim((string) ($row['status'] ?? '')) !== '开店中') { + continue; + } + $key = trim((string) ($row['erp_store_name'] ?? '')); if ($key === '') { $key = trim((string) ($row['store_id'] ?? $row['id'] ?? '')); @@ -438,18 +353,32 @@ SQL); if ($key === '') { continue; } - $stores[$key] = (string) ($row['status'] ?? ''); - $periodSales += (float) ($row['period_sales'] ?? 0); + + $normalStores[$key] = true; + $previousValue = (float) ($row[$previousMonthKey] ?? 0); + $currentValue = (float) ($row[$currentMonthKey] ?? 0); + $previousSales += $previousValue; + $currentSales += $currentValue; + if ($previousValue > 0) { + $previousStores[$key] = true; + } + if ($currentValue > 0) { + $currentStores[$key] = true; + } } - $statusCounts = array_count_values($stores); - return [ - 'stores' => count($stores), - 'normal' => (int) ($statusCounts['开店中'] ?? 0), - 'closing' => (int) ($statusCounts['关店中'] ?? 0), - 'closed' => (int) ($statusCounts['关店'] ?? 0), - 'period_sales' => round($periodSales, 2), + 'normal_stores' => count($normalStores), + 'previous_month_sales' => round($previousSales, 2), + 'current_month_sales' => round($currentSales, 2), + 'previous_month_stores' => count($previousStores), + 'current_month_stores' => count($currentStores), + 'previous_month_key' => $previousMonthKey, + 'current_month_key' => $currentMonthKey, + 'previous_month_label' => $previousMonth->isSameYear($currentMonth) + ? '上月(' . $previousMonth->format('n月') . ')' + : '上月(' . $previousMonth->format('Y年n月') . ')', + 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', ]; } @@ -601,21 +530,26 @@ SQL); $row['city'] = $this->cityFromDetails($row['company_details']); $row['company_status'] = $row['company_details'][0]['company_status'] ?? null; $classification = $classifications[$row['entity']] ?? []; - $row['tax_type'] = $classification['tax_type'] + $taxType = $classification['tax_type'] ?? $row['company_details'][0]['taxpayer_category'] ?? null; - $row['business_registration_type'] = $classification['business_registration_type'] + $businessType = $classification['business_registration_type'] ?? $row['company_details'][0]['business_registration_type'] ?? null; + $classificationStatus = trim((string) ($classification['status'] ?? '')); + $retained = $classificationStatus !== '' + ? $classificationStatus === '保留' + : $row['company_status'] === '正常'; + $row['company_status'] = $retained ? '正常' : '已注销'; + $row['tax_type'] = $retained + ? ($this->normalizeTaxType($taxType) ?? '待维护') + : '/'; + $row['business_registration_type'] = $retained + ? (trim((string) $businessType) !== '' ? trim((string) $businessType) : '待维护') + : '/'; } unset($row); - // 所有主体该月收入均为空时,该月份列在页面上默认隐藏;「筛选字段」中仍保留勾选项,可手动选出来。 - request()->attributes->set( - 'subject-operating-report.default_hidden_columns', - $this->columnsWithoutData($rows, 'income_', zeroCountsAsData: true), - ); - $rows = array_values(array_filter($rows, fn (array $row): bool => $this->subjectMatchesFilters($row, $filters))); $summary = $this->subjectSummary($rows, $companyStatusReady); $this->sortSubjectRows($rows, $filters); @@ -630,9 +564,17 @@ SQL); { $normal = 0; $deregistered = 0; - $rollingIncome = 0.0; - $currentMonthKey = 'income_' . now()->format('m'); + $smallTaxpayers = 0; + $generalTaxpayers = 0; + $previousMonth = now()->startOfMonth()->subMonthNoOverflow(); + $currentMonth = now()->startOfMonth(); + $previousMonthKey = 'income_' . $previousMonth->format('m'); + $currentMonthKey = 'income_' . $currentMonth->format('m'); + $previousIncome = 0.0; $currentMonthIncome = 0.0; + $previousIncomeEntities = 0; + $currentIncomeEntities = 0; + foreach ($rows as $row) { $status = trim((string) ($row['company_status'] ?? '')); if ($status === '正常') { @@ -640,11 +582,28 @@ SQL); } elseif ($status === '已注销') { $deregistered++; } - if (($row['rolling_12m_income'] ?? null) !== null) { - $rollingIncome += (float) $row['rolling_12m_income']; + + if ($status !== '正常') { + continue; } - if (($row[$currentMonthKey] ?? null) !== null) { - $currentMonthIncome += (float) $row[$currentMonthKey]; + + $taxType = (string) ($row['tax_type'] ?? ''); + if (str_contains($taxType, '小规模')) { + $smallTaxpayers++; + } + if (str_contains($taxType, '一般纳税人')) { + $generalTaxpayers++; + } + + $previousValue = (float) ($row[$previousMonthKey] ?? 0); + $currentValue = (float) ($row[$currentMonthKey] ?? 0); + $previousIncome += $previousValue; + $currentMonthIncome += $currentValue; + if ($previousValue > 0) { + $previousIncomeEntities++; + } + if ($currentValue > 0) { + $currentIncomeEntities++; } } @@ -652,45 +611,34 @@ SQL); 'total' => count($rows), 'normal' => $normal, 'deregistered' => $deregistered, - 'rolling_income' => round($rollingIncome, 2), + 'small_taxpayer' => $smallTaxpayers, + 'general_taxpayer' => $generalTaxpayers, + 'previous_income' => round($previousIncome, 2), 'current_month_income' => round($currentMonthIncome, 2), - 'current_month_number' => now()->format('m'), - 'current_month_label' => now()->format('Y年n月'), + 'previous_income_entities' => $previousIncomeEntities, + 'current_income_entities' => $currentIncomeEntities, + 'previous_month_key' => $previousMonthKey, + 'current_month_key' => $currentMonthKey, + 'previous_month_label' => $previousMonth->isSameYear($currentMonth) + ? '上月(' . $previousMonth->format('n月') . ')' + : '上月(' . $previousMonth->format('Y年n月') . ')', + 'current_month_label' => '当月(' . $currentMonth->format('n月') . ')', 'company_status_ready' => $companyStatusReady, ]; } - /** - * 没有数据的月份列:所有行该月都没有数值时返回该列名,页面据此默认隐藏。 - * - * 收入(income_)以 null/空字符串表示无账单,金额 0 也算有账单; - * 销量(sales_)没有空值标记,数值 0 视为当月没有业绩。 - * - * @param array> $rows - * @return array - */ - private function columnsWithoutData(array $rows, string $prefix, bool $zeroCountsAsData): array + private function normalizeTaxType(mixed $value): ?string { - $columns = []; - - for ($month = 1; $month <= 12; $month++) { - $key = sprintf('%s%02d', $prefix, $month); - foreach ($rows as $row) { - $value = $row[$key] ?? null; - if ($value === null || $value === '') { - continue; - } - if (! $zeroCountsAsData && (float) $value === 0.0) { - continue; - } - - continue 2; - } - - $columns[] = $key; + $taxType = trim((string) $value); + if ($taxType === '') { + return null; } - return $columns; + return match ($taxType) { + '小规模' => '小规模纳税人', + '一般纳税人', '小规模纳税人', '个体工商户', '无税务登记' => $taxType, + default => $taxType, + }; } /** @param array $row @param array $filters */ diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index ea4535b1..617fd6e6 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\Business; use App\Business\BusinessTableImporter; +use App\Business\BusinessSummaryCards; use App\Business\InventoryInboundDetailImporter; use App\Business\InventoryWarehouseNameMappingWorkbookImporter; use App\Business\InventoryWarehouseShortNames; @@ -1116,6 +1117,10 @@ class Pages extends Controller return response()->json([ 'total' => $payload['total'], 'rows' => $payload['rows'], + 'previous_total' => $payload['previous_total'] ?? null, + 'previous_from' => $payload['previous_from'] ?? null, + 'previous_to' => $payload['previous_to'] ?? null, + 'change_rate' => $payload['change_rate'] ?? null, ], 200, ['Cache-Control' => 'private, no-store']); } @@ -3764,9 +3769,25 @@ class Pages extends Controller $agreementCourierContractParties = $this->agreementCourierContractParties($page['slug'] ?? null, $agreementCourierNames); $agreementSupplierOptions = $this->agreementSupplierOptions($page['slug'] ?? null); + $staticSummaryKey = (string) ($section['slug'] ?? '') . '/' . (string) ($page['slug'] ?? ''); + if (in_array($staticSummaryKey, [ + 'company-management/company-cost-arrangement', + 'company-management/tax-forecast', + 'company-management/personnel-inventory', + ], true)) { + request()->attributes->set( + 'business-page-summary-cards', + app(BusinessSummaryCards::class)->forPage($staticSummaryKey), + ); + } + // 内部流转报表读取共享业务镜像中由工作簿导入的数据集,不走上游只读查询。 if ($page && $content === null && $this->isInternalFlowReportPage($section, $page)) { - [$rows, $upstreamError] = $this->internalFlowReportRows(); + [$rows, $upstreamError, $flowSummary] = $this->internalFlowReportRows(); + request()->attributes->set( + 'business-page-summary-cards', + app(BusinessSummaryCards::class)->forPage('store-management/internal-flow-report', $flowSummary), + ); } if (($section['slug'] ?? null) === 'product-inventory' && ($page['slug'] ?? null) === 'inventory-report') { @@ -4216,7 +4237,7 @@ class Pages extends Controller * 内部流转报表明细。数据来自共享业务镜像中按工作簿整份替换的导入结果, * 尚未导入时保留空页面并说明原因,不把“未导入”当成业务数据为 0。 * - * @return array{0: LengthAwarePaginator, 1: ?string} + * @return array{0: LengthAwarePaginator, 1: ?string, 2: array} */ private function internalFlowReportRows(): array { @@ -4236,6 +4257,7 @@ class Pages extends Controller return [ $paginate([], 0), '内部流转数据尚未导入。导入《一般纳税人内部流转》工作簿后,本页会显示明细。', + [], ]; } @@ -4250,7 +4272,11 @@ class Pages extends Controller (string) request('direction', 'asc'), ); - return [$paginate($payload['data'], $payload['total']), null]; + return [ + $paginate($payload['data'], $payload['total']), + null, + (array) ($payload['summary'] ?? []), + ]; } private function storeReceivingAccountRows(?string $tab): LengthAwarePaginator diff --git a/config/business_navigation.php b/config/business_navigation.php index dd31dff8..88ae629f 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -17,9 +17,16 @@ $navigation = [ 'fields' => [ ['name' => 'city', 'label' => '市'], ['name' => 'entity', 'label' => '主体'], - ['name' => 'company_type', 'label' => '公司类型'], - ['name' => 'tax_type', 'label' => '税务类型'], - ['name' => 'business_registration_type', 'label' => '工商类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], + ['name' => 'tax_type', 'label' => '税务类型', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], ['name' => 'rolling_12m_income', 'label' => '最近12个月累计收入', 'kind' => 'number'], ...array_map(static fn (int $month): array => ['name' => sprintf('income_%02d', $month), 'label' => $month . '月收入', 'kind' => 'number'], range(1, 12)), ['name' => 'can_change_entity', 'label' => '能否变更主体'], @@ -38,8 +45,11 @@ $navigation = [ 'empty' => '公司成本安排尚未同步数据。', 'fields' => [ ['name' => 'company', 'label' => '公司'], - ['name' => 'company_type', 'label' => '公司类型'], - ['name' => 'invoice_type', 'label' => '发票类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], + ['name' => 'invoice_type', 'label' => '发票类型', 'kind' => 'badge', 'tones' => [ + '专票' => 'accent', + '普票' => 'info', + ]], ['name' => 'current_month_income_forecast', 'label' => '当月收入预测', 'kind' => 'number'], ['name' => 'historical_cost_gap', 'label' => '往期成本缺口', 'kind' => 'number'], ['name' => 'supplier_cost_arrangement', 'label' => '供应商成本安排', 'kind' => 'number'], @@ -61,9 +71,21 @@ $navigation = [ 'empty' => '税金预测表尚未接入取数:工商类型、征收方式(查账征收/核定征收)确认并补齐数据来源后开始计算。', 'fields' => [ ['name' => 'company_name', 'label' => '公司名称'], - ['name' => 'taxpayer_type', 'label' => '公司税务类型'], - ['name' => 'business_registration_type', 'label' => '工商类型'], - ['name' => 'collection_method', 'label' => '征收方式'], + ['name' => 'taxpayer_type', 'label' => '公司税务类型', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], + ['name' => 'collection_method', 'label' => '征收方式', 'kind' => 'badge', 'tones' => [ + '查账征收' => 'info', + '核定征收' => 'accent', + '待维护' => 'warning', + '/' => 'muted', + ]], ['name' => 'assessed_income_rate', 'label' => '核定应税所得率(%)', 'kind' => 'number'], ['name' => 'forecast_month', 'label' => '预测月份'], ['name' => 'revenue_incl_tax', 'label' => '当月含税收入预测', 'kind' => 'number'], @@ -95,9 +117,16 @@ $navigation = [ 'fields' => [ ['name' => 'unified_social_credit_code', 'label' => '统一社会信用代码'], ['name' => 'company_name', 'label' => '公司名称'], - ['name' => 'taxpayer_type', 'label' => '税务类型', 'source' => 'taxpayer_category'], - ['name' => 'business_registration_type', 'label' => '工商类型'], - ['name' => 'company_status', 'label' => '公司状态'], + ['name' => 'taxpayer_type', 'label' => '税务类型', 'source' => 'taxpayer_category', 'kind' => 'badge', 'tones' => [ + '小规模纳税人' => 'info', + '一般纳税人' => 'accent', + '个体工商户' => 'success', + '无税务登记' => 'warning', + '待维护' => 'warning', + '/' => 'muted', + ]], + ['name' => 'business_registration_type', 'label' => '工商类型', 'kind' => 'badge'], + ['name' => 'company_status', 'label' => '公司状态', 'kind' => 'badge'], ['name' => 'subscribed_capital', 'label' => '认缴注册资本(万元)', 'kind' => 'number'], ['name' => 'registered_at', 'label' => '注册时间', 'kind' => 'date', 'source' => 'establishment_date'], ['name' => 'legal_representative', 'label' => '法人'], @@ -123,8 +152,8 @@ $navigation = [ 'fields' => [ ['name' => 'person_name', 'label' => '人员名称'], ['name' => 'alias', 'label' => '花名'], - ['name' => 'position', 'label' => '职位'], - ['name' => 'tag', 'label' => '标签'], + ['name' => 'position', 'label' => '职位', 'kind' => 'badge'], + ['name' => 'tag', 'label' => '标签', 'kind' => 'badge'], ['name' => 'tax_account', 'label' => '税务局账号'], ['name' => 'tax_password', 'label' => '税务局密码', 'kind' => 'secret'], ['name' => 'department', 'label' => '部门'], @@ -133,7 +162,7 @@ $navigation = [ ['name' => 'verified_mobile', 'label' => '实名手机号', 'kind' => 'secret'], ['name' => 'sim_card_keeper', 'label' => '电话卡保管人'], ['name' => 'id_address', 'label' => '身份证地址', 'kind' => 'secret'], - ['name' => 'company_type', 'label' => '公司类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], ['name' => 'total_company_count', 'label' => '合计担任公司数量', 'kind' => 'number'], ['name' => 'retained_company_count', 'label' => '保留担任公司数量', 'kind' => 'number'], ['name' => 'available_company_count', 'label' => '可担任公司数量', 'kind' => 'number'], @@ -169,7 +198,7 @@ $navigation = [ 'fields' => [ ['name' => 'trademark_name', 'label' => '商标名称'], ['name' => 'trademark_number', 'label' => '商标号'], - ['name' => 'trademark_status', 'label' => '商标状态'], + ['name' => 'trademark_status', 'label' => '商标状态', 'kind' => 'badge'], ['name' => 'registration_date', 'label' => '注册时间', 'kind' => 'date'], ['name' => 'expiry_date', 'label' => '有效期', 'kind' => 'date'], ['name' => 'international_classification', 'label' => '国际分类', 'source' => 'trademark_type'], @@ -265,10 +294,10 @@ $navigation = [ ['name' => 'store_manager', 'label' => '店铺负责人'], ['name' => 'department_manager', 'label' => '部门负责人'], ['name' => 'opened_at', 'label' => '开店时间', 'kind' => 'date'], - ['name' => 'status', 'label' => '状态'], + ['name' => 'status', 'label' => '状态', 'kind' => 'badge'], ['name' => 'can_change', 'label' => '能否变更'], ['name' => 'entity', 'label' => '主体'], - ['name' => 'company_type', 'label' => '公司类型'], + ['name' => 'company_type', 'label' => '公司类型', 'kind' => 'badge'], ['name' => 'effective_at', 'label' => '生效时间', 'kind' => 'date'], ['name' => 'invalid_at', 'label' => '失效时间', 'kind' => 'date'], ['name' => 'period_sales', 'label' => '期间销量', 'kind' => 'number'], @@ -290,7 +319,11 @@ $navigation = [ 'empty' => '内部流转数据尚未导入。导入工作簿后这里会显示明细。', 'fields' => [ ['name' => 'company_name', 'label' => '抬头'], - ['name' => 'tag', 'label' => '标签'], + ['name' => 'tag', 'label' => '标签', 'kind' => 'badge', 'tones' => [ + '1' => 'accent', + '2' => 'warning', + '3' => 'info', + ]], ['name' => 'projected_sales', 'label' => '预计9月销量', 'kind' => 'number'], ['name' => 'invoiced_to_buyer', 'label' => '开票-给买家开票', 'kind' => 'number'], ['name' => 'internal_invoiced', 'label' => '开票-内部开票', 'kind' => 'number'], @@ -316,11 +349,11 @@ $navigation = [ 'description' => '优先读取共享业务镜像中的店铺资料、店铺 ID 和续签时间;提现账户仅显示配置状态。', 'empty' => '尚未读取到店铺基础资料。请检查共享镜像或上游同步状态。', 'fields' => [ - ['name' => 'platform', 'label' => '平台'], + ['name' => 'platform', 'label' => '平台', 'kind' => 'badge'], ['name' => 'store_id', 'label' => '店铺ID'], ['name' => 'store_name', 'label' => '店铺名称', 'source' => 'erp_store_name'], ['name' => 'store_entity', 'label' => '店铺主体', 'source' => 'company_name'], - ['name' => 'store_status', 'label' => '店铺状态'], + ['name' => 'store_status', 'label' => '店铺状态', 'kind' => 'badge'], ['name' => 'trademark', 'label' => '商标', 'source' => 'trademark_name'], ['name' => 'trademark_number', 'label' => '商标号'], ['name' => 'business_department', 'label' => '业务部门', 'source' => 'profit_center'], @@ -346,16 +379,32 @@ $navigation = [ ['name' => 'operation_department_head', 'label' => '店铺运营部门负责人'], ['name' => 'operation_director', 'label' => '店铺运营总监'], ['name' => 'profit_center', 'label' => '店铺所属利润中心'], - ['name' => 'account_type', 'label' => '账号类型'], + ['name' => 'account_type', 'label' => '账号类型', 'kind' => 'badge', 'tones' => [ + '主账号' => 'accent', + '子账号' => 'info', + ]], ['name' => 'account', 'label' => '账号'], ['name' => 'password_configured', 'label' => '密码', 'kind' => 'secret_status'], ['name' => 'bound_phone', 'label' => '绑定手机号'], ['name' => 'verification_contact', 'label' => '手机号验证码对接人'], ['name' => 'account_group_name', 'label' => '账号分组名称'], - ['name' => 'authorization_status', 'label' => '授权状态'], - ['name' => 'store_status', 'label' => '店铺状态'], - ['name' => 'flow_type', 'label' => '流程类型'], - ['name' => 'approval_status', 'label' => '审批状态'], + ['name' => 'authorization_status', 'label' => '授权状态', 'kind' => 'badge', 'tones' => [ + '已授权' => 'success', + '已过期' => 'warning', + '未授权' => 'danger', + ]], + ['name' => 'store_status', 'label' => '店铺状态', 'kind' => 'badge'], + ['name' => 'flow_type', 'label' => '流程类型', 'kind' => 'badge'], + ['name' => 'approval_status', 'label' => '审批状态', 'kind' => 'badge', 'tones' => [ + '完成' => 'success', + '已签署' => 'success', + '拒绝' => 'danger', + '撤回' => 'muted', + '流程中' => 'info', + '待审批' => 'warning', + '暂存' => 'warning', + '待签署' => 'warning', + ]], ['name' => 'created_at', 'label' => '店铺创建时间', 'kind' => 'date'], ['name' => 'updated_at', 'label' => '店铺修改时间', 'kind' => 'date'], ], diff --git a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php index a1145da4..aa5b7217 100644 --- a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php @@ -175,7 +175,7 @@ SQL) ->all(); } - /** @return array */ + /** @return array */ public function companyClassificationsByNames(array $names): array { $names = array_values(array_unique(array_filter(array_map('trim', $names)))); @@ -184,13 +184,15 @@ SQL) } return $this->connection->table('fin_companies') - ->select(['name', 'tax_type', 'business_type']) + ->select(['name', 'status', 'tax_type', 'business_type']) ->whereIn('name', $names) ->get() ->mapWithKeys(static function (object $row): array { $businessType = trim((string) ($row->business_type ?? '')); + $status = trim((string) ($row->status ?? '')); return [trim((string) $row->name) => [ + 'status' => $status !== '' ? $status : null, 'tax_type' => trim((string) ($row->tax_type ?? '')) ?: null, 'business_registration_type' => in_array($businessType, ['', '/'], true) ? null : $businessType, ]]; diff --git a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php index a33cb19a..e77ff7aa 100644 --- a/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/InstallationDetailMirrorRepository.php @@ -136,29 +136,32 @@ class InstallationDetailMirrorRepository $items = $this->datasets->get(self::ITEM_DATASET); $connection = $this->mirror->connection(); if (! $this->ready($dataset, $items, $connection->getName())) { - return ['ready' => false, 'total' => 0.0, 'rows' => []]; + return [ + 'ready' => false, + 'total' => 0.0, + 'rows' => [], + 'previous_total' => null, + 'previous_from' => null, + 'previous_to' => null, + 'change_rate' => null, + ]; } $from = trim((string) ($filters['customer_review_from'] ?? '')); $to = trim((string) ($filters['customer_review_to'] ?? '')); if ($from === '' || $to === '') { - return ['ready' => true, 'total' => 0.0, 'rows' => []]; + return [ + 'ready' => true, + 'total' => 0.0, + 'rows' => [], + 'previous_total' => null, + 'previous_from' => null, + 'previous_to' => null, + 'change_rate' => null, + ]; } - $latest = $connection->table($dataset->targetTable()) - ->selectRaw('MAX(Id) AS id') - ->groupBy('SourceGuid'); - $main = $connection->table($dataset->targetTable() . ' AS v') - ->joinSub($latest, 'latest_install', static fn ($join) => $join->on('latest_install.id', '=', 'v.Id')) - ->select(['v.SourceGuid', 'v.shop_name', 'v.installCompanyName', 'v.costWay']); - $this->applyShipmentCutoff($main); - - $query = $connection->table($items->targetTable() . ' AS i') - ->joinSub($main, 'install_main', static fn ($join) => - $join->on('install_main.SourceGuid', '=', 'i.guid')) - ->whereNotNull('i.kfshTime') - ->where('i.kfshTime', '>=', $from) - ->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString()); + $query = $this->feeDetailsQuery($connection, $dataset, $items, $from, $to); $platformSql = "CASE" . " WHEN COALESCE(TRIM(install_main.shop_name), '') LIKE '%阿里巴巴%'" @@ -202,26 +205,119 @@ class InstallationDetailMirrorRepository ->values() ->all(); + $total = round(array_sum(array_column($rows, 'amount')), 2); + $previousRange = $this->previousMonthRange($from, $to); + $previousTotal = null; + $changeRate = null; + if ($previousRange !== null) { + $previousQuery = $this->feeDetailsQuery( + $connection, + $dataset, + $items, + $previousRange['from'], + $previousRange['to'], + ); + $previousTotal = round((float) ($previousQuery + ->selectRaw('COALESCE(SUM(CAST(i.settleItemAmount AS DECIMAL(18,2))), 0) AS total') + ->first()->total ?? 0), 2); + if ($previousTotal > 0) { + $changeRate = round((($total - $previousTotal) / $previousTotal) * 100, 1); + } + } + return [ 'ready' => true, - 'total' => round(array_sum(array_column($rows, 'amount')), 2), + 'total' => $total, 'rows' => $rows, + 'previous_total' => $previousTotal, + 'previous_from' => $previousRange['from'] ?? null, + 'previous_to' => $previousRange['to'] ?? null, + 'change_rate' => $changeRate, + ]; + } + + private function feeDetailsQuery( + $connection, + $dataset, + $items, + string $from, + string $to, + ) { + $latest = $connection->table($dataset->targetTable()) + ->selectRaw('MAX(Id) AS id') + ->groupBy('SourceGuid'); + $main = $connection->table($dataset->targetTable() . ' AS v') + ->joinSub($latest, 'latest_install', static fn ($join) => $join->on('latest_install.id', '=', 'v.Id')) + ->select(['v.SourceGuid', 'v.shop_name', 'v.installCompanyName', 'v.costWay']); + $this->applyShipmentCutoff($main); + + return $connection->table($items->targetTable() . ' AS i') + ->joinSub($main, 'install_main', static fn ($join) => + $join->on('install_main.SourceGuid', '=', 'i.guid')) + ->whereNotNull('i.kfshTime') + ->where('i.kfshTime', '>=', $from) + ->where('i.kfshTime', '<', \Illuminate\Support\Carbon::parse($to)->addDay()->toDateString()); + } + + /** @return array{from:string,to:string}|null */ + private function previousMonthRange(string $from, string $to): ?array + { + $start = \Illuminate\Support\Carbon::parse($from)->startOfDay(); + $end = \Illuminate\Support\Carbon::parse($to)->startOfDay(); + if ($end->lt($start)) { + return null; + } + + if ($start->day === 1 && $end->isSameDay($end->copy()->endOfMonth())) { + $previousStart = $start->copy()->subMonthNoOverflow()->startOfMonth(); + $previousEnd = $previousStart->copy()->endOfMonth(); + } else { + $previousStart = $start->copy()->subMonthNoOverflow(); + $previousEnd = $end->copy()->subMonthNoOverflow(); + } + + return [ + 'from' => $previousStart->toDateString(), + 'to' => $previousEnd->toDateString(), ]; } public static function requestFilters(): array { $rules = []; + $rules['time_scope'] = ['nullable', 'in:created,customer_review,finance_review']; + $rules['date_from'] = ['nullable', 'date_format:Y-m-d']; + $rules['date_to'] = ['nullable', 'date_format:Y-m-d']; + if (request()->filled('date_from')) $rules['date_to'][] = 'after_or_equal:date_from'; foreach (['created', 'customer_review', 'finance_review'] as $prefix) { $rules[$prefix . '_from'] = ['nullable', 'date_format:Y-m-d']; $rules[$prefix . '_to'] = ['nullable', 'date_format:Y-m-d']; if (request()->filled($prefix . '_from')) $rules[$prefix . '_to'][] = 'after_or_equal:' . $prefix . '_from'; } - foreach (['push_status','order_no','customer_review','finance_review','quote_status','arranged_status','fee_payer','completed_status','shop_name','delete_status','installation_company','installation_no','order_type','product_id'] as $key) { + foreach (['push_status','order_no','customer_review','finance_review','quote_status','arranged_status','fee_payer','completed_status','shop_name','shop_code','delete_status','installation_company','installation_no','order_type','product_id'] as $key) { $rules[$key] = ['nullable', 'string', 'max:191']; } foreach (['customer_review', 'finance_review', 'arranged_status', 'completed_status', 'delete_status'] as $key) $rules[$key][] = 'in:0,1'; - return validator(request()->only(array_keys($rules)), $rules)->validate(); + $filters = validator(request()->only(array_keys($rules)), $rules)->validate(); + if (! array_key_exists('time_scope', $filters) + && ! array_key_exists('date_from', $filters) + && ! array_key_exists('date_to', $filters)) { + return $filters; + } + + $scope = (string) ($filters['time_scope'] ?? 'customer_review'); + $prefix = match ($scope) { + 'created' => 'created', + 'finance_review' => 'finance_review', + default => 'customer_review', + }; + foreach (['created', 'customer_review', 'finance_review'] as $legacyPrefix) { + unset($filters[$legacyPrefix . '_from'], $filters[$legacyPrefix . '_to']); + } + $filters[$prefix . '_from'] = $filters['date_from'] ?? null; + $filters[$prefix . '_to'] = $filters['date_to'] ?? null; + + return $filters; } private function applyFilters($query, array $filters, string $itemTable): void @@ -230,7 +326,7 @@ class InstallationDetailMirrorRepository if (!empty($filters[$prefix.'_from'])) $query->where('v.'.$column, '>=', $filters[$prefix.'_from']); if (!empty($filters[$prefix.'_to'])) $query->where('v.'.$column, '<', \Illuminate\Support\Carbon::parse($filters[$prefix.'_to'])->addDay()->toDateString()); } - foreach (['push_status'=>'apistatus','order_no'=>'OrderNum','quote_status'=>'BidStatus','fee_payer'=>'costtaker','shop_name'=>'shop_name','installation_company'=>'installCompanyName','installation_no'=>'ordernum_id','order_type'=>'TypeDto'] as $key=>$column) { + foreach (['push_status'=>'apistatus','order_no'=>'OrderNum','quote_status'=>'BidStatus','fee_payer'=>'costtaker','shop_name'=>'shop_name','shop_code'=>'shopcode','installation_company'=>'installCompanyName','installation_no'=>'ordernum_id','order_type'=>'TypeDto'] as $key=>$column) { if (isset($filters[$key]) && $filters[$key] !== '') $query->where('v.'.$column, $filters[$key]); } foreach (['customer_review'=>'kfsh','finance_review'=>'cwsh','completed_status'=>'installComplete','delete_status'=>'Delete'] as $key=>$column) { diff --git a/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php b/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php index 3435f0f1..e8668bef 100644 --- a/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php +++ b/modules/EcommerceFinance/Repositories/InternalFlowReportRepository.php @@ -47,12 +47,22 @@ class InternalFlowReportRepository /** * @param array{company_name?:string,tag?:string} $filters - * @return array{data: list>, total: int} + * @return array{data: list>, total: int, summary: array} */ public function paginate(int $page, int $perPage, array $filters = [], ?string $sort = null, string $direction = 'asc'): array { if (! $this->ready()) { - return ['data' => [], 'total' => 0]; + return [ + 'data' => [], + 'total' => 0, + 'summary' => [ + 'companies' => 0, + 'projected_sales' => 0.0, + 'receipt_total' => 0.0, + 'remaining_gap' => 0.0, + 'average_invoicing_ratio' => 0.0, + ], + ]; } $query = $this->connection->table(self::TABLE); @@ -68,6 +78,30 @@ class InternalFlowReportRepository } $total = (clone $query)->count(); + $totals = (clone $query)->selectRaw( + 'COUNT(*) AS companies, ' + . 'COALESCE(SUM(projected_sales), 0) AS projected_sales, ' + . 'COALESCE(SUM(receipt_total), 0) AS receipt_total, ' + . 'COALESCE(SUM(remaining_gap), 0) AS remaining_gap', + )->first(); + $ratios = (clone $query) + ->whereNotNull('invoicing_ratio') + ->pluck('invoicing_ratio') + ->map(static function (mixed $value): ?float { + $text = str_replace([',', ' ', '%'], '', trim((string) $value)); + if ($text === '' || $text === '/' || ! is_numeric($text)) { + return null; + } + + $ratio = (float) $text; + if (str_contains((string) $value, '%') || $ratio > 1) { + $ratio /= 100; + } + + return $ratio >= 0 ? $ratio : null; + }) + ->filter(static fn (?float $value): bool => $value !== null) + ->values(); $column = in_array($sort, self::SORTABLE, true) ? $sort : null; $direction = strtolower($direction) === 'desc' ? 'desc' : 'asc'; @@ -84,6 +118,16 @@ class InternalFlowReportRepository ->map(static fn (object $row): array => (array) $row) ->all(); - return ['data' => $rows, 'total' => $total]; + return [ + 'data' => $rows, + 'total' => $total, + 'summary' => [ + 'companies' => (int) ($totals->companies ?? $total), + 'projected_sales' => round((float) ($totals->projected_sales ?? 0), 2), + 'receipt_total' => round((float) ($totals->receipt_total ?? 0), 2), + 'remaining_gap' => round((float) ($totals->remaining_gap ?? 0), 2), + 'average_invoicing_ratio' => round((float) ($ratios->avg() ?? 0) * 100, 2), + ], + ]; } } diff --git a/resources/views/business/pages/partials/business-summary-cards.blade.php b/resources/views/business/pages/partials/business-summary-cards.blade.php index 7f8fe31a..a714bcb3 100644 --- a/resources/views/business/pages/partials/business-summary-cards.blade.php +++ b/resources/views/business/pages/partials/business-summary-cards.blade.php @@ -32,6 +32,49 @@ background: #fbf7ec; } + .business-summary-card[data-tone="accent"] { + border-color: #cdb985; + background: #fbf7ec; + } + + .business-summary-card[data-tone="success"] { + border-color: #b9c9ad; + background: #f3f8ef; + } + + .business-summary-card[data-tone="warning"] { + border-color: #ddc79e; + background: #fff8ea; + } + + .business-summary-card[data-tone="danger"] { + border-color: #dfbbbb; + background: #fff4f4; + } + + .business-summary-card[data-tone]::before { + position: absolute; + top: 0.7rem; + bottom: 0.7rem; + left: 0; + width: 3px; + border-radius: 0 999px 999px 0; + background: var(--vip-content-accent-dark, #856632); + content: ""; + } + + .business-summary-card[data-tone="success"]::before { + background: #53793e; + } + + .business-summary-card[data-tone="warning"]::before { + background: #935f07; + } + + .business-summary-card[data-tone="danger"]::before { + background: #b80000; + } + .business-summary-card:hover, .business-summary-card:focus-visible, .business-summary-card[aria-current="true"] { @@ -107,23 +150,28 @@ $format = $card['format'] ?? 'integer'; $formattedValue = match ($format) { 'money' => '¥' . number_format((float) ($card['value'] ?? 0), 2), + 'amount' => number_format((float) ($card['value'] ?? 0), 2), + 'percent' => number_format((float) ($card['value'] ?? 0), 2) . '%', 'text' => (string) ($card['value'] ?? '—'), default => number_format((int) ($card['value'] ?? 0)), }; + $tone = (string) ($card['tone'] ?? 'neutral'); + $isAmount = in_array($format, ['money', 'amount'], true); $activeQuery = collect($query) ->filter(fn ($value, string $key): bool => $key !== 'page' && $value !== null && $value !== ''); $isActive = $activeQuery->isNotEmpty() && $activeQuery->every(fn ($value, string $key): bool => (string) request($key) === (string) $value); @endphp {{ $card['label'] ?? '' }} - {{ $formattedValue }} + {{ $formattedValue }} {{ $card['hint'] ?? '' }} diff --git a/resources/views/business/pages/partials/date-range-picker.blade.php b/resources/views/business/pages/partials/date-range-picker.blade.php index 0de248c4..8dd024b2 100644 --- a/resources/views/business/pages/partials/date-range-picker.blade.php +++ b/resources/views/business/pages/partials/date-range-picker.blade.php @@ -286,6 +286,7 @@ }); input.addEventListener('click', open); + input.addEventListener('input', syncClear); input.addEventListener('keydown', function (event) { if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') { event.preventDefault(); diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index c744a5c9..304c64d7 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -584,6 +584,61 @@ text-align: right; } + .directory-value-badge { + display: inline-flex; + max-width: 12rem; + min-height: 1.45rem; + align-items: center; + justify-content: center; + padding: 0.15rem 0.48rem; + border: 1px solid #d8d2c7; + border-radius: 999px; + background: #f4f1eb; + color: #595959; + font-size: 0.6875rem; + line-height: 1.2; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: middle; + white-space: nowrap; + } + + .directory-value-badge[data-tone="accent"] { + border-color: #dcc99d; + background: #f7f0de; + color: #6a5129; + } + + .directory-value-badge[data-tone="success"] { + border-color: #bfd1b3; + background: #f1f7ed; + color: #53793e; + } + + .directory-value-badge[data-tone="warning"] { + border-color: #e1ca9c; + background: #fff7e7; + color: #935f07; + } + + .directory-value-badge[data-tone="danger"] { + border-color: #e5bcbc; + background: #fff1f1; + color: #b80000; + } + + .directory-value-badge[data-tone="info"] { + border-color: #b7ceda; + background: #eef5f8; + color: #006395; + } + + .directory-value-badge[data-tone="muted"] { + border-color: #ddd7ce; + background: #f3f1ed; + color: #68635c; + } + /* 快递运费:这几列是从来源表/快递应付带出来的金额与计算列,按业务要求表头和数值都靠左展示。 */ .directory-table-page .directory-table th.is-number[data-column-key="freight_amount"], .directory-table-page .directory-table td.is-number[data-column-key="freight_amount"], @@ -1367,6 +1422,12 @@ > @if ($directoryUsesFieldQuery) {{ $directory['status'] ?? '尚未同步' }} +
+
+ {{ $page['title'] ?? $section['title'] }} +

{{ $directoryQueryPageDescriptions[$directoryPageKey] ?? '按当前字段查询、筛选并导出业务数据' }}

+
+
@if (!empty($upstreamError))