Merge remote-tracking branch 'origin/main' into feature/yueyang-process-details
This commit is contained in:
commit
914fb0b4fe
182
app/Business/BusinessSummaryCards.php
Normal file
182
app/Business/BusinessSummaryCards.php
Normal file
@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Business;
|
||||
|
||||
/**
|
||||
* 经营报表页面顶部的小格子指标。
|
||||
*
|
||||
* 汇总卡片只描述当前表本身,不把其他目录的数据拼成跨表大看板。
|
||||
*/
|
||||
class BusinessSummaryCards
|
||||
{
|
||||
/**
|
||||
* @param array<string,mixed> $summary
|
||||
* @return list<array{label:string,value:mixed,hint:string,format:string,query:array<string,mixed>,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<string,mixed> $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<string,mixed> $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<string,mixed> $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<string,mixed>,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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@ -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<array<string,mixed>> $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<int,array<string,mixed>> $rows
|
||||
* @return array<int,string>
|
||||
*/
|
||||
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<string,mixed> $row @param array<string,string> $filters */
|
||||
|
||||
@ -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<string,int|float>}
|
||||
*/
|
||||
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
|
||||
|
||||
@ -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'],
|
||||
],
|
||||
|
||||
@ -175,7 +175,7 @@ SQL)
|
||||
->all();
|
||||
}
|
||||
|
||||
/** @return array<string,array{tax_type:?string,business_registration_type:?string}> */
|
||||
/** @return array<string,array{status:?string,tax_type:?string,business_registration_type:?string}> */
|
||||
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,
|
||||
]];
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -47,12 +47,22 @@ class InternalFlowReportRepository
|
||||
|
||||
/**
|
||||
* @param array{company_name?:string,tag?:string} $filters
|
||||
* @return array{data: list<array<string,mixed>>, total: int}
|
||||
* @return array{data: list<array<string,mixed>>, total: int, summary: array<string,int|float>}
|
||||
*/
|
||||
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),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
<a
|
||||
class="business-summary-card{{ $format === 'money' ? ' business-summary-card--money' : '' }}"
|
||||
class="business-summary-card{{ $isAmount ? ' business-summary-card--money' : '' }}"
|
||||
href="{{ rtrim(request()->fullUrlWithQuery($query), '?') }}"
|
||||
data-business-summary-card="{{ $card['label'] ?? '' }}"
|
||||
data-tone="{{ $tone }}"
|
||||
aria-label="{{ $card['label'] ?? '' }}:{{ $formattedValue }},查看明细"
|
||||
@if ($isActive) aria-current="true" @endif
|
||||
>
|
||||
<span class="business-summary-card__label">{{ $card['label'] ?? '' }}</span>
|
||||
<strong class="business-summary-card__value{{ $format === 'money' ? ' business-summary-card__value--money' : '' }}">{{ $formattedValue }}</strong>
|
||||
<strong class="business-summary-card__value{{ $isAmount ? ' business-summary-card__value--money' : '' }}">{{ $formattedValue }}</strong>
|
||||
<span class="business-summary-card__hint" title="{{ $card['hint'] ?? '' }}">{{ $card['hint'] ?? '' }}</span>
|
||||
<span class="material-icons-outlined business-summary-card__open" aria-hidden="true">arrow_outward</span>
|
||||
</a>
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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)
|
||||
<span class="sr-only" data-directory-status>{{ $directory['status'] ?? '尚未同步' }}</span>
|
||||
<div class="directory-query-page-head">
|
||||
<div>
|
||||
<strong>{{ $page['title'] ?? $section['title'] }}</strong>
|
||||
<p>{{ $directoryQueryPageDescriptions[$directoryPageKey] ?? '按当前字段查询、筛选并导出业务数据' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (!empty($upstreamError))
|
||||
<div class="directory-table-notice" role="alert">
|
||||
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
|
||||
@ -2733,7 +2794,7 @@
|
||||
<span class="directory-field-picker__hint">{{ $directoryHasGroupedHeader ? '两层合并表头按固定列顺序渲染,仅可切换显示字段。' : '拖动字段调整顺序;固定序号列不可隐藏。' }}</span>
|
||||
@foreach ($directoryFields as $field)
|
||||
<label class="directory-field-picker__option" draggable="true" data-directory-field-option>
|
||||
<input type="checkbox" value="{{ $field['name'] }}" data-directory-field-check @checked(!in_array($field['name'], ['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06'], true))>
|
||||
<input type="checkbox" value="{{ $field['name'] }}" data-directory-field-check @checked(in_array($field['name'], $visibleDirectoryColumns, true))>
|
||||
<span>{{ $field['label'] }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@ -2865,6 +2926,20 @@
|
||||
} elseif ($kind === 'boolean' && $value !== null && $value !== '') {
|
||||
$display = in_array($value, [true, 1, '1', '是', '✔'], true) ? '是' : '否';
|
||||
}
|
||||
$badgeTone = null;
|
||||
if ($kind === 'badge') {
|
||||
$badgeText = trim((string) ($display ?? ''));
|
||||
$badgeTone = (string) (($field['tones'] ?? [])[$badgeText] ?? 'neutral');
|
||||
if ($badgeTone === 'neutral') {
|
||||
$badgeTone = match (true) {
|
||||
in_array($badgeText, ['正常', '开店中', '存续中', '有效', '已完成', '已通过', '是'], true) => 'success',
|
||||
in_array($badgeText, ['已注销', '已关闭', '关店', '拒绝', '已拒绝'], true) => 'danger',
|
||||
in_array($badgeText, ['待维护', '关店中', '关闭中', '未启用', '未设置', '待处理', '待审批', '暂存'], true) => 'warning',
|
||||
in_array($badgeText, ['/'], true) => 'muted',
|
||||
default => 'neutral',
|
||||
};
|
||||
}
|
||||
}
|
||||
@endphp
|
||||
<td data-column-key="{{ $field['name'] }}" class="{{ in_array($kind, ['number', 'integer'], true) ? 'is-number' : '' }} {{ !in_array($field['name'], $visibleDirectoryColumns, true) ? 'is-column-hidden' : '' }}"@if (($page['slug'] ?? '') === 'courier-freight' && in_array($field['name'], ['transaction_no', 'express_no', 'barcode', 'shop_name'], true) && filled($display)) title="{{ $display }}"@endif>
|
||||
@if (($page['slug'] ?? '') === 'store-accounts' && $field['name'] === 'password_configured')
|
||||
@ -2882,7 +2957,11 @@
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
{{ filled($display) ? $display : ($isInventoryReportPage && $field['name'] === 'warehouse_short_name' ? '' : '—') }}
|
||||
@if ($kind === 'badge' && filled($display))
|
||||
<span class="directory-value-badge" data-tone="{{ $badgeTone }}">{{ $display }}</span>
|
||||
@else
|
||||
{{ filled($display) ? $display : ($isInventoryReportPage && $field['name'] === 'warehouse_short_name' ? '' : '—') }}
|
||||
@endif
|
||||
@endif
|
||||
</td>
|
||||
@endforeach
|
||||
|
||||
@ -1,47 +1,21 @@
|
||||
<style>
|
||||
.installation-fee-board {
|
||||
margin: 1rem 1.5rem;
|
||||
padding: 1rem;
|
||||
border: 1px solid #d8d2c7;
|
||||
border-radius: 0.75rem;
|
||||
background: #fcfbf8;
|
||||
}
|
||||
|
||||
.installation-fee-board__head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-fee-board__head strong {
|
||||
display: block;
|
||||
color: #171717;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.installation-fee-board__head span {
|
||||
display: block;
|
||||
margin-top: 0.2rem;
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.installation-fee-card {
|
||||
display: flex;
|
||||
width: min(100%, 15rem);
|
||||
min-height: 6.5rem;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 0.45rem;
|
||||
padding: 0.8rem 0.9rem;
|
||||
position: relative;
|
||||
display: block;
|
||||
width: min(100%, 52rem);
|
||||
min-height: 9.25rem;
|
||||
padding: 0.95rem 1.15rem 0.8rem 1.25rem;
|
||||
border: 1px solid #d8d2c7;
|
||||
border-radius: 0.5rem;
|
||||
border-left: 3px solid #006395;
|
||||
border-radius: 0.75rem;
|
||||
background: #ffffff;
|
||||
color: #171717;
|
||||
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.04);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -49,6 +23,7 @@
|
||||
.installation-fee-card:hover,
|
||||
.installation-fee-card:focus-visible {
|
||||
border-color: #b9954f;
|
||||
border-left-color: #006395;
|
||||
background: #fffdf8;
|
||||
outline: none;
|
||||
}
|
||||
@ -57,32 +32,125 @@
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.2);
|
||||
}
|
||||
|
||||
.installation-fee-card__label {
|
||||
color: #595959;
|
||||
.installation-fee-card__top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.installation-fee-card__title {
|
||||
flex: 0 0 auto;
|
||||
color: #171717;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.installation-fee-status {
|
||||
max-width: 31rem;
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.45;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.installation-fee-status.is-error {
|
||||
color: #b80000;
|
||||
}
|
||||
|
||||
.installation-fee-card__main {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
min-width: 0;
|
||||
margin: 0.35rem 0 0.58rem;
|
||||
}
|
||||
|
||||
.installation-fee-card__value {
|
||||
color: #171717;
|
||||
font-size: 1.25rem;
|
||||
font-size: 2.625rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -0.035em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.installation-fee-card__value small {
|
||||
margin-left: 0.15rem;
|
||||
.installation-fee-card__unit {
|
||||
color: #595959;
|
||||
font-size: 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.installation-fee-card__action {
|
||||
color: #856632;
|
||||
font-size: 0.6875rem;
|
||||
.installation-fee-card__trend {
|
||||
margin-left: 0.4rem;
|
||||
color: #595959;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.installation-fee-status {
|
||||
.installation-fee-card__trend.is-up {
|
||||
color: #935f07;
|
||||
}
|
||||
|
||||
.installation-fee-card__trend.is-down {
|
||||
color: #53793e;
|
||||
}
|
||||
|
||||
.installation-fee-card__trend-note {
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.installation-fee-card__split {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
flex-wrap: wrap;
|
||||
padding-top: 0.55rem;
|
||||
border-top: 1px solid #e5e0d7;
|
||||
color: #171717;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.installation-fee-card__split strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.installation-fee-card__split-percent {
|
||||
margin-left: 0.1rem;
|
||||
color: #595959;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.installation-fee-card__split-sep {
|
||||
color: #d8d2c7;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.installation-fee-card {
|
||||
min-height: 0;
|
||||
padding: 0.9rem 0.95rem 0.8rem 1rem;
|
||||
}
|
||||
.installation-fee-card__top {
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.installation-fee-status {
|
||||
max-width: none;
|
||||
text-align: left;
|
||||
}
|
||||
.installation-fee-card__main {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.installation-fee-card__value {
|
||||
font-size: 2.625rem;
|
||||
}
|
||||
}
|
||||
|
||||
.installation-fee-dialog {
|
||||
@ -283,23 +351,29 @@
|
||||
</style>
|
||||
|
||||
<section class="installation-fee-board" aria-label="安装费金额汇总">
|
||||
<div class="installation-fee-board__head">
|
||||
<div>
|
||||
<strong>安装费金额</strong>
|
||||
<span>按客服审核时间汇总安装明细的结算明细金额;收款渠道为空归为包安装,有值归为不包安装。</span>
|
||||
</div>
|
||||
<span class="installation-fee-status" data-installation-fee-status>请选择完整的客服审核起止日期</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="installation-fee-card"
|
||||
data-installation-fee-open
|
||||
data-url="{{ route('business.installation-details.summary') }}"
|
||||
aria-label="查看安装费金额明细"
|
||||
title="查看安装费金额明细"
|
||||
>
|
||||
<span class="installation-fee-card__label">安装费金额</span>
|
||||
<strong class="installation-fee-card__value"><span data-installation-fee-total>0.00</span><small>万</small></strong>
|
||||
<span class="installation-fee-card__action">查看明细 →</span>
|
||||
<span class="installation-fee-card__top">
|
||||
<strong class="installation-fee-card__title">安装费金额</strong>
|
||||
<span class="installation-fee-status" data-installation-fee-status aria-live="polite">请选择完整的客服审核起止日期</span>
|
||||
</span>
|
||||
<span class="installation-fee-card__main">
|
||||
<strong class="installation-fee-card__value" data-installation-fee-total>0.00</strong>
|
||||
<span class="installation-fee-card__unit">万元</span>
|
||||
<span class="installation-fee-card__trend" data-installation-fee-trend>—</span>
|
||||
<span class="installation-fee-card__trend-note">较上月</span>
|
||||
</span>
|
||||
<span class="installation-fee-card__split">
|
||||
<span>包安装 <strong data-installation-fee-package>0.00</strong><span class="installation-fee-card__split-percent" data-installation-fee-package-percent>(0%)</span></span>
|
||||
<span class="installation-fee-card__split-sep" aria-hidden="true">|</span>
|
||||
<span>不包安装 <strong data-installation-fee-non-package>0.00</strong><span class="installation-fee-card__split-percent" data-installation-fee-non-package-percent>(0%)</span></span>
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
@ -381,10 +455,15 @@
|
||||
var dialog = document.getElementById('installation-fee-dialog');
|
||||
if (!form || !card || !dialog) { return; }
|
||||
|
||||
var fromInput = form.elements.namedItem('customer_review_from');
|
||||
var toInput = form.elements.namedItem('customer_review_to');
|
||||
var fromInput = form.elements.namedItem('date_from');
|
||||
var toInput = form.elements.namedItem('date_to');
|
||||
var cardTotal = card.querySelector('[data-installation-fee-total]');
|
||||
var status = document.querySelector('[data-installation-fee-status]');
|
||||
var status = card.querySelector('[data-installation-fee-status]');
|
||||
var trend = card.querySelector('[data-installation-fee-trend]');
|
||||
var packageAmount = card.querySelector('[data-installation-fee-package]');
|
||||
var packagePercent = card.querySelector('[data-installation-fee-package-percent]');
|
||||
var nonPackageAmount = card.querySelector('[data-installation-fee-non-package]');
|
||||
var nonPackagePercent = card.querySelector('[data-installation-fee-non-package-percent]');
|
||||
var subtitle = dialog.querySelector('[data-installation-fee-subtitle]');
|
||||
var currentTotal = dialog.querySelector('[data-installation-fee-current-total]');
|
||||
var platformFilter = dialog.querySelector('[data-installation-fee-platform]');
|
||||
@ -414,6 +493,78 @@
|
||||
});
|
||||
}
|
||||
|
||||
function amount(value) {
|
||||
return Number(value || 0).toLocaleString('zh-CN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
function captionRange(value) {
|
||||
var matchFrom = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.from);
|
||||
var matchTo = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value.to);
|
||||
if (!matchFrom || !matchTo) {
|
||||
return value.from + ' 至 ' + value.to;
|
||||
}
|
||||
var end = matchFrom[1] === matchTo[1]
|
||||
? matchTo[2] + '-' + matchTo[3]
|
||||
: value.to;
|
||||
return value.from + ' 至 ' + end;
|
||||
}
|
||||
|
||||
function setTrend(changeRate) {
|
||||
trend.className = 'installation-fee-card__trend';
|
||||
if (changeRate === null || changeRate === undefined || !Number.isFinite(Number(changeRate))) {
|
||||
trend.textContent = '—';
|
||||
return;
|
||||
}
|
||||
|
||||
var rate = Number(changeRate);
|
||||
if (rate > 0) {
|
||||
trend.textContent = '↑' + Math.abs(rate).toLocaleString('zh-CN', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%';
|
||||
trend.classList.add('is-up');
|
||||
} else if (rate < 0) {
|
||||
trend.textContent = '↓' + Math.abs(rate).toLocaleString('zh-CN', { minimumFractionDigits: 1, maximumFractionDigits: 1 }) + '%';
|
||||
trend.classList.add('is-down');
|
||||
} else {
|
||||
trend.textContent = '持平';
|
||||
}
|
||||
}
|
||||
|
||||
function setCardValue(total, rows, value, data) {
|
||||
var packageTotal = 0;
|
||||
var nonPackageTotal = 0;
|
||||
rows.forEach(function (row) {
|
||||
if (row.payment_channel === '包安装') {
|
||||
packageTotal += Number(row.amount || 0);
|
||||
} else {
|
||||
nonPackageTotal += Number(row.amount || 0);
|
||||
}
|
||||
});
|
||||
var packageRate = total > 0 ? Math.round((packageTotal / total) * 100) : 0;
|
||||
var nonPackageRate = total > 0 ? 100 - packageRate : 0;
|
||||
|
||||
cardTotal.textContent = wan(total);
|
||||
packageAmount.textContent = amount(packageTotal);
|
||||
nonPackageAmount.textContent = amount(nonPackageTotal);
|
||||
packagePercent.textContent = '(' + packageRate + '%)';
|
||||
nonPackagePercent.textContent = '(' + nonPackageRate + '%)';
|
||||
setTrend(data.change_rate);
|
||||
status.classList.remove('is-error');
|
||||
status.textContent = '按客服审核时间 · ' + captionRange(value) + ' · ' + rows.length + ' 单';
|
||||
}
|
||||
|
||||
function resetCard(message) {
|
||||
cardTotal.textContent = '0.00';
|
||||
packageAmount.textContent = '0.00';
|
||||
nonPackageAmount.textContent = '0.00';
|
||||
packagePercent.textContent = '(0%)';
|
||||
nonPackagePercent.textContent = '(0%)';
|
||||
setTrend(null);
|
||||
status.textContent = message;
|
||||
status.classList.toggle('is-error', message.indexOf('失败') >= 0 || message.indexOf('不能') >= 0);
|
||||
}
|
||||
|
||||
function range() {
|
||||
return {
|
||||
from: String(fromInput?.value || ''),
|
||||
@ -542,14 +693,12 @@
|
||||
function refresh(force) {
|
||||
var value = range();
|
||||
if (!value.from || !value.to) {
|
||||
cardTotal.textContent = '0.00';
|
||||
status.textContent = '请选择完整的客服审核起止日期';
|
||||
resetCard('请选择完整的客服审核起止日期');
|
||||
resetDialog('请选择完整的客服审核起止日期');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
if (value.from > value.to) {
|
||||
cardTotal.textContent = '0.00';
|
||||
status.textContent = '客服审核结束日期不能早于开始日期';
|
||||
resetCard('客服审核结束日期不能早于开始日期');
|
||||
resetDialog('客服审核结束日期不能早于开始日期');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
@ -561,7 +710,8 @@
|
||||
if (controller) { controller.abort(); }
|
||||
controller = new AbortController();
|
||||
dialog.setAttribute('aria-busy', 'true');
|
||||
status.textContent = '正在汇总安装费…';
|
||||
status.classList.remove('is-error');
|
||||
status.textContent = '按客服审核时间 · 正在汇总…';
|
||||
cardTotal.textContent = '…';
|
||||
var url = new URL(card.dataset.url, window.location.origin);
|
||||
url.searchParams.set('customer_review_from', value.from);
|
||||
@ -581,8 +731,7 @@
|
||||
}).then(function (data) {
|
||||
allRows = Array.isArray(data.rows) ? data.rows : [];
|
||||
loadedSignature = currentSignature;
|
||||
cardTotal.textContent = wan(data.total);
|
||||
status.textContent = value.from + ' 至 ' + value.to + ' · ' + allRows.length + ' 条汇总';
|
||||
setCardValue(Number(data.total || 0), allRows, value, data);
|
||||
subtitle.textContent = value.from + ' 至 ' + value.to + ' · 按客服审核时间汇总';
|
||||
fillFilter(platformFilter, '全部平台', distinct('platform'));
|
||||
fillFilter(storeFilter, '全部店铺', distinct('store_name'));
|
||||
@ -593,8 +742,7 @@
|
||||
return true;
|
||||
}).catch(function (error) {
|
||||
if (error && error.name === 'AbortError') { return false; }
|
||||
cardTotal.textContent = '0.00';
|
||||
status.textContent = error.message || '安装费汇总读取失败';
|
||||
resetCard(error.message || '安装费汇总读取失败');
|
||||
resetDialog(error.message || '安装费汇总读取失败,请稍后重试。');
|
||||
dialog.removeAttribute('aria-busy');
|
||||
return false;
|
||||
|
||||
@ -0,0 +1,320 @@
|
||||
@php
|
||||
$installationTimeScopes = [
|
||||
'created' => '创建时间',
|
||||
'customer_review' => '客服审核时间',
|
||||
'finance_review' => '财务审核时间',
|
||||
];
|
||||
$installationTimePrefixes = [
|
||||
'created' => 'created',
|
||||
'customer_review' => 'customer_review',
|
||||
'finance_review' => 'finance_review',
|
||||
];
|
||||
$usesUnifiedTime = request()->hasAny(['time_scope', 'date_from', 'date_to']);
|
||||
$installationTimeScope = (string) request('time_scope', '');
|
||||
if (! array_key_exists($installationTimeScope, $installationTimeScopes)) {
|
||||
$installationTimeScope = request()->filled('created_from') || request()->filled('created_to')
|
||||
? 'created'
|
||||
: (request()->filled('finance_review_from') || request()->filled('finance_review_to')
|
||||
? 'finance_review'
|
||||
: 'customer_review');
|
||||
}
|
||||
$installationTimePrefix = $installationTimePrefixes[$installationTimeScope];
|
||||
$installationDateFrom = $usesUnifiedTime
|
||||
? (string) request('date_from', '')
|
||||
: (string) request($installationTimePrefix . '_from', '');
|
||||
$installationDateTo = $usesUnifiedTime
|
||||
? (string) request('date_to', '')
|
||||
: (string) request($installationTimePrefix . '_to', '');
|
||||
$installationStore = (string) request('shop_name', '');
|
||||
$installationCompleted = (string) request('completed_status', '');
|
||||
$installationMoreFields = [
|
||||
'shop_code' => ['label' => '店铺 ID', 'placeholder' => '按店铺编码精确查询'],
|
||||
'order_no' => ['label' => '订单编号', 'placeholder' => '按原值精确查询'],
|
||||
'installation_no' => ['label' => '安装单号', 'placeholder' => '按原值精确查询'],
|
||||
'installation_company' => ['label' => '安装公司', 'placeholder' => '按原值精确查询'],
|
||||
'push_status' => ['label' => '推送状态', 'placeholder' => '按原值精确查询'],
|
||||
'quote_status' => ['label' => '报价状态', 'placeholder' => '按原值精确查询'],
|
||||
'fee_payer' => ['label' => '安装费承担方', 'placeholder' => '按原值精确查询'],
|
||||
'order_type' => ['label' => '订单类型', 'placeholder' => '按原值精确查询'],
|
||||
'product_id' => ['label' => '商品 ID', 'placeholder' => '按原值精确查询'],
|
||||
'customer_review' => ['label' => '客服审核', 'options' => ['0' => '未审核', '1' => '已审核']],
|
||||
'finance_review' => ['label' => '财务审核', 'options' => ['0' => '未审核', '1' => '已审核']],
|
||||
'arranged_status' => ['label' => '是否安排', 'options' => ['0' => '未安排', '1' => '已安排']],
|
||||
'delete_status' => ['label' => '删除状态', 'options' => ['0' => '未删除', '1' => '已删除']],
|
||||
];
|
||||
$installationActiveMore = collect($installationMoreFields)
|
||||
->filter(static fn (array $field, string $name): bool => request()->filled($name))
|
||||
->map(static fn (array $field, string $name): array => $field + [
|
||||
'name' => $name,
|
||||
'value' => (string) request($name, ''),
|
||||
])
|
||||
->all();
|
||||
$installationMoreCount = count($installationActiveMore);
|
||||
$installationQuery = request()->query();
|
||||
$installationRemoveUrl = static function (array $keys) use ($installationQuery): string {
|
||||
unset($installationQuery['page']);
|
||||
foreach ($keys as $key) {
|
||||
unset($installationQuery[$key]);
|
||||
}
|
||||
|
||||
$query = \Illuminate\Support\Arr::query($installationQuery);
|
||||
|
||||
return request()->url() . ($query !== '' ? '?' . $query : '');
|
||||
};
|
||||
$installationDateChip = trim($installationDateFrom . ' 至 ' . $installationDateTo, ' 至');
|
||||
@endphp
|
||||
|
||||
<form method="GET" id="installationFilterForm" action="{{ request()->url() }}" class="installation-filter-bar installation-filter-compact" aria-label="安装明细筛选查询栏" autocomplete="off">
|
||||
<div class="installation-filter-primary">
|
||||
<div class="installation-filter-time">
|
||||
<label class="installation-filter-control">
|
||||
<span>时间口径</span>
|
||||
<select id="installationTimeScope" name="time_scope">
|
||||
@foreach ($installationTimeScopes as $value => $label)
|
||||
<option value="{{ $value }}" @selected($installationTimeScope === $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</label>
|
||||
<div class="installation-filter-control installation-filter-range">
|
||||
<span>日期区间</span>
|
||||
<div class="upstream-data-date-range">
|
||||
<span class="upstream-data-date" data-date-picker>
|
||||
<input id="date_from" type="text" name="date_from" value="{{ $installationDateFrom }}" readonly autocomplete="off" placeholder="开始日期" aria-label="开始日期" aria-describedby="installationDateRangeError" data-date-input>
|
||||
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除开始日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择开始日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
|
||||
</button>
|
||||
<div class="upstream-data-calendar" role="dialog" aria-label="开始日期日历" data-date-calendar hidden></div>
|
||||
</span>
|
||||
<b class="upstream-data-date-range__sep">至</b>
|
||||
<span class="upstream-data-date" data-date-picker>
|
||||
<input id="date_to" type="text" name="date_to" value="{{ $installationDateTo }}" readonly autocomplete="off" placeholder="结束日期" aria-label="结束日期" aria-describedby="installationDateRangeError" data-date-input>
|
||||
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除结束日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择结束日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
|
||||
</button>
|
||||
<div class="upstream-data-calendar" role="dialog" aria-label="结束日期日历" data-date-calendar hidden></div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="installation-filter-control">
|
||||
<span>店铺</span>
|
||||
<input id="installationShopName" type="search" name="shop_name" value="{{ $installationStore }}" placeholder="输入完整店铺名称">
|
||||
</label>
|
||||
|
||||
<label class="installation-filter-control">
|
||||
<span>安装状态</span>
|
||||
<select name="completed_status">
|
||||
<option value="">全部</option>
|
||||
<option value="0" @selected($installationCompleted === '0')>未完工</option>
|
||||
<option value="1" @selected($installationCompleted === '1')>已完工</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<div class="installation-filter-actions">
|
||||
<button type="submit" class="biz-btn biz-btn-primary">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<span>查询</span>
|
||||
</button>
|
||||
<button type="button" class="biz-btn biz-btn-ghost installation-more-toggle" data-installation-more-toggle aria-expanded="{{ $installationMoreCount > 0 ? 'true' : 'false' }}" aria-controls="installationMoreFilters">
|
||||
<span class="material-icons-outlined" aria-hidden="true">tune</span>
|
||||
<span>更多筛选</span>
|
||||
@if ($installationMoreCount > 0)
|
||||
<b data-installation-more-count>{{ $installationMoreCount }}</b>
|
||||
@endif
|
||||
</button>
|
||||
<a href="{{ request()->url() }}" class="biz-btn biz-btn-ghost">清除</a>
|
||||
<a href="{{ route('business.installation-details.export', request()->query()) }}" class="biz-btn biz-btn-ghost">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>
|
||||
<span>导出</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="installation-filter-context">
|
||||
<div class="installation-quick-ranges" role="group" aria-label="快捷日期区间">
|
||||
<span>快捷区间</span>
|
||||
@foreach (['today' => '今天', 'this_week' => '本周', 'this_month' => '本月', 'last_month' => '上月', 'last_30_days' => '近30天'] as $value => $label)
|
||||
<button type="button" class="installation-quick-range" data-installation-quick-range="{{ $value }}">{{ $label }}</button>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="installation-active-filters" aria-label="当前生效的筛选条件">
|
||||
@if ($installationDateFrom !== '' || $installationDateTo !== '')
|
||||
<a class="installation-filter-chip" href="{{ $installationRemoveUrl(['time_scope', 'date_from', 'date_to', 'created_from', 'created_to', 'customer_review_from', 'customer_review_to', 'finance_review_from', 'finance_review_to']) }}" aria-label="删除「{{ $installationTimeScopes[$installationTimeScope] }}」条件">
|
||||
<span>{{ $installationTimeScopes[$installationTimeScope] }}</span>
|
||||
<strong>{{ $installationDateChip }}</strong>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</a>
|
||||
@endif
|
||||
@if ($installationStore !== '')
|
||||
<a class="installation-filter-chip" href="{{ $installationRemoveUrl(['shop_name']) }}" aria-label="删除「店铺」条件">
|
||||
<span>店铺</span><strong>{{ $installationStore }}</strong>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</a>
|
||||
@endif
|
||||
@if ($installationCompleted !== '')
|
||||
<a class="installation-filter-chip" href="{{ $installationRemoveUrl(['completed_status']) }}" aria-label="删除「安装状态」条件">
|
||||
<span>安装状态</span><strong>{{ $installationCompleted === '1' ? '已完工' : '未完工' }}</strong>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</a>
|
||||
@endif
|
||||
@foreach ($installationActiveMore as $field)
|
||||
@php
|
||||
$displayValue = isset($field['options'])
|
||||
? ($field['options'][$field['value']] ?? $field['value'])
|
||||
: $field['value'];
|
||||
@endphp
|
||||
<a class="installation-filter-chip" href="{{ $installationRemoveUrl([$field['name']]) }}" aria-label="删除「{{ $field['label'] }}」条件">
|
||||
<span>{{ $field['label'] }}</span><strong>{{ $displayValue }}</strong>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p id="installationDateRangeError" class="installation-date-range-error" role="alert" hidden>结束日期不能早于开始日期。</p>
|
||||
|
||||
<div id="installationMoreFilters" class="installation-more-panel" @if ($installationMoreCount === 0) hidden @endif>
|
||||
<div class="installation-more-head">
|
||||
<strong>更多筛选</strong>
|
||||
<span>低频条件仅在展开后显示,已生效条件会保留在下方标签中。</span>
|
||||
</div>
|
||||
<div class="installation-more-grid">
|
||||
@foreach ($installationMoreFields as $name => $field)
|
||||
<label class="installation-filter-control">
|
||||
<span>{{ $field['label'] }}</span>
|
||||
@if (isset($field['options']))
|
||||
<select name="{{ $name }}">
|
||||
<option value="">全部</option>
|
||||
@foreach ($field['options'] as $value => $label)
|
||||
<option value="{{ $value }}" @selected((string) request($name) === (string) $value)>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@else
|
||||
<input id="installation-more-{{ $name }}" type="text" name="{{ $name }}" value="{{ request($name) }}" placeholder="{{ $field['placeholder'] }}">
|
||||
@endif
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('installationFilterForm');
|
||||
if (!form) { return; }
|
||||
|
||||
if (window.CaiWuDatePicker) {
|
||||
window.CaiWuDatePicker.setup(form);
|
||||
}
|
||||
|
||||
var fromInput = form.elements.namedItem('date_from');
|
||||
var toInput = form.elements.namedItem('date_to');
|
||||
var error = document.getElementById('installationDateRangeError');
|
||||
var moreToggle = form.querySelector('[data-installation-more-toggle]');
|
||||
var morePanel = document.getElementById('installationMoreFilters');
|
||||
var quickButtons = Array.from(form.querySelectorAll('[data-installation-quick-range]'));
|
||||
|
||||
function pad2(number) {
|
||||
return (number < 10 ? '0' : '') + number;
|
||||
}
|
||||
|
||||
function isoDate(date) {
|
||||
return date.getFullYear() + '-' + pad2(date.getMonth() + 1) + '-' + pad2(date.getDate());
|
||||
}
|
||||
|
||||
function startOfWeek(date) {
|
||||
var start = new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
||||
var offset = (start.getDay() + 6) % 7;
|
||||
start.setDate(start.getDate() - offset);
|
||||
return start;
|
||||
}
|
||||
|
||||
function quickRange(name) {
|
||||
var today = new Date();
|
||||
today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
||||
var from;
|
||||
var to = today;
|
||||
|
||||
if (name === 'this_week') {
|
||||
from = startOfWeek(today);
|
||||
} else if (name === 'this_month') {
|
||||
from = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
} else if (name === 'last_month') {
|
||||
from = new Date(today.getFullYear(), today.getMonth() - 1, 1);
|
||||
to = new Date(today.getFullYear(), today.getMonth(), 0);
|
||||
} else if (name === 'last_30_days') {
|
||||
from = new Date(today.getFullYear(), today.getMonth(), today.getDate() - 29);
|
||||
} else {
|
||||
from = today;
|
||||
}
|
||||
|
||||
return { from: isoDate(from), to: isoDate(to) };
|
||||
}
|
||||
|
||||
function syncQuickState() {
|
||||
quickButtons.forEach(function (button) {
|
||||
var value = quickRange(button.getAttribute('data-installation-quick-range'));
|
||||
var selected = fromInput.value === value.from && toInput.value === value.to;
|
||||
button.classList.toggle('is-active', selected);
|
||||
button.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
function setRange(from, to) {
|
||||
fromInput.value = from;
|
||||
toInput.value = to;
|
||||
[fromInput, toInput].forEach(function (input) {
|
||||
input.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
input.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
});
|
||||
syncQuickState();
|
||||
form.querySelector('button[type="submit"]')?.focus();
|
||||
}
|
||||
|
||||
moreToggle?.addEventListener('click', function () {
|
||||
var open = morePanel.hasAttribute('hidden');
|
||||
morePanel.toggleAttribute('hidden', !open);
|
||||
moreToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
|
||||
});
|
||||
|
||||
quickButtons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var value = quickRange(button.getAttribute('data-installation-quick-range'));
|
||||
setRange(value.from, value.to);
|
||||
});
|
||||
});
|
||||
|
||||
[fromInput, toInput].forEach(function (input) {
|
||||
input.addEventListener('change', syncQuickState);
|
||||
input.addEventListener('input', syncQuickState);
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
var invalid = Boolean(fromInput.value && toInput.value && fromInput.value > toInput.value);
|
||||
fromInput.setAttribute('aria-invalid', invalid ? 'true' : 'false');
|
||||
toInput.setAttribute('aria-invalid', invalid ? 'true' : 'false');
|
||||
error.hidden = !invalid;
|
||||
if (invalid) {
|
||||
event.preventDefault();
|
||||
toInput.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
Array.from(form.querySelectorAll('input[name], select[name]')).forEach(function (control) {
|
||||
if (!control.disabled && String(control.value || '').trim() === '') {
|
||||
control.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
syncQuickState();
|
||||
})();
|
||||
</script>
|
||||
@ -203,12 +203,10 @@
|
||||
}
|
||||
|
||||
.installation-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(12rem, 1fr));
|
||||
gap: 0.65rem 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
background: #fcfbf8;
|
||||
padding: 0.7rem 1.25rem 0.75rem;
|
||||
border-bottom: 1px solid #e8e2d8;
|
||||
background: #fffdf8;
|
||||
color: #595959;
|
||||
}
|
||||
|
||||
.installation-filter-field {
|
||||
@ -238,34 +236,207 @@
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-date-filter {
|
||||
display: flex;
|
||||
grid-column: span 2;
|
||||
.installation-filter-compact {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.installation-filter-primary {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
minmax(28rem, 1.55fr)
|
||||
minmax(10.5rem, 0.62fr)
|
||||
minmax(8.5rem, 0.42fr)
|
||||
auto;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.installation-filter-time {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(7rem, 0.45fr) minmax(17rem, 1.4fr);
|
||||
gap: 0.55rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.installation-filter-control {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.22rem;
|
||||
color: #595959;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-filter-control > span {
|
||||
height: 0.95rem;
|
||||
overflow: hidden;
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
font-weight: 500;
|
||||
line-height: 0.95rem;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.installation-date-filter__label {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.installation-date-filter__range {
|
||||
flex: 1 1 auto;
|
||||
.installation-filter-control input,
|
||||
.installation-filter-control select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.installation-date-filter .upstream-data-date input {
|
||||
height: 2.1rem;
|
||||
height: 2rem;
|
||||
padding: 0 0.55rem;
|
||||
border: 1px solid #d8d2c7;
|
||||
border-radius: 0.45rem;
|
||||
background: #ffffff;
|
||||
color: #171717;
|
||||
font: inherit;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-filter-control input:focus,
|
||||
.installation-filter-control select:focus {
|
||||
border-color: #856632;
|
||||
box-shadow: 0 0 0 3px rgba(133, 102, 50, 0.16);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.installation-filter-range .upstream-data-date-range {
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.installation-filter-range .upstream-data-date input {
|
||||
height: 2rem;
|
||||
border: 1px solid #d8d2c7;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-filter-range .upstream-data-date__clear,
|
||||
.installation-filter-range .upstream-data-date__pick {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.installation-filter-actions {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: flex-end;
|
||||
gap: 0.35rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.installation-filter-actions .biz-btn {
|
||||
height: 2rem;
|
||||
gap: 0.25rem;
|
||||
padding: 0 0.62rem;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.installation-filter-actions .material-icons-outlined {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.installation-more-toggle b {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.05rem;
|
||||
height: 1.05rem;
|
||||
padding: 0 0.2rem;
|
||||
border-radius: 999px;
|
||||
background: #856632;
|
||||
color: #ffffff;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.installation-more-toggle b[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.installation-filter-context {
|
||||
display: flex;
|
||||
min-height: 1.65rem;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 0.55rem;
|
||||
}
|
||||
|
||||
.installation-quick-ranges,
|
||||
.installation-active-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.installation-quick-ranges > span {
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.installation-quick-range {
|
||||
height: 1.55rem;
|
||||
padding: 0 0.55rem;
|
||||
border: 1px solid #ddd6ca;
|
||||
border-radius: 999px;
|
||||
background: #ffffff;
|
||||
color: #595959;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.installation-quick-range:hover,
|
||||
.installation-quick-range:focus-visible,
|
||||
.installation-quick-range.is-active {
|
||||
border-color: #b9954f;
|
||||
background: #f7f0de;
|
||||
color: #856632;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.installation-active-filters {
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.installation-filter-chip {
|
||||
display: inline-flex;
|
||||
max-width: 24rem;
|
||||
min-height: 1.55rem;
|
||||
align-items: center;
|
||||
gap: 0.28rem;
|
||||
padding: 0.08rem 0.35rem 0.08rem 0.55rem;
|
||||
border: 1px solid #e2d6bb;
|
||||
border-radius: 999px;
|
||||
background: #f7f0de;
|
||||
color: #6a5129;
|
||||
font-size: 0.6875rem;
|
||||
line-height: 1.2;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.installation-filter-chip:hover,
|
||||
.installation-filter-chip:focus-visible {
|
||||
border-color: #b9954f;
|
||||
background: #f2e7c5;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.installation-filter-chip strong {
|
||||
overflow: hidden;
|
||||
max-width: 15rem;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.installation-filter-chip .material-icons-outlined {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.installation-date-range-error {
|
||||
grid-column: 1 / -1;
|
||||
margin: 0;
|
||||
margin: 0.4rem 0 0;
|
||||
color: #b80000;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
@ -274,22 +445,92 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.installation-filter-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
.installation-more-panel {
|
||||
margin-top: 0.6rem;
|
||||
padding: 0.65rem 0.75rem 0.75rem;
|
||||
border: 1px solid #e7dfd1;
|
||||
border-radius: 0.55rem;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.installation-filter-bar { grid-template-columns: repeat(2, minmax(14rem, 1fr)); }
|
||||
.installation-date-filter { grid-column: 1 / -1; }
|
||||
.installation-more-panel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.installation-more-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.55rem;
|
||||
}
|
||||
|
||||
.installation-more-head strong {
|
||||
color: #171717;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.installation-more-head span {
|
||||
color: #595959;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
|
||||
.installation-more-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(9rem, 1fr));
|
||||
gap: 0.55rem 0.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.installation-filter-primary {
|
||||
grid-template-columns: minmax(27rem, 1fr) minmax(10rem, 0.55fr) minmax(8rem, 0.4fr);
|
||||
}
|
||||
.installation-filter-actions {
|
||||
grid-column: 1 / -1;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.installation-more-grid {
|
||||
grid-template-columns: repeat(3, minmax(9rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.installation-filter-primary {
|
||||
grid-template-columns: minmax(0, 1fr) minmax(11rem, 0.55fr);
|
||||
}
|
||||
.installation-filter-time {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.installation-filter-actions {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
.installation-more-grid {
|
||||
grid-template-columns: repeat(2, minmax(9rem, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
.installation-filter-bar { grid-template-columns: minmax(0, 1fr); padding: 1rem; }
|
||||
.installation-date-filter {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 0.25rem;
|
||||
.installation-filter-bar {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
.installation-filter-primary,
|
||||
.installation-filter-time {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
.installation-filter-time,
|
||||
.installation-filter-actions {
|
||||
grid-column: auto;
|
||||
}
|
||||
.installation-filter-actions {
|
||||
justify-content: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.installation-active-filters {
|
||||
margin-left: 0;
|
||||
}
|
||||
.installation-more-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,102 +1014,7 @@
|
||||
<div class="biz-card mt-6">
|
||||
@if ($page['slug'] === 'installation-details')
|
||||
@include('business.pages.partials.date-range-picker')
|
||||
<form method="GET" id="installationFilterForm" action="{{ request()->url() }}" class="installation-filter-bar" aria-label="安装明细筛选查询栏" autocomplete="off">
|
||||
@php
|
||||
$dateRangeFilters = [
|
||||
['from' => 'created_from', 'to' => 'created_to', 'label' => '创建日期:', 'aria' => '创建日期'],
|
||||
['from' => 'customer_review_from', 'to' => 'customer_review_to', 'label' => '客服审核时间:', 'aria' => '客服审核时间'],
|
||||
['from' => 'finance_review_from', 'to' => 'finance_review_to', 'label' => '财务审核时间:', 'aria' => '财务审核时间'],
|
||||
];
|
||||
$filterFields = [
|
||||
['name' => 'push_status', 'label' => '推送状态:'], ['name' => 'order_no', 'label' => '订单编号:'],
|
||||
['name' => 'customer_review', 'label' => '客服审核:', 'options' => ['0'=>'未审核','1'=>'已审核']], ['name' => 'finance_review', 'label' => '财务审核:', 'options' => ['0'=>'未审核','1'=>'已审核']],
|
||||
['name' => 'quote_status', 'label' => '报价状态:'], ['name' => 'arranged_status', 'label' => '是否安排:', 'options' => ['0'=>'未安排','1'=>'已安排']],
|
||||
['name' => 'fee_payer', 'label' => '安装费承担方:'], ['name' => 'completed_status', 'label' => '是否完工:', 'options' => ['0'=>'未完工','1'=>'已完工']],
|
||||
['name' => 'shop_name', 'label' => '选择店铺:'], ['name' => 'delete_status', 'label' => '选择删除:', 'options' => ['0'=>'未删除','1'=>'已删除']],
|
||||
['name' => 'installation_company', 'label' => '安装公司:'], ['name' => 'installation_no', 'label' => '安装单号:'],
|
||||
['name' => 'order_type', 'label' => '订单类型:'], ['name' => 'product_id', 'label' => '商品ID:'],
|
||||
];
|
||||
@endphp
|
||||
@foreach ($dateRangeFilters as $dateRange)
|
||||
<div class="installation-date-filter">
|
||||
<span class="installation-date-filter__label">{{ $dateRange['label'] }}</span>
|
||||
<div class="upstream-data-date-range installation-date-filter__range">
|
||||
<span class="upstream-data-date" data-date-picker>
|
||||
<input id="{{ $dateRange['from'] }}" type="text" name="{{ $dateRange['from'] }}" value="{{ request($dateRange['from']) }}" readonly autocomplete="off" placeholder="开始日期" aria-label="{{ $dateRange['aria'] }}开始日期" aria-describedby="installationDateRangeError" data-date-input>
|
||||
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除{{ $dateRange['aria'] }}开始日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择{{ $dateRange['aria'] }}开始日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
|
||||
</button>
|
||||
<div class="upstream-data-calendar" role="dialog" aria-label="{{ $dateRange['aria'] }}开始日期日历" data-date-calendar hidden></div>
|
||||
</span>
|
||||
<b class="upstream-data-date-range__sep">至</b>
|
||||
<span class="upstream-data-date" data-date-picker>
|
||||
<input id="{{ $dateRange['to'] }}" type="text" name="{{ $dateRange['to'] }}" value="{{ request($dateRange['to']) }}" readonly autocomplete="off" placeholder="结束日期" aria-label="{{ $dateRange['aria'] }}结束日期" aria-describedby="installationDateRangeError" data-date-input>
|
||||
<button type="button" class="upstream-data-date__clear" data-date-clear hidden aria-label="清除{{ $dateRange['aria'] }}结束日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
<button type="button" class="upstream-data-date__pick" data-date-toggle aria-label="选择{{ $dateRange['aria'] }}结束日期">
|
||||
<span class="material-icons-outlined" aria-hidden="true">calendar_today</span>
|
||||
</button>
|
||||
<div class="upstream-data-calendar" role="dialog" aria-label="{{ $dateRange['aria'] }}结束日期日历" data-date-calendar hidden></div>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
@foreach ($filterFields as $filter)
|
||||
<label class="installation-filter-field">
|
||||
<span>{{ $filter['label'] }}</span>
|
||||
@if (isset($filter['options']))
|
||||
<select name="{{ $filter['name'] }}"><option value="">全部</option>@foreach ($filter['options'] as $value => $label)<option value="{{ $value }}" @selected((string) request($filter['name']) === (string) $value)>{{ $label }}</option>@endforeach</select>
|
||||
@else
|
||||
<input type="{{ $filter['type'] ?? 'text' }}" name="{{ $filter['name'] }}" value="{{ request($filter['name']) }}" @if (($filter['type'] ?? '') !== 'date') placeholder="按原值精确查询" @endif>
|
||||
@endif
|
||||
</label>
|
||||
@endforeach
|
||||
<p id="installationDateRangeError" class="installation-date-range-error" role="alert" hidden>结束日期不能早于开始日期。</p>
|
||||
<div class="installation-filter-actions">
|
||||
<button type="submit" class="biz-btn biz-btn-primary">查询</button>
|
||||
<a href="{{ request()->url() }}" class="biz-btn biz-btn-ghost">清除</a>
|
||||
<a href="{{ route('business.installation-details.export', request()->query()) }}" class="biz-btn biz-btn-ghost">导出</a>
|
||||
</div>
|
||||
</form>
|
||||
<script>
|
||||
(function () {
|
||||
var form = document.getElementById('installationFilterForm');
|
||||
if (!form) { return; }
|
||||
|
||||
if (window.CaiWuDatePicker) {
|
||||
window.CaiWuDatePicker.setup(form);
|
||||
}
|
||||
|
||||
var error = document.getElementById('installationDateRangeError');
|
||||
var ranges = [
|
||||
['created_from', 'created_to'],
|
||||
['customer_review_from', 'customer_review_to'],
|
||||
['finance_review_from', 'finance_review_to']
|
||||
];
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
var invalidEnd = null;
|
||||
ranges.forEach(function (names) {
|
||||
var from = form.elements.namedItem(names[0]);
|
||||
var to = form.elements.namedItem(names[1]);
|
||||
var invalid = Boolean(from && to && from.value && to.value && from.value > to.value);
|
||||
if (from) { from.setAttribute('aria-invalid', invalid ? 'true' : 'false'); }
|
||||
if (to) { to.setAttribute('aria-invalid', invalid ? 'true' : 'false'); }
|
||||
if (invalid && !invalidEnd) { invalidEnd = to; }
|
||||
});
|
||||
error.hidden = !invalidEnd;
|
||||
if (invalidEnd) {
|
||||
event.preventDefault();
|
||||
invalidEnd.focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@include('business.pages.partials.installation-filters')
|
||||
@include('business.pages.partials.installation-fee-summary')
|
||||
@endif
|
||||
<div class="biz-card-head">
|
||||
|
||||
@ -33,6 +33,13 @@ class InternalFlowReportPageTest extends FeatureTestCase
|
||||
'invoicing_ratio' => '1',
|
||||
]],
|
||||
'total' => 1,
|
||||
'summary' => [
|
||||
'companies' => 1,
|
||||
'projected_sales' => 1200000.0,
|
||||
'receipt_total' => 0.0,
|
||||
'remaining_gap' => 0.0,
|
||||
'average_invoicing_ratio' => 100.0,
|
||||
],
|
||||
]);
|
||||
$this->app->instance(InternalFlowReportRepository::class, $repository);
|
||||
|
||||
@ -49,6 +56,11 @@ class InternalFlowReportPageTest extends FeatureTestCase
|
||||
->assertSeeText('收票-内部流转(来源)1')
|
||||
->assertSeeText('剩余缺口')
|
||||
->assertSeeText('开票比例')
|
||||
->assertSeeText('公司数')
|
||||
->assertSeeText('预计销量合计')
|
||||
->assertSeeText('已收票合计')
|
||||
->assertSeeText('剩余缺口合计')
|
||||
->assertSeeText('平均开票比例')
|
||||
->assertSee('data-directory-query-picker', false)
|
||||
->assertSee('data-directory-query-export', false)
|
||||
->assertSee('data-directory-query-field="company_name"', false)
|
||||
|
||||
@ -244,30 +244,51 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertSeeText('安装单号')
|
||||
->assertSeeText('申请总金额')
|
||||
->assertSeeText('商品名称(1)')
|
||||
->assertSeeText('创建日期:')
|
||||
->assertSeeText('客服审核时间:')
|
||||
->assertSeeText('财务审核时间:')
|
||||
->assertSeeText('推送状态:')
|
||||
->assertSeeText('选择店铺:')
|
||||
->assertSeeText('选择删除:')
|
||||
->assertSeeText('订单类型:')
|
||||
->assertSeeText('商品ID:')
|
||||
->assertSeeText('时间口径')
|
||||
->assertSeeText('日期区间')
|
||||
->assertSeeText('店铺')
|
||||
->assertSeeText('安装状态')
|
||||
->assertSeeText('更多筛选')
|
||||
->assertSeeText('今天')
|
||||
->assertSeeText('本周')
|
||||
->assertSeeText('本月')
|
||||
->assertSeeText('上月')
|
||||
->assertSeeText('近30天')
|
||||
->assertSeeText('推送状态')
|
||||
->assertSeeText('订单编号')
|
||||
->assertSeeText('店铺 ID')
|
||||
->assertSeeText('安装公司')
|
||||
->assertSeeText('商品 ID')
|
||||
->assertSeeText('导出')
|
||||
->assertSee('id="installationFilterForm"', false)
|
||||
->assertSee('name="created_from"', false)
|
||||
->assertSee('name="created_to"', false)
|
||||
->assertSee('name="customer_review_from"', false)
|
||||
->assertSee('name="customer_review_to"', false)
|
||||
->assertSee('name="finance_review_from"', false)
|
||||
->assertSee('name="finance_review_to"', false)
|
||||
->assertSee('name="time_scope"', false)
|
||||
->assertSee('name="date_from"', false)
|
||||
->assertSee('name="date_to"', false)
|
||||
->assertSee('name="shop_name"', false)
|
||||
->assertSee('name="completed_status"', false)
|
||||
->assertSee('id="installationMoreFilters"', false)
|
||||
->assertSee('id="installationMoreFilters" class="installation-more-panel" hidden', false)
|
||||
->assertSee('data-installation-more-toggle', false)
|
||||
->assertSee('data-installation-quick-range="today"', false)
|
||||
->assertSee('data-installation-quick-range="last_30_days"', false)
|
||||
->assertSee('data-date-picker', false)
|
||||
->assertSee('data-date-calendar', false)
|
||||
->assertSee("window.CaiWuDatePicker.setup(form)", false)
|
||||
->assertSee("input.addEventListener('input', syncClear)", false)
|
||||
->assertSee('结束日期不能早于开始日期。')
|
||||
->assertSee('data-installation-fee-open', false)
|
||||
->assertSee('id="installation-fee-dialog"', false)
|
||||
->assertSeeText('安装费金额')
|
||||
->assertSeeText('按客服审核时间汇总安装明细的结算明细金额')
|
||||
->assertSeeText('较上月')
|
||||
->assertSeeText('包安装')
|
||||
->assertSeeText('不包安装')
|
||||
->assertSee('data-installation-fee-trend', false)
|
||||
->assertSee('data-installation-fee-package', false)
|
||||
->assertSee('data-installation-fee-non-package', false)
|
||||
->assertSee('border-left: 3px solid #006395', false)
|
||||
->assertSee('font-size: 2.625rem', false)
|
||||
->assertSee('function captionRange(value)', false)
|
||||
->assertSee('function setCardValue(total, rows, value, data)', false)
|
||||
->assertSeeText('平台')
|
||||
->assertSeeText('店铺名称')
|
||||
->assertSeeText('安装公司')
|
||||
@ -307,6 +328,58 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
$this->assertStringContainsString('INSTALL-EXPORT-001', $response->streamedContent());
|
||||
}
|
||||
|
||||
public function testInstallationDetailsUnifiedFiltersRenderActiveChipsAndKeepLowFrequencyFieldsInMorePanel(): void
|
||||
{
|
||||
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
|
||||
$mirror->shouldReceive('paginate')->once()->andReturn([
|
||||
'ready' => true,
|
||||
'total' => 1,
|
||||
'data' => [array_replace(array_fill_keys(array_keys(InstallationDetail::columnLabels()), null), [
|
||||
'id' => 9,
|
||||
'installation_no' => 'INSTALL-CHIP-001',
|
||||
'installation_company' => '测试安装公司',
|
||||
'store_name' => '测试店铺',
|
||||
'requested_amount' => '120.50',
|
||||
'settled_amount' => '100.00',
|
||||
'related_guid' => 'guid-install-chip',
|
||||
])],
|
||||
]);
|
||||
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
|
||||
|
||||
$url = route('business.pages.show', [
|
||||
'section' => 'financial-operations',
|
||||
'page' => 'installation-details',
|
||||
]) . '?' . http_build_query([
|
||||
'time_scope' => 'customer_review',
|
||||
'date_from' => '2026-09-01',
|
||||
'date_to' => '2026-09-30',
|
||||
'shop_name' => '测试店铺',
|
||||
'completed_status' => '1',
|
||||
'shop_code' => 'SHOP-001',
|
||||
'push_status' => 'PUSHED',
|
||||
'order_no' => 'ORDER-CHIP-001',
|
||||
]);
|
||||
|
||||
$this->loginAs()
|
||||
->get($url)
|
||||
->assertOk()
|
||||
->assertSeeText('客服审核时间')
|
||||
->assertSeeText('2026-09-01 至 2026-09-30')
|
||||
->assertSeeText('测试店铺')
|
||||
->assertSeeText('已完工')
|
||||
->assertSeeText('SHOP-001')
|
||||
->assertSeeText('PUSHED')
|
||||
->assertSeeText('ORDER-CHIP-001')
|
||||
->assertSee('aria-label="删除「客服审核时间」条件"', false)
|
||||
->assertSee('aria-label="删除「店铺」条件"', false)
|
||||
->assertSee('aria-label="删除「安装状态」条件"', false)
|
||||
->assertSee('aria-label="删除「店铺 ID」条件"', false)
|
||||
->assertSee('aria-label="删除「推送状态」条件"', false)
|
||||
->assertSee('aria-label="删除「订单编号」条件"', false)
|
||||
->assertSee('data-installation-more-count>3</b>', false)
|
||||
->assertDontSee('id="installationMoreFilters" class="installation-more-panel" hidden', false);
|
||||
}
|
||||
|
||||
public function testInstallationDetailsEndpointReturnsChildItemsForMainRecord(): void
|
||||
{
|
||||
$mirror = Mockery::mock(InstallationDetailMirrorRepository::class);
|
||||
@ -342,6 +415,10 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
['platform' => '淘系', 'store_name' => '店铺A', 'installation_company' => '万师傅', 'payment_channel' => '包安装', 'amount' => 1250.50],
|
||||
['platform' => '京东', 'store_name' => '店铺B', 'installation_company' => '鲁班到家', 'payment_channel' => '不包安装', 'amount' => 750.00],
|
||||
],
|
||||
'previous_total' => 1600.40,
|
||||
'previous_from' => '2026-08-01',
|
||||
'previous_to' => '2026-08-31',
|
||||
'change_rate' => 25.0,
|
||||
]);
|
||||
$this->app->instance(InstallationDetailMirrorRepository::class, $mirror);
|
||||
|
||||
@ -355,7 +432,9 @@ class ManagementCrudTest extends FeatureTestCase
|
||||
->assertJsonPath('rows.0.platform', '淘系')
|
||||
->assertJsonPath('rows.0.store_name', '店铺A')
|
||||
->assertJsonPath('rows.0.payment_channel', '包安装')
|
||||
->assertJsonPath('rows.1.payment_channel', '不包安装');
|
||||
->assertJsonPath('rows.1.payment_channel', '不包安装')
|
||||
->assertJsonPath('previous_total', 1600.4)
|
||||
->assertJsonPath('change_rate', 25.0);
|
||||
}
|
||||
|
||||
public function testInstallationSummaryRequiresCompleteCustomerReviewRange(): void
|
||||
|
||||
@ -46,11 +46,13 @@ class PrototypePageTest extends FeatureTestCase
|
||||
->assertSeeText('主账号')
|
||||
->assertSeeText('已设置')
|
||||
->assertSeeText('13800000000')
|
||||
->assertSee('directory-value-badge', false)
|
||||
->assertSee('data-tone="accent"', false)
|
||||
->assertDontSeeText('MAIN')
|
||||
->assertDontSeeText('共享镜像中暂未读取到店铺账号数据');
|
||||
}
|
||||
|
||||
public function testSharedBusinessPagesRenderClickableSummaryCards(): void
|
||||
public function testBaseInformationPagesDoNotRenderSummaryCards(): void
|
||||
{
|
||||
$rows = new LengthAwarePaginator([(object) [
|
||||
'id' => 'account-summary',
|
||||
@ -64,15 +66,7 @@ class PrototypePageTest extends FeatureTestCase
|
||||
->shouldReceive('paginate')
|
||||
->once()
|
||||
->with('store-management', 'store-accounts', 50)
|
||||
->andReturnUsing(function () use ($rows): LengthAwarePaginator {
|
||||
request()->attributes->set('business-page-summary-cards', [
|
||||
['label' => '账号总数', 'value' => 958, 'hint' => '当前筛选内账号记录', 'format' => 'integer', 'query' => ['page' => null]],
|
||||
['label' => '主账号', 'value' => 640, 'hint' => '账号类型为 MAIN', 'format' => 'integer', 'query' => ['page' => null, 'account_type' => '主账号']],
|
||||
['label' => '子账号', 'value' => 318, 'hint' => '账号类型为 SUB', 'format' => 'integer', 'query' => ['page' => null, 'account_type' => '子账号']],
|
||||
]);
|
||||
|
||||
return $rows;
|
||||
});
|
||||
->andReturn($rows);
|
||||
|
||||
$this->loginAs()
|
||||
->get(route('business.pages.show', [
|
||||
@ -81,16 +75,56 @@ class PrototypePageTest extends FeatureTestCase
|
||||
'account_type' => '主账号',
|
||||
]))
|
||||
->assertOk()
|
||||
->assertSee('data-business-summary-cards', false)
|
||||
->assertSee('data-business-summary-card="账号总数"', false)
|
||||
->assertSee('data-business-summary-card="主账号"', false)
|
||||
->assertSee('data-business-summary-card="子账号"', false)
|
||||
->assertSee('account_type=', false)
|
||||
->assertSee('aria-current="true"', false)
|
||||
->assertSeeText('958')
|
||||
->assertSeeText('640')
|
||||
->assertSeeText('318')
|
||||
->assertSee('查看明细', false);
|
||||
->assertDontSee('data-business-summary-cards', false);
|
||||
}
|
||||
|
||||
public function testOperatingReportPagesRenderTableSpecificSummaryCards(): void
|
||||
{
|
||||
$pages = [
|
||||
['company-cost-arrangement', ['数据状态', '完整表头', '11 列', '核心结果', '缺口提示', '配平提示']],
|
||||
['tax-forecast', ['数据状态', '完整表头', '22 列', '计算税种', '必补口径', '当前记录']],
|
||||
['personnel-inventory', ['数据状态', '完整表头', '17 列', '敏感字段', '容量规则', '当前记录']],
|
||||
];
|
||||
|
||||
foreach ($pages as [$page, $labels]) {
|
||||
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'company-management',
|
||||
'page' => $page,
|
||||
]));
|
||||
|
||||
$response->assertOk()->assertSee('data-business-summary-cards', false);
|
||||
foreach ($labels as $label) {
|
||||
$response->assertSeeText($label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function testOperatingReportFieldContractsKeepAllOriginalHeaders(): void
|
||||
{
|
||||
$navigation = require config_path('business_navigation.php');
|
||||
$pages = collect($navigation)
|
||||
->flatMap(static fn (array $section) => collect($section['pages'] ?? [])
|
||||
->map(static fn (array $page): array => [
|
||||
'key' => $section['slug'] . '/' . $page['slug'],
|
||||
'fields' => $page['directory']['fields'] ?? [],
|
||||
]))
|
||||
->keyBy('key');
|
||||
$expectedFieldCounts = [
|
||||
'company-management/subject-operating-report' => 21,
|
||||
'company-management/company-cost-arrangement' => 10,
|
||||
'company-management/tax-forecast' => 21,
|
||||
'company-management/personnel-inventory' => 16,
|
||||
'store-management/store-report' => 26,
|
||||
'store-management/internal-flow-report' => 15,
|
||||
];
|
||||
|
||||
foreach ($expectedFieldCounts as $key => $count) {
|
||||
$fields = $pages[$key]['fields'];
|
||||
$names = array_column($fields, 'name');
|
||||
|
||||
$this->assertCount($count, $fields, $key . ' 的字段数量不应减少');
|
||||
$this->assertCount($count, array_unique($names), $key . ' 的字段名不应重复');
|
||||
}
|
||||
}
|
||||
|
||||
public function testNavigationUsesConfiguredPermissionsAndTitles(): void
|
||||
@ -150,6 +184,8 @@ class PrototypePageTest extends FeatureTestCase
|
||||
->assertSeeText('测试主体')
|
||||
->assertSeeText('小规模纳税人')
|
||||
->assertSeeText('个人独资企业')
|
||||
->assertSee('directory-value-badge', false)
|
||||
->assertSee('data-tone="info"', false)
|
||||
->assertSeeText('共 1 条')
|
||||
->assertSeeText('已从共享业务镜像读取')
|
||||
->assertDontSee('<div class="directory-table-tools" data-directory-tools>', false)
|
||||
@ -191,16 +227,11 @@ class PrototypePageTest extends FeatureTestCase
|
||||
->assertSeeTextInOrder(['最近12个月累计收入', '1月收入', '12月收入']);
|
||||
}
|
||||
|
||||
public function testSubjectOperatingReportHidesMonthsWithoutDataAndKeepsThemSelectable(): void
|
||||
public function testSubjectOperatingReportShowsAllMonthsByDefaultAndStillSupportsColumnSelection(): void
|
||||
{
|
||||
$service = Mockery::mock(UpstreamBusinessInformation::class);
|
||||
$service->shouldReceive('paginate')->with('company-management', 'subject-operating-report', 50)
|
||||
->andReturnUsing(function (): LengthAwarePaginator {
|
||||
request()->attributes->set(
|
||||
'subject-operating-report.default_hidden_columns',
|
||||
['income_01', 'income_02'],
|
||||
);
|
||||
|
||||
return new LengthAwarePaginator([(object) [
|
||||
'entity' => '测试主体',
|
||||
'city' => '常州市',
|
||||
@ -224,14 +255,13 @@ class PrototypePageTest extends FeatureTestCase
|
||||
$response->assertOk();
|
||||
$html = $response->getContent();
|
||||
|
||||
// 没有数据的月份默认隐藏,但表头和「筛选字段」勾选项仍在页面中。
|
||||
$this->assertMatchesRegularExpression('/<th[^>]*data-column-key="income_01"[^>]*is-column-hidden/', $html);
|
||||
$this->assertMatchesRegularExpression('/<th[^>]*data-column-key="income_02"[^>]*is-column-hidden/', $html);
|
||||
// 所有原始月份表头默认显示,避免新增汇总卡后缩减原表字段。
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="income_01"[^>]*is-column-hidden/', $html);
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="income_02"[^>]*is-column-hidden/', $html);
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="income_03"[^>]*is-column-hidden/', $html);
|
||||
$this->assertStringContainsString('value="income_01" data-directory-query-column', $html);
|
||||
$this->assertStringNotContainsString('value="income_01" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('value="income_01" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('value="income_03" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('筛选字段 19/21', $html);
|
||||
|
||||
// 手动勾选(columns 参数)后,对应月份列恢复显示。
|
||||
$restored = $this->get(route('business.pages.show', [
|
||||
@ -247,16 +277,11 @@ class PrototypePageTest extends FeatureTestCase
|
||||
$this->assertMatchesRegularExpression('/<th[^>]*data-column-key="income_04"[^>]*is-column-hidden/', $restoredHtml);
|
||||
}
|
||||
|
||||
public function testStoreReportHidesSalesMonthsWithoutDataAndKeepsThemSelectable(): void
|
||||
public function testStoreReportShowsAllMonthsByDefaultAndStillSupportsColumnSelection(): void
|
||||
{
|
||||
$service = Mockery::mock(UpstreamBusinessInformation::class);
|
||||
$service->shouldReceive('paginate')->with('store-management', 'store-report', 50)
|
||||
->andReturnUsing(function (): LengthAwarePaginator {
|
||||
request()->attributes->set(
|
||||
'store-report.default_hidden_columns',
|
||||
['sales_01', 'sales_08'],
|
||||
);
|
||||
|
||||
return new LengthAwarePaginator([(object) [
|
||||
'erp_store_name' => 'ERP测试店铺',
|
||||
'store_name' => '前台测试店铺',
|
||||
@ -280,14 +305,13 @@ class PrototypePageTest extends FeatureTestCase
|
||||
$response->assertOk();
|
||||
$html = $response->getContent();
|
||||
|
||||
// 没有业绩的月份默认隐藏,但表头和「筛选字段」勾选项仍在页面中。
|
||||
$this->assertMatchesRegularExpression('/<th[^>]*data-column-key="sales_01"[^>]*is-column-hidden/', $html);
|
||||
$this->assertMatchesRegularExpression('/<th[^>]*data-column-key="sales_08"[^>]*is-column-hidden/', $html);
|
||||
// 所有原始月份表头默认显示,避免新增汇总卡后缩减原表字段。
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="sales_01"[^>]*is-column-hidden/', $html);
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="sales_08"[^>]*is-column-hidden/', $html);
|
||||
$this->assertDoesNotMatchRegularExpression('/<th[^>]*data-column-key="sales_07"[^>]*is-column-hidden/', $html);
|
||||
$this->assertStringContainsString('value="sales_01" data-directory-query-column', $html);
|
||||
$this->assertStringNotContainsString('value="sales_01" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('value="sales_01" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('value="sales_07" data-directory-query-column checked', $html);
|
||||
$this->assertStringContainsString('筛选字段 24/26', $html);
|
||||
|
||||
// 手动勾选(columns 参数)后,对应月份列恢复显示。
|
||||
$restored = $this->get(route('business.pages.show', [
|
||||
|
||||
@ -117,6 +117,11 @@ class InternalFlowReportImportTest extends TestCase
|
||||
$all = $repository->paginate(1, 50);
|
||||
$this->assertSame(3, $all['total']);
|
||||
$this->assertSame('示例甲供应链有限公司', $all['data'][0]['company_name']);
|
||||
$this->assertSame(3, $all['summary']['companies']);
|
||||
$this->assertSame(3669135.68, $all['summary']['projected_sales']);
|
||||
$this->assertSame(1420000.61, $all['summary']['receipt_total']);
|
||||
$this->assertEqualsWithDelta(1401086.9311, $all['summary']['remaining_gap'], 0.005);
|
||||
$this->assertEqualsWithDelta(71.0, $all['summary']['average_invoicing_ratio'], 0.000001);
|
||||
|
||||
$filtered = $repository->paginate(1, 50, ['company_name' => '示例乙']);
|
||||
$this->assertSame(1, $filtered['total']);
|
||||
|
||||
@ -136,6 +136,8 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(1, $businessTypedCompany['total']);
|
||||
$this->assertSame('个人独资企业', $businessTypedCompany['data'][0]['business_registration_type']);
|
||||
$this->assertSame(1, $cityCompany['total']);
|
||||
$this->assertSame('保留', $companyClassifications['新公司']['status']);
|
||||
$this->assertSame('待注销', $companyClassifications['其他公司']['status']);
|
||||
$this->assertSame('小规模纳税人', $companyClassifications['新公司']['tax_type']);
|
||||
$this->assertSame('个人独资企业', $companyClassifications['新公司']['business_registration_type']);
|
||||
$this->assertNull($companyClassifications['旧公司']['business_registration_type']);
|
||||
|
||||
@ -32,7 +32,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
$schema->create('business_mirror_table_states',function(Blueprint $t){$t->string('dataset_key')->primary();$t->dateTime('initial_completed_at')->nullable();});
|
||||
$schema->create('fixture_install',function(Blueprint $t){
|
||||
$t->integer('Id');
|
||||
foreach(['SourceGuid','OrderNum','ordernum_id','shop_name','SubmitTime','ShipTIme','kfshTime','cwshTime','apistatus','kfsh','cwsh','BidStatus','costtaker','costWay','installComplete','Delete','installCompanyName','TypeDto'] as $c) $t->string($c)->nullable();
|
||||
foreach(['SourceGuid','OrderNum','ordernum_id','shop_name','shopcode','SubmitTime','ShipTIme','kfshTime','cwshTime','apistatus','kfsh','cwsh','BidStatus','costtaker','costWay','installComplete','Delete','installCompanyName','TypeDto'] as $c) $t->string($c)->nullable();
|
||||
$t->integer('StatusId')->nullable()->default(0);
|
||||
foreach(['installcost','settleAmount','acceptAmount'] as $c) $t->decimal($c,14,2)->default(0);
|
||||
});
|
||||
@ -48,7 +48,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
foreach(array_keys($definitions) as $key) DB::connection('business_mirror')->table('business_mirror_table_states')->insert(['dataset_key'=>$key,'initial_completed_at'=>'2026-08-01']);
|
||||
DB::connection('business_mirror')->table('fixture_install')->insert([
|
||||
['Id'=>1,'SourceGuid'=>'A','OrderNum'=>'OLD','shop_name'=>'旧店','SubmitTime'=>'2026-08-01','ShipTIme'=>'2026-08-02','installcost'=>500],
|
||||
['Id'=>2,'SourceGuid'=>'A','OrderNum'=>'NEW','shop_name'=>'测试店','SubmitTime'=>'2026-08-20 23:59:59','ShipTIme'=>'2026-08-21','installcost'=>20],
|
||||
['Id'=>2,'SourceGuid'=>'A','OrderNum'=>'NEW','shop_name'=>'测试店','shopcode'=>'SHOP-A','SubmitTime'=>'2026-08-20 23:59:59','ShipTIme'=>'2026-08-21','installcost'=>20],
|
||||
['Id'=>3,'SourceGuid'=>'B','OrderNum'=>'OTHER','shop_name'=>'其他店','SubmitTime'=>'2026-08-20','ShipTIme'=>'2026-08-21','installcost'=>30],
|
||||
['Id'=>4,'SourceGuid'=>'C','OrderNum'=>'NEXT','shop_name'=>'测试店','SubmitTime'=>'2026-08-21','ShipTIme'=>'2026-08-22','installcost'=>40],
|
||||
['Id'=>5,'SourceGuid'=>'D','OrderNum'=>'PRE-2026','shop_name'=>'测试店','SubmitTime'=>'2026-08-21','ShipTIme'=>'2025-12-31 23:59:59','installcost'=>999],
|
||||
@ -71,6 +71,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(1,$this->repository->paginate(1,50,$filters)['total']);
|
||||
$this->assertSame(20.0,$this->repository->summary($filters)['requested']);
|
||||
$this->assertSame(0,$this->repository->paginate(1,50,['shop_name'=>"' OR 1=1 --"])['total']);
|
||||
$this->assertSame(1,$this->repository->paginate(1,50,['shop_code'=>'SHOP-A'])['total']);
|
||||
}
|
||||
|
||||
public function test_product_filter_does_not_multiply_main_rows(): void
|
||||
@ -139,6 +140,7 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
DB::connection('business_mirror')->table('fixture_install')->where('Id',3)->update(['installCompanyName'=>'鲁班到家','costWay'=>'银行转账']);
|
||||
DB::connection('business_mirror')->table('fixture_install')->where('Id',4)->update(['installCompanyName'=>'万师傅']);
|
||||
DB::connection('business_mirror')->table('fixture_items')->insert([
|
||||
['Id'=>19,'guid'=>'A','shopproid'=>'P0','Sourceno'=>'A-0','kfshTime'=>'2026-08-15 10:00:00','settleItemAmount'=>90.00,'acceptItemAmount'=>80.00],
|
||||
['Id'=>20,'guid'=>'A','shopproid'=>'P1','Sourceno'=>'A-1','kfshTime'=>'2026-09-01 09:00:00','settleItemAmount'=>60.10,'acceptItemAmount'=>40.00],
|
||||
['Id'=>21,'guid'=>'A','shopproid'=>'P2','Sourceno'=>'A-1','kfshTime'=>'2026-09-30 18:00:00','settleItemAmount'=>39.90,'acceptItemAmount'=>0.00],
|
||||
['Id'=>22,'guid'=>'B','shopproid'=>'P3','Sourceno'=>'B-1','kfshTime'=>'2026-09-15 10:00:00','settleItemAmount'=>80.00,'acceptItemAmount'=>70.00],
|
||||
@ -158,6 +160,10 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
['platform'=>'其他','store_name'=>'其他店','installation_company'=>'鲁班到家','payment_channel'=>'不包安装','amount'=>80.0],
|
||||
],$result['rows']);
|
||||
$this->assertSame(180.0,$result['total']);
|
||||
$this->assertSame(90.0,$result['previous_total']);
|
||||
$this->assertSame('2026-08-01',$result['previous_from']);
|
||||
$this->assertSame('2026-08-31',$result['previous_to']);
|
||||
$this->assertSame(100.0,$result['change_rate']);
|
||||
}
|
||||
|
||||
public function test_fee_details_classifies_platform_by_store_name_rules(): void
|
||||
@ -223,4 +229,27 @@ class InstallationDetailMirrorRepositoryTest extends TestCase
|
||||
$this->expectException(\Illuminate\Validation\ValidationException::class);
|
||||
InstallationDetailMirrorRepository::requestFilters();
|
||||
}
|
||||
|
||||
public function test_unified_time_scope_maps_to_the_selected_legacy_date_range(): void
|
||||
{
|
||||
request()->merge([
|
||||
'time_scope' => 'finance_review',
|
||||
'date_from' => '2026-08-01',
|
||||
'date_to' => '2026-08-31',
|
||||
'created_from' => '2026-07-01',
|
||||
'created_to' => '2026-07-31',
|
||||
'shop_name' => '测试店',
|
||||
]);
|
||||
|
||||
$filters = InstallationDetailMirrorRepository::requestFilters();
|
||||
|
||||
$this->assertSame('finance_review', $filters['time_scope']);
|
||||
$this->assertSame('2026-08-01', $filters['date_from']);
|
||||
$this->assertSame('2026-08-31', $filters['date_to']);
|
||||
$this->assertSame('2026-08-01', $filters['finance_review_from']);
|
||||
$this->assertSame('2026-08-31', $filters['finance_review_to']);
|
||||
$this->assertSame('测试店', $filters['shop_name']);
|
||||
$this->assertArrayNotHasKey('created_from', $filters);
|
||||
$this->assertArrayNotHasKey('created_to', $filters);
|
||||
}
|
||||
}
|
||||
|
||||
@ -256,6 +256,7 @@ class StoreReportRepositoryTest extends TestCase
|
||||
|
||||
public function test_upstream_business_information_exposes_shared_store_report_rows(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-07-15 10:00:00');
|
||||
$this->insertBill('店铺A', '2026-07-05', 60);
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
|
||||
@ -287,14 +288,14 @@ class StoreReportRepositoryTest extends TestCase
|
||||
$this->assertSame('部门负责人', $paginator->items()[0]->department_manager);
|
||||
$this->assertSame('运营中心A', $paginator->items()[0]->profit_center);
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
$this->assertSame(1, $summaryCards['店铺总数']['value']);
|
||||
$this->assertSame(1, $summaryCards['正常店铺']['value']);
|
||||
$this->assertSame(0, $summaryCards['关店中']['value']);
|
||||
$this->assertSame(0, $summaryCards['已关闭']['value']);
|
||||
$this->assertSame(60.0, (float) $summaryCards['期间销量']['value']);
|
||||
$this->assertSame(1, $summaryCards['正常存续店铺']['value']);
|
||||
$this->assertSame(0.0, (float) $summaryCards['上月(6月)销量']['value']);
|
||||
$this->assertSame(60.0, (float) $summaryCards['当月(7月)销量']['value']);
|
||||
$this->assertSame(0, $summaryCards['上月(6月)有销量店铺']['value']);
|
||||
$this->assertSame(1, $summaryCards['当月(7月)有销量店铺']['value']);
|
||||
}
|
||||
|
||||
public function test_store_report_hides_sales_months_without_data_by_default(): void
|
||||
public function test_store_report_keeps_all_sales_months_visible_by_default(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-15 10:00:00');
|
||||
$this->insertBill('店铺A', '2026-07-05', 60);
|
||||
@ -314,15 +315,44 @@ class StoreReportRepositoryTest extends TestCase
|
||||
|
||||
$paginator = $service->paginate('store-management', 'store-report', 50);
|
||||
|
||||
// 只有 7 月有业绩,其余月份列默认隐藏。
|
||||
$this->assertSame(
|
||||
['sales_01', 'sales_02', 'sales_03', 'sales_04', 'sales_05', 'sales_06', 'sales_08', 'sales_09', 'sales_10', 'sales_11', 'sales_12'],
|
||||
$this->assertNull(
|
||||
$this->app['request']->attributes->get('store-report.default_hidden_columns'),
|
||||
);
|
||||
$this->assertSame(1, $paginator->total());
|
||||
$this->assertSame(60.0, (float) $paginator->items()[0]->sales_07);
|
||||
}
|
||||
|
||||
public function test_store_cards_exclude_closed_store_but_keep_its_detail_row(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-07-15 10:00:00');
|
||||
$this->insertBill('正常店铺', '2026-07-05', 10);
|
||||
$this->insertBill('已关店铺', '2026-07-06', 90);
|
||||
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
|
||||
$mirror->shouldReceive('allStores')->andReturn([
|
||||
['erp_store_name' => '正常店铺', 'company_name' => '主体甲', 'store_status' => '0'],
|
||||
['erp_store_name' => '已关店铺', 'company_name' => '主体乙', 'store_status' => '6'],
|
||||
]);
|
||||
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
|
||||
$accounts->shouldReceive('ownershipByStoreName')->andReturn([]);
|
||||
$service = new UpstreamBusinessInformation(
|
||||
Mockery::mock(UpstreamQueryCacheFactory::class),
|
||||
$mirror,
|
||||
$this->repository,
|
||||
$accounts,
|
||||
);
|
||||
|
||||
$paginator = $service->paginate('store-management', 'store-report', 50);
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
|
||||
// 已关店铺明细继续保留,只从“正常存续”汇总卡中剔除。
|
||||
$this->assertSame(2, $paginator->total());
|
||||
$this->assertSame(1, $summaryCards['正常存续店铺']['value']);
|
||||
$this->assertSame(10.0, (float) $summaryCards['当月(7月)销量']['value']);
|
||||
$this->assertSame(1, $summaryCards['当月(7月)有销量店铺']['value']);
|
||||
}
|
||||
|
||||
public function test_subject_operating_report_aggregates_income_by_entity(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-01 10:00:00');
|
||||
@ -371,10 +401,12 @@ class StoreReportRepositoryTest extends TestCase
|
||||
$this->assertSame('小规模纳税人', $rows[0]->tax_type);
|
||||
$this->assertSame('个人独资企业', $rows[0]->business_registration_type);
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
$this->assertSame(1, $summaryCards['主体总数']['value']);
|
||||
$this->assertSame(1, $summaryCards['有效主体']['value']);
|
||||
$this->assertSame(0, $summaryCards['已注销主体']['value']);
|
||||
$this->assertSame(90.0, (float) $summaryCards['最近12个月收入']['value']);
|
||||
$this->assertSame(1, $summaryCards['正常存续主体']['value']);
|
||||
$this->assertSame(70.0, (float) $summaryCards['上月(8月)收入']['value']);
|
||||
$this->assertSame(0.0, (float) $summaryCards['当月(9月)收入']['value']);
|
||||
$this->assertSame(1, $summaryCards['小规模纳税人']['value']);
|
||||
$this->assertSame(0, $summaryCards['一般纳税人']['value']);
|
||||
$this->assertArrayNotHasKey('最近12个月收入', $summaryCards);
|
||||
|
||||
request()->merge(['income_08' => '70.00']);
|
||||
$this->assertSame(1, $service->paginate('company-management', 'subject-operating-report', 50)->total());
|
||||
@ -392,6 +424,110 @@ class StoreReportRepositoryTest extends TestCase
|
||||
$this->assertSame(0, $service->paginate('company-management', 'subject-operating-report', 50)->total());
|
||||
}
|
||||
|
||||
public function test_subject_cards_exclude_deregistered_entities_and_show_slash_for_their_types(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-15 10:00:00');
|
||||
$this->insertBill('店铺正常', '2026-08-05', 30);
|
||||
$this->insertBill('店铺注销', '2026-08-06', 70);
|
||||
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
|
||||
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
|
||||
$mirror->shouldReceive('allStores')->andReturn([
|
||||
['erp_store_name' => '店铺正常', 'company_name' => '存续主体', 'store_status' => '0'],
|
||||
['erp_store_name' => '店铺注销', 'company_name' => '注销主体', 'store_status' => '0'],
|
||||
]);
|
||||
$mirror->shouldReceive('companiesByNames')->andReturn([
|
||||
'存续主体' => [[
|
||||
'Id' => 2,
|
||||
'businessLicenseName' => '存续主体',
|
||||
'companyStatus' => '0',
|
||||
'taxCategoryAndType' => '小规模',
|
||||
'business_registration_type' => null,
|
||||
]],
|
||||
'注销主体' => [[
|
||||
'Id' => 1,
|
||||
'businessLicenseName' => '注销主体',
|
||||
'companyStatus' => '1',
|
||||
'taxCategoryAndType' => '一般纳税人',
|
||||
'business_registration_type' => '有限公司',
|
||||
]],
|
||||
]);
|
||||
$mirror->shouldReceive('companyClassificationsByNames')->andReturn([]);
|
||||
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
|
||||
$accounts->shouldReceive('ownershipByStoreName')->andReturn([]);
|
||||
$service = new UpstreamBusinessInformation(
|
||||
Mockery::mock(UpstreamQueryCacheFactory::class),
|
||||
$mirror,
|
||||
$this->repository,
|
||||
$accounts,
|
||||
);
|
||||
|
||||
$rows = collect($service->paginate('company-management', 'subject-operating-report', 50)->items())->keyBy('entity');
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
|
||||
$this->assertSame('小规模纳税人', $rows['存续主体']->tax_type);
|
||||
$this->assertSame('待维护', $rows['存续主体']->business_registration_type);
|
||||
$this->assertSame('/', $rows['注销主体']->tax_type);
|
||||
$this->assertSame('/', $rows['注销主体']->business_registration_type);
|
||||
$this->assertSame(1, $summaryCards['正常存续主体']['value']);
|
||||
$this->assertSame(30.0, (float) $summaryCards['上月(8月)收入']['value']);
|
||||
$this->assertSame(1, $summaryCards['小规模纳税人']['value']);
|
||||
$this->assertSame(0, $summaryCards['一般纳税人']['value']);
|
||||
}
|
||||
|
||||
public function test_subject_uses_company_ledger_retained_tag_for_operating_population(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-09-15 10:00:00');
|
||||
$this->insertBill('保留店铺', '2026-08-05', 30);
|
||||
$this->insertBill('待注销店铺', '2026-08-06', 70);
|
||||
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
|
||||
$mirror->shouldReceive('ready')->with('company-information.read')->andReturnTrue();
|
||||
$mirror->shouldReceive('allStores')->andReturn([
|
||||
['erp_store_name' => '保留店铺', 'company_name' => '保留主体', 'store_status' => '0'],
|
||||
['erp_store_name' => '待注销店铺', 'company_name' => '待注销主体', 'store_status' => '0'],
|
||||
]);
|
||||
$mirror->shouldReceive('companiesByNames')->andReturn([
|
||||
'保留主体' => [[
|
||||
'Id' => 1,
|
||||
'businessLicenseName' => '保留主体',
|
||||
'companyStatus' => '0',
|
||||
'taxCategoryAndType' => '小规模纳税人',
|
||||
'business_registration_type' => '/',
|
||||
]],
|
||||
'待注销主体' => [[
|
||||
'Id' => 2,
|
||||
'businessLicenseName' => '待注销主体',
|
||||
'companyStatus' => '0',
|
||||
'taxCategoryAndType' => '一般纳税人',
|
||||
'business_registration_type' => '核定',
|
||||
]],
|
||||
]);
|
||||
$mirror->shouldReceive('companyClassificationsByNames')->andReturn([
|
||||
'保留主体' => ['status' => '保留', 'tax_type' => '小规模纳税人', 'business_registration_type' => null],
|
||||
'待注销主体' => ['status' => '待注销', 'tax_type' => '一般纳税人', 'business_registration_type' => '核定'],
|
||||
]);
|
||||
$accounts = Mockery::mock(StoreAccountMirrorRepository::class);
|
||||
$accounts->shouldReceive('ownershipByStoreName')->andReturn([]);
|
||||
$service = new UpstreamBusinessInformation(
|
||||
Mockery::mock(UpstreamQueryCacheFactory::class),
|
||||
$mirror,
|
||||
$this->repository,
|
||||
$accounts,
|
||||
);
|
||||
|
||||
$rows = collect($service->paginate('company-management', 'subject-operating-report', 50)->items())->keyBy('entity');
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
|
||||
$this->assertSame('待维护', $rows['保留主体']->business_registration_type);
|
||||
$this->assertSame('/', $rows['待注销主体']->tax_type);
|
||||
$this->assertSame('/', $rows['待注销主体']->business_registration_type);
|
||||
$this->assertSame(1, $summaryCards['正常存续主体']['value']);
|
||||
$this->assertSame(30.0, (float) $summaryCards['上月(8月)收入']['value']);
|
||||
}
|
||||
|
||||
private function insertBill(string $store, string $occurredOn, float $income): void
|
||||
{
|
||||
$this->insertIncome($store, $occurredOn, $income);
|
||||
|
||||
@ -160,14 +160,10 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
$this->assertNotSame('运营总监', $paginator->items()[0]->department_manager);
|
||||
$this->assertSame('正常', $paginator->items()[0]->store_status);
|
||||
$summaryCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
$this->assertSame(1, $summaryCards['店铺总数']['value']);
|
||||
$this->assertSame(1, $summaryCards['正常店铺']['value']);
|
||||
$this->assertSame(0, $summaryCards['关闭中']['value']);
|
||||
$this->assertSame(0, $summaryCards['已关闭']['value']);
|
||||
$this->assertSame(1, $summaryCards['平台数']['value']);
|
||||
$this->assertSame([], $summaryCards->all());
|
||||
}
|
||||
|
||||
public function test_company_and_trademark_mirror_summaries_become_clickable_cards(): void
|
||||
public function test_base_information_pages_do_not_render_summary_cards(): void
|
||||
{
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->times(2)->andReturnUsing(
|
||||
@ -203,16 +199,11 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
);
|
||||
$information->paginate('company-management', 'basic-information', 50);
|
||||
$companyCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
$this->assertSame(818, $companyCards['公司总数']['value']);
|
||||
$this->assertSame(['page' => null, 'company_status' => '正常'], $companyCards['正常公司']['query']);
|
||||
$this->assertSame(['page' => null, 'taxpayer_type' => '一般纳税人'], $companyCards['一般纳税人']['query']);
|
||||
$this->assertSame([], $companyCards->all());
|
||||
|
||||
$information->paginate('trademark-management', 'information', 50);
|
||||
$trademarkCards = collect($this->app['request']->attributes->get('business-page-summary-cards'))->keyBy('label');
|
||||
$this->assertSame(593, $trademarkCards['商标总数']['value']);
|
||||
$this->assertSame(45, $trademarkCards['未启用']['value']);
|
||||
$this->assertSame(['page' => null, 'trademark_status' => '有效'], $trademarkCards['有效商标']['query']);
|
||||
$this->assertSame('final_holder', $trademarkCards['最终持有人']['query']['sort']);
|
||||
$this->assertSame([], $trademarkCards->all());
|
||||
}
|
||||
|
||||
public function test_bootstrap_without_mirror_explains_shared_connection_instead_of_requesting_upstream_credentials(): void
|
||||
@ -228,7 +219,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
->paginate('store-management', 'basic-information');
|
||||
}
|
||||
|
||||
public function test_subject_operating_report_hides_income_months_without_data_by_default(): void
|
||||
public function test_subject_operating_report_keeps_all_income_months_visible_by_default(): void
|
||||
{
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
|
||||
@ -267,9 +258,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
|
||||
$paginator = $information->paginate('company-management', 'subject-operating-report', 50);
|
||||
|
||||
// income_02 有金额、income_03 为 0 都算有数据,其余空月份默认隐藏。
|
||||
$this->assertSame(
|
||||
['income_01', 'income_04', 'income_05', 'income_06', 'income_07', 'income_08', 'income_09', 'income_10', 'income_11', 'income_12'],
|
||||
$this->assertNull(
|
||||
$this->app['request']->attributes->get('subject-operating-report.default_hidden_columns'),
|
||||
);
|
||||
$this->assertSame(1, $paginator->total());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user