集成原型支线并修复共享镜像字段和导航权限 #88
@ -105,6 +105,7 @@ SELECT
|
||||
registeredAddress AS registered_address,
|
||||
businessScope AS business_scope,
|
||||
accountSerialNumber AS basic_deposit_account_number,
|
||||
registeredCapitalVaildTime AS subscribed_at,
|
||||
companyStatus AS company_status,
|
||||
financialManager AS financial_officer,
|
||||
supervisor AS supervisor,
|
||||
@ -140,7 +141,9 @@ SQL);
|
||||
$rows = $query->select(<<<SQL
|
||||
SELECT
|
||||
s.Id AS id,
|
||||
s.sotreId AS store_id,
|
||||
s.openTime AS opened_at,
|
||||
s.renewalTime AS renewed_at,
|
||||
s.erpShopName AS erp_store_name,
|
||||
s.unifiedSocialCreditCode AS unified_social_credit_code,
|
||||
c.businessLicenseName AS company_name,
|
||||
@ -149,7 +152,9 @@ SELECT
|
||||
s.platform AS platform,
|
||||
s.responsibleDepartment AS profit_center,
|
||||
s.shopHead AS store_manager,
|
||||
NULL AS department_manager,
|
||||
s.shopState AS store_status,
|
||||
s.shopBankAccount AS withdrawal_account,
|
||||
s.shopType AS store_type,
|
||||
s.channelType AS channel_mode
|
||||
FROM rpt.fys_shopInfo AS s
|
||||
@ -188,11 +193,16 @@ SQL);
|
||||
$rows = $query->select(<<<SQL
|
||||
SELECT
|
||||
Id AS id,
|
||||
trademarkNumber AS trademark_number,
|
||||
trademarkName AS trademark_name,
|
||||
internationalClassification AS trademark_type,
|
||||
trademarkStatus AS trademark_status,
|
||||
registrationDate AS registration_date,
|
||||
termOfValidity AS expiry_date,
|
||||
approvedCommodities AS approved_goods,
|
||||
transferenceUser AS final_holder,
|
||||
registrationAddress AS registered_address,
|
||||
custodyDepartment AS custody_department,
|
||||
remark AS notes
|
||||
FROM rpt.fys_TrademarkInfo
|
||||
WHERE isDelete = 0
|
||||
@ -212,6 +222,11 @@ SQL);
|
||||
return $this->normalize($scope, $this->mirror->paginate($scope, $page, $perPage));
|
||||
}
|
||||
|
||||
if ((app(\App\Utilities\SystemDatabaseConfiguration::class)->current()['mode'] ?? '') === 'bootstrap'
|
||||
&& ! config('upstreams.local_configuration.enabled', false)) {
|
||||
throw new ServiceUnavailableHttpException(null, '当前仍使用本机临时引导库。请在“系统设置 → 数据库管理 → 系统数据库”连接团队共享数据库,然后重新登录;无需另配上游 SQL Server。');
|
||||
}
|
||||
|
||||
$cache = $this->caches->forSource(self::SOURCE);
|
||||
|
||||
return $cache->remember($scope, compact('page', 'perPage'), function () use ($resolver) {
|
||||
@ -239,6 +254,7 @@ SQL);
|
||||
{
|
||||
$row = $this->toUtf8($row);
|
||||
$row['establishment_date'] = $this->date($row['establishment_date'] ?? null);
|
||||
$row['subscribed_at'] = $this->date($row['subscribed_at'] ?? null);
|
||||
$row['company_status'] = match ((string) ($row['company_status'] ?? '')) {
|
||||
'0' => '正常',
|
||||
'1' => '已注销',
|
||||
@ -252,6 +268,7 @@ SQL);
|
||||
{
|
||||
$row = $this->toUtf8($row);
|
||||
$row['opened_at'] = $this->date($row['opened_at'] ?? null);
|
||||
$row['renewed_at'] = $this->date($row['renewed_at'] ?? null);
|
||||
$row['store_status'] = match ((string) ($row['store_status'] ?? '')) {
|
||||
'0' => '正常',
|
||||
'3' => '关闭中',
|
||||
|
||||
@ -7,6 +7,14 @@ use App\Utilities\SidebarPermissions;
|
||||
|
||||
class ShowInAdmin
|
||||
{
|
||||
private const PROTOTYPE_NAV_SECTIONS = [
|
||||
'workspace',
|
||||
'company-management',
|
||||
'trademark-management',
|
||||
'store-management',
|
||||
'prototype-finance-reports',
|
||||
];
|
||||
|
||||
/**
|
||||
* Build the business navigation defined by the imported directory workbook.
|
||||
*/
|
||||
@ -15,7 +23,25 @@ class ShowInAdmin
|
||||
$menu = $event->menu;
|
||||
|
||||
foreach (SidebarPermissions::navigationFor(user()) as $sectionIndex => $section) {
|
||||
// 订单数据按配置顺序展示,让“原始数据”保持在新增子页面之前。
|
||||
if (in_array($section['slug'], self::PROTOTYPE_NAV_SECTIONS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! empty($section['external_url'])) {
|
||||
$menu->url(
|
||||
$section['external_url'],
|
||||
$section['title'],
|
||||
($sectionIndex + 1) * 10,
|
||||
[
|
||||
'icon' => $section['icon'] ?? '',
|
||||
'target' => '_blank',
|
||||
'rel' => 'noopener noreferrer',
|
||||
]
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$pages = $section['slug'] === 'order-data'
|
||||
? array_values($section['pages'] ?? [])
|
||||
: $this->sortByTitleLength($section['pages'] ?? []);
|
||||
|
||||
@ -39,7 +39,7 @@ return [
|
||||
'directory' => [
|
||||
'icon' => 'business',
|
||||
'status' => '既有只读上游',
|
||||
'description' => '沿用现有公司基础资料只读链路;Excel 中暂未匹配到的城市、认缴时间等字段保留为空。',
|
||||
'description' => '优先读取共享业务镜像中的公司资料;未匹配的城市等字段保留为空。',
|
||||
'empty' => '尚未读取到公司基础资料。请检查共享镜像或上游同步状态。',
|
||||
'fields' => [
|
||||
['name' => 'unified_social_credit_code', 'label' => '统一社会信用代码'],
|
||||
@ -112,7 +112,7 @@ return [
|
||||
'directory' => [
|
||||
'icon' => 'verified',
|
||||
'status' => '既有只读上游',
|
||||
'description' => '沿用现有商标资料只读链路;当前已匹配名称、状态、注册时间和有效期,其余 Excel 字段待接入。',
|
||||
'description' => '优先读取共享业务镜像中的商标权属、有效期和保管资料。',
|
||||
'empty' => '尚未读取到商标基础资料。请检查共享镜像或上游同步状态。',
|
||||
'fields' => [
|
||||
['name' => 'trademark_name', 'label' => '商标名称'],
|
||||
@ -190,7 +190,7 @@ return [
|
||||
'directory' => [
|
||||
'icon' => 'storefront',
|
||||
'status' => '既有只读上游',
|
||||
'description' => '沿用现有店铺资料只读链路;Excel 中的店铺 ID、续签时间和提现账户待字段映射后补充。',
|
||||
'description' => '优先读取共享业务镜像中的店铺资料、店铺 ID 和续签时间;提现账户仅显示配置状态。',
|
||||
'empty' => '尚未读取到店铺基础资料。请检查共享镜像或上游同步状态。',
|
||||
'fields' => [
|
||||
['name' => 'platform', 'label' => '平台'],
|
||||
|
||||
@ -28,6 +28,12 @@ php artisan business-mirror:register database_17 public example_orders \
|
||||
|
||||
## 页面读取与同步状态
|
||||
|
||||
### 已有公司、店铺、商标页在新人电脑没有数据
|
||||
|
||||
这些页面优先读取 `caiwu_business_mirror`。本机 Docker 正常、临时引导库有用户,并不代表已经连接团队共享库。Codex 应先在最新主线执行 `deploy/local-init.sh`,通过系统数据库入口完成一次共享接入和重新登录,再运行 `system-database:verify` 并检查相应 `business-mirror:status`。镜像连接默认复用系统数据库主机、端口、账号和 TLS,不要求新人另填 SQL Server 密码,也不要把 `furniture3d_prod` 填成 `operations_replica`。
|
||||
|
||||
已有镜像可读时,上游开关关闭或网络暂不可达不应阻断页面。读取失败不能解释为真实记录数为 0。只有探索尚未接入的新表时,才需要使用对应上游的只读连接;不要为了让已有资料页出数据,在每个开发者的引导库重复登记生产来源。
|
||||
|
||||
新页面在原有公司、角色和数据权限校验之后调用 `BusinessMirrorDataReader::paginate($datasetKey, $filters, $page, $perPage)`。它只读业务镜像,返回完整总数、稳定分页、实际同步状态和最近完成时间。来源暂时关闭或不可达时,已完成镜像仍可读取;无首次镜像时明确报“尚未完成首次同步”,不登记逐页快照、不从页面直接访问上游。
|
||||
|
||||
通用筛选只接受已声明字段的等值条件;复杂业务联表、金额口径、公司行级过滤继续由固定 Repository 实现。不能把此内部读取器当作绕过业务权限的公开接口。
|
||||
|
||||
@ -63,6 +63,7 @@ legalRepresentative AS legal_representative,
|
||||
registeredAddress AS registered_address,
|
||||
businessScope AS business_scope,
|
||||
accountSerialNumber AS basic_deposit_account_number,
|
||||
registeredCapitalVaildTime AS subscribed_at,
|
||||
companyStatus AS company_status,
|
||||
financialManager AS financial_officer,
|
||||
supervisor AS supervisor,
|
||||
@ -89,7 +90,9 @@ SQL);
|
||||
->leftJoin('ops_fys_company_info AS c', 'c.Id', '=', 'company_keys.latest_id')
|
||||
->selectRaw(<<<'SQL'
|
||||
s.Id AS id,
|
||||
s.sotreId AS store_id,
|
||||
s.openTime AS opened_at,
|
||||
s.renewalTime AS renewed_at,
|
||||
s.erpShopName AS erp_store_name,
|
||||
s.unifiedSocialCreditCode AS unified_social_credit_code,
|
||||
c.businessLicenseName AS company_name,
|
||||
@ -98,7 +101,9 @@ NULL AS trademark_name,
|
||||
s.platform AS platform,
|
||||
s.responsibleDepartment AS profit_center,
|
||||
s.shopHead AS store_manager,
|
||||
NULL AS department_manager,
|
||||
s.shopState AS store_status,
|
||||
s.shopBankAccount AS withdrawal_account,
|
||||
s.shopType AS store_type,
|
||||
s.channelType AS channel_mode
|
||||
SQL);
|
||||
@ -113,11 +118,16 @@ SQL);
|
||||
->where('isDelete', 0)
|
||||
->selectRaw(<<<'SQL'
|
||||
Id AS id,
|
||||
trademarkNumber AS trademark_number,
|
||||
trademarkName AS trademark_name,
|
||||
internationalClassification AS trademark_type,
|
||||
trademarkStatus AS trademark_status,
|
||||
registrationDate AS registration_date,
|
||||
termOfValidity AS expiry_date,
|
||||
approvedCommodities AS approved_goods,
|
||||
transferenceUser AS final_holder,
|
||||
registrationAddress AS registered_address,
|
||||
custodyDepartment AS custody_department,
|
||||
remark AS notes
|
||||
SQL);
|
||||
|
||||
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -1,6 +1,15 @@
|
||||
@php
|
||||
$directoryRows = $rows ?? collect();
|
||||
$directoryFields = $directory['fields'] ?? [];
|
||||
$directoryFields = collect($directoryFields)->flatMap(function (array $field): array {
|
||||
if ($field['name'] !== 'recent_three_month_sales') {
|
||||
return [$field];
|
||||
}
|
||||
return array_map(static function (int $monthsAgo): array {
|
||||
$month = now()->startOfMonth()->subMonthsNoOverflow($monthsAgo);
|
||||
return ['name' => $month->format('sales_Y_m'), 'label' => $month->format('n月销量'), 'kind' => 'number'];
|
||||
}, range(3, 1));
|
||||
})->all();
|
||||
$directoryIsPaginated = is_object($directoryRows) && method_exists($directoryRows, 'total');
|
||||
$directoryTotal = $directoryIsPaginated ? $directoryRows->total() : (is_countable($directoryRows) ? count($directoryRows) : 0);
|
||||
$directoryCurrentPage = $directoryIsPaginated ? $directoryRows->currentPage() : 1;
|
||||
@ -274,7 +283,7 @@
|
||||
<td colspan="{{ count($directoryFields) + 1 }}">
|
||||
<div class="directory-table-empty">
|
||||
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
|
||||
<span>{{ $directory['empty'] ?? '尚未同步数据。' }}</span>
|
||||
<span>{{ !empty($upstreamError) ? '资料暂时无法读取,请按上方提示检查连接;这不代表业务数据为 0。' : ($directory['empty'] ?? '尚未同步数据。') }}</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -283,7 +292,7 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if ($directoryIsPaginated)
|
||||
@if ($directoryIsPaginated && empty($upstreamError))
|
||||
<footer class="directory-table-pagination">
|
||||
<span>共 {{ $directoryTotal }} 条 · 第 {{ $directoryCurrentPage }} / {{ $directoryLastPage }} 页</span>
|
||||
<span>
|
||||
|
||||
@ -0,0 +1,71 @@
|
||||
@php
|
||||
$slug = $page['slug'] ?? '';
|
||||
$recentCompletedMonthSalesColumns = collect(range(3, 1))
|
||||
->map(fn (int $monthsAgo): string => now()
|
||||
->startOfMonth()
|
||||
->subMonthsNoOverflow($monthsAgo)
|
||||
->format('n月销量'))
|
||||
->all();
|
||||
$definitions = [
|
||||
'overview' => [
|
||||
'title' => '经营总览', 'description' => '跨主体经营、风险与任务的当日视图',
|
||||
'columns' => ['指标', '当前值', '变化', '状态'],
|
||||
],
|
||||
'warnings' => [
|
||||
'title' => '预警页面', 'description' => '集中查看经营、税务、人员与资质风险',
|
||||
'columns' => ['预警编号', '风险等级', '类型', '关联对象', '影响金额', '当前状态'],
|
||||
],
|
||||
'tasks' => [
|
||||
'title' => '待办事项', 'description' => '跟进任务节点、责任人与处理时限',
|
||||
'columns' => ['待办编号', '业务类型', '关联对象', '任务内容', '优先级', '截止日期', '状态'],
|
||||
],
|
||||
'profit-report' => [
|
||||
'title' => '公司利润报表', 'description' => '查看公司主体利润数据',
|
||||
'columns' => ['公司主体', '关联店铺', '本月收入', '本月成本', '本月利润', '利润率'],
|
||||
],
|
||||
'asset-report' => [
|
||||
'title' => '公司资产报表', 'description' => '查看公司主体资产数据',
|
||||
'columns' => ['公司主体', '银行账户', '店铺资产', '保证金', '资产合计', '数据状态'],
|
||||
],
|
||||
'people-inventory' => [
|
||||
'title' => '人员盘点表',
|
||||
'description' => '盘点法人、股东、监事及开票员任职',
|
||||
'columns' => ['人员名称', '花名', '职位', '标签', '税务局账号', '税务局密码', '部门', '部门负责人', '身份证号码', '实名手机号', '电话卡保管人', '身份证地址', '公司类型', '合计担任公司数量', '保留担任公司数量', '可担任公司数量'],
|
||||
],
|
||||
'company-progress' => [
|
||||
'title' => '公司进度表', 'description' => '跟踪公司注册、变更和注销办理进度',
|
||||
'columns' => ['公司名称', '公司类型', '当前进度', '是否完成', '完成时间', '办理人'],
|
||||
],
|
||||
'company-information' => [
|
||||
'title' => '公司基础信息', 'description' => '维护公司主体资料与存续状态',
|
||||
'columns' => ['统一社会信用代码', '公司名称', '纳税人类型', '公司状态', '认缴注册资本(万元)', '注册时间', '法人', '股东', '监事', '开票员', '城市', '注册地址', '经营范围', '基本存款账户信息', '认缴时间'],
|
||||
],
|
||||
'entity-report' => [
|
||||
'title' => '主体维度经营报表', 'description' => '按公司主体汇总店铺经营表现',
|
||||
'columns' => ['市', '主体', '公司类型', 'ERP系统店铺名称', '店铺所属利润中心', '店铺负责人', '部门负责人', '开店时间', '状态', ...$recentCompletedMonthSalesColumns, '连续12个月店铺累计销量', '连续12个月主体累计销量', '能否变更主体', '规划', '新主体(变更流程同步新主体)'],
|
||||
],
|
||||
'trademark-progress' => [
|
||||
'title' => '商标进度表', 'description' => '跟踪商标续展与变更业务',
|
||||
'columns' => ['商标名称', '商标号', '业务', '进度', '是否完成', '完成时间', '办理人'],
|
||||
],
|
||||
'trademark-information' => [
|
||||
'title' => '商标基础信息', 'description' => '维护商标权属、有效期与保管信息',
|
||||
'columns' => ['商标名称', '商标号', '商标状态', '注册时间', '有效期', '国际分类', '核定使用商品', '最终持有人', '注册地址', '保管部门'],
|
||||
],
|
||||
'store-information' => [
|
||||
'title' => '店铺基础信息', 'description' => '维护店铺资料、经营状态与续签信息',
|
||||
'columns' => ['平台', '店铺ID', '店铺名称', '店铺主体', '店铺状态', '商标', '商标号', '业务部门', '店铺负责人', '部门负责人', '开店时间', '续签时间', '提现账户'],
|
||||
],
|
||||
'store-change-progress' => [
|
||||
'title' => '店铺变更进度表', 'description' => '跟踪店铺主体变更办理进度',
|
||||
'columns' => ['公司名称', 'ERP系统店铺名称', '部门', '店铺负责人', '部门负责人', '账号', '密码', '接替公司', '进度', '是否完成', '完成时间', '办理人'],
|
||||
],
|
||||
'store-report' => [
|
||||
'title' => '店铺维度报表', 'description' => '查看店铺销量及历史主体归属',
|
||||
'columns' => ['ERP系统店铺名称', '店铺ID(唯一值)', '店铺所属利润中心', '店铺负责人', '部门负责人', '开店时间', '状态', '能否变更', '主体', '公司类型', '生效时间', '失效时间', '期间销量', '1月销量', '2月销量', '3月销量', '4月销量', '5月销量', '6月销量', '7月销量', '8月销量', '9月销量', '10月销量', '11月销量', '12月销量'],
|
||||
],
|
||||
];
|
||||
$definition = $definitions[$slug] ?? ['title' => $page['title'] ?? '业务页面', 'description' => '', 'columns' => [], 'rows' => []];
|
||||
@endphp
|
||||
|
||||
@include('business.pages.partials.prototype-table', ['prototypeColumns' => $definition['columns']])
|
||||
@ -0,0 +1,34 @@
|
||||
@php
|
||||
$columns = $prototypeColumns ?? [];
|
||||
@endphp
|
||||
|
||||
<div class="biz-card mt-6">
|
||||
<div class="biz-card-head">
|
||||
<div class="biz-card-title">
|
||||
<span class="material-icons-outlined" aria-hidden="true">table_view</span>
|
||||
<span>{{ $page['title'] ?? '业务页面' }} · 数据列表</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="biz-table-wrap">
|
||||
<table class="biz-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
@foreach ($columns as $column)
|
||||
<th>{{ $column }}</th>
|
||||
@endforeach
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td colspan="{{ count($columns) + 1 }}">
|
||||
<div class="biz-empty">
|
||||
<span class="material-icons-outlined" aria-hidden="true">inbox</span>
|
||||
<div>页面结构已接入,业务数据尚未接入。</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
1
resources/views/business/pages/workspace/tasks.blade.php
Normal file
1
resources/views/business/pages/workspace/tasks.blade.php
Normal file
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -0,0 +1 @@
|
||||
@include('business.pages.partials.prototype-page')
|
||||
@ -131,16 +131,90 @@
|
||||
<img src="{{ asset('public/img/akaunting-logo-gold.svg') }}" class="w-6 h-6" alt="Akaunting" />
|
||||
</div>
|
||||
|
||||
<div class="flex ltr:ml-2 rtl:mr-2">
|
||||
<div class="flex flex-col ltr:ml-2 rtl:mr-2">
|
||||
<span class="w-28 ltr:text-left rtl:text-right block text-base truncate">
|
||||
财务管理
|
||||
税务
|
||||
</span>
|
||||
<span class="text-xs text-gray-400">财务运营管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@stack('main_menu_admin_start')
|
||||
|
||||
@php
|
||||
$prototypeGroups = [
|
||||
['title' => '工作台', 'items' => [
|
||||
['workspace', 'overview', '经营总览', 'monitoring'],
|
||||
]],
|
||||
['title' => '经营报表', 'items' => [
|
||||
['company-management', 'subject-operating-report', '主体维度经营报表', 'domain'],
|
||||
['store-management', 'store-report', '店铺维度报表', 'storefront'],
|
||||
['company-management', 'personnel-inventory', '人员盘点表', 'groups'],
|
||||
]],
|
||||
['title' => '基础资料', 'items' => [
|
||||
['store-management', 'basic-information', '店铺基础信息', 'shopping_bag'],
|
||||
['company-management', 'basic-information', '公司基础信息', 'badge'],
|
||||
['trademark-management', 'information', '商标基础信息', 'workspace_premium'],
|
||||
]],
|
||||
['title' => '业务进度', 'items' => [
|
||||
['company-management', 'company-progress', '公司进度表', 'timeline'],
|
||||
['store-management', 'change-progress', '店铺变更进度表', 'swap_horiz'],
|
||||
['trademark-management', 'trademark-progress', '商标进度表', 'format_list_bulleted'],
|
||||
['workspace', 'tasks', '待办事项', 'assignment_turned_in'],
|
||||
['workspace', 'warnings', '预警页面', 'warning_amber'],
|
||||
]],
|
||||
['title' => '财务报表', 'items' => [
|
||||
['prototype-finance-reports', 'profit-report', '公司利润报表', 'request_quote'],
|
||||
['prototype-finance-reports', 'asset-report', '公司资产报表', 'account_balance'],
|
||||
]],
|
||||
];
|
||||
$visiblePages = [];
|
||||
foreach (\App\Utilities\SidebarPermissions::navigationFor(user()) as $visibleSection) {
|
||||
foreach ($visibleSection['pages'] ?? [] as $visiblePage) {
|
||||
$visiblePages[$visibleSection['slug'] . '/' . $visiblePage['slug']] = $visiblePage['title'];
|
||||
}
|
||||
}
|
||||
$prototypeGroups = array_values(array_filter(array_map(function ($group) use ($visiblePages) {
|
||||
$group['items'] = array_values(array_filter($group['items'], fn ($item) => isset($visiblePages[$item[0] . '/' . $item[1]])));
|
||||
foreach ($group['items'] as &$item) {
|
||||
$item[2] = $visiblePages[$item[0] . '/' . $item[1]];
|
||||
}
|
||||
unset($item);
|
||||
return $group;
|
||||
}, $prototypeGroups), fn ($group) => $group['items'] !== []));
|
||||
@endphp
|
||||
|
||||
<style>
|
||||
.prototype-tag-navigation { margin: 0 -0.25rem 1.25rem; }
|
||||
.prototype-tag-group { margin-bottom: 1.15rem; }
|
||||
.prototype-tag-group-title { padding: 0 0.75rem 0.45rem; color: var(--vip-sidebar-gold-soft, #DDD0AA); font-size: 0.6875rem; line-height: 1rem; }
|
||||
.prototype-tag-link { position: relative; display: flex; min-height: 2.4rem; align-items: center; gap: 0.65rem; margin: 0.125rem 0; padding: 0.55rem 0.75rem; border-radius: 0.25rem; color: var(--vip-sidebar-gold-soft, #DDD0AA); font-size: 0.75rem; line-height: 1rem; }
|
||||
.prototype-tag-link:hover { color: var(--vip-sidebar-gold, #F0E4BF); background: rgba(240, 228, 191, 0.08); }
|
||||
.prototype-tag-link.is-active { color: var(--vip-rail-gold, #F2E7C5); background: var(--vip-sidebar-active, #2A2927); }
|
||||
.prototype-tag-link:focus-visible { outline: 2px solid var(--vip-sidebar-gold, #F0E4BF); outline-offset: 2px; }
|
||||
.prototype-tag-link .material-icons-outlined { width: 1.25rem; flex: 0 0 1.25rem; font-size: 1rem; text-align: center; }
|
||||
.prototype-tag-link > span:nth-child(2) { flex: 1; white-space: nowrap; }
|
||||
</style>
|
||||
|
||||
<div class="prototype-tag-navigation" aria-label="税务功能导航">
|
||||
@foreach ($prototypeGroups as $group)
|
||||
<section class="prototype-tag-group">
|
||||
<div class="prototype-tag-group-title">{{ $group['title'] }}</div>
|
||||
@foreach ($group['items'] as [$navSection, $navPage, $navTitle, $navIcon])
|
||||
<a
|
||||
href="{{ route('business.pages.show', ['section' => $navSection, 'page' => $navPage]) }}"
|
||||
@class(['prototype-tag-link', 'is-active' => request()->route('section') === $navSection && request()->route('page') === $navPage])
|
||||
@if (request()->route('section') === $navSection && request()->route('page') === $navPage) aria-current="page" @endif
|
||||
>
|
||||
<span class="material-icons-outlined" aria-hidden="true">{{ $navIcon }}</span>
|
||||
<span>{{ $navTitle }}</span>
|
||||
</a>
|
||||
@endforeach
|
||||
</section>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
<div class="main-menu transform">
|
||||
{!! menu('admin') !!}
|
||||
</div>
|
||||
|
||||
75
tests/Feature/Business/PrototypePageTest.php
Normal file
75
tests/Feature/Business/PrototypePageTest.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class PrototypePageTest extends FeatureTestCase
|
||||
{
|
||||
public function testNavigationUsesConfiguredPermissionsAndTitles(): void
|
||||
{
|
||||
setting()->set('system.sidebar_permissions', [
|
||||
'configured' => ['user_' . $this->user->id],
|
||||
'subjects' => ['user_' . $this->user->id => ['company-management/subject-operating-report']],
|
||||
]);
|
||||
setting()->set('navigation.titles', ['pages' => ['company-management' => ['subject-operating-report' => '主体经营测试']]]);
|
||||
$response = $this->loginAs()->get(route('business.pages.show', ['section' => 'company-management', 'page' => 'subject-operating-report']));
|
||||
$response->assertOk()->assertSeeText('主体经营测试');
|
||||
$html = $response->getContent();
|
||||
preg_match('/<div class="prototype-tag-navigation".*?<div class="main-menu/s', $html, $matches);
|
||||
$this->assertNotEmpty($matches);
|
||||
$this->assertStringContainsString('主体经营测试', $matches[0]);
|
||||
$this->assertStringNotContainsString('/workspace/store-management/basic-information', $matches[0]);
|
||||
$this->assertStringNotContainsString('/workspace/workspace/', $matches[0]);
|
||||
$this->assertStringNotContainsString('<em>', $matches[0]);
|
||||
}
|
||||
|
||||
public function testBootstrapConnectionFailureDoesNotDisplayZeroBusinessRecords(): void
|
||||
{
|
||||
$this->mock(\App\Business\UpstreamBusinessInformation::class)
|
||||
->shouldReceive('paginate')->once()->andThrow(new \Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException(null, '当前仍使用本机临时引导库,请连接团队共享数据库。'));
|
||||
$this->loginAs()->get(route('business.pages.show', ['section' => 'store-management', 'page' => 'basic-information']))
|
||||
->assertOk()->assertSeeText('请连接团队共享数据库')->assertDontSeeText('共 0 条');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Carbon::setTestNow();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function testEntityReportShowsPreviousThreeCompleteMonthsAsSeparateSalesHeaders(): void
|
||||
{
|
||||
Carbon::setTestNow('2026-08-20 12:00:00');
|
||||
|
||||
$augustResponse = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'company-management',
|
||||
'page' => 'subject-operating-report',
|
||||
]));
|
||||
|
||||
$augustResponse
|
||||
->assertOk()
|
||||
->assertSeeTextInOrder(['5月销量', '6月销量', '7月销量'])
|
||||
->assertDontSeeText('8月销量')
|
||||
->assertDontSeeText('展示近3个月月度销量');
|
||||
|
||||
Carbon::setTestNow('2026-09-01 00:00:00');
|
||||
|
||||
$septemberResponse = $this->get(route('business.pages.show', [
|
||||
'section' => 'company-management',
|
||||
'page' => 'subject-operating-report',
|
||||
]));
|
||||
|
||||
$septemberResponse
|
||||
->assertOk()
|
||||
->assertSeeTextInOrder(['6月销量', '7月销量', '8月销量'])
|
||||
->assertDontSeeText('5月销量')
|
||||
->assertDontSeeText('9月销量');
|
||||
|
||||
Carbon::setTestNow('2027-01-01 00:00:00');
|
||||
$this->get(route('business.pages.show', ['section' => 'company-management', 'page' => 'subject-operating-report']))
|
||||
->assertOk()->assertSeeTextInOrder(['10月销量', '11月销量', '12月销量']);
|
||||
}
|
||||
}
|
||||
@ -57,6 +57,9 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
['Id' => 6, 'trademarkName' => '已删除', 'internationalClassification' => '20', 'trademarkStatus' => '0', 'registrationDate' => null, 'termOfValidity' => null, 'remark' => null, 'isDelete' => 1],
|
||||
]);
|
||||
$this->markReady();
|
||||
$connection->table('ops_fys_company_info')->where('Id', 1)->update(['registeredCapitalVaildTime' => '2027-01-01']);
|
||||
$connection->table('ops_fys_shop_info')->where('Id', 8)->update(['sotreId' => 'STORE-8', 'renewalTime' => '2027-08-14', 'shopBankAccount' => 'fixture-account']);
|
||||
$connection->table('ops_fys_trademark_info')->where('Id', 5)->update(['trademarkNumber' => 'TM-5', 'approvedCommodities' => '家具', 'transferenceUser' => '测试持有人', 'registrationAddress' => '测试地址', 'custodyDepartment' => '资料部']);
|
||||
|
||||
$companies = $this->repository->paginate('company-information.read', 2, 2);
|
||||
$store = $this->repository->paginate('store-information.read', 1, 20);
|
||||
@ -67,6 +70,14 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame('新公司', $store['data'][0]['company_name']);
|
||||
$this->assertSame(1, $trademarks['total']);
|
||||
$this->assertSame('有效商标', $trademarks['data'][0]['trademark_name']);
|
||||
$this->assertSame('2027-01-01', $companies['data'][0]['subscribed_at']);
|
||||
$this->assertSame('STORE-8', $store['data'][0]['store_id']);
|
||||
$this->assertSame('2027-08-14', $store['data'][0]['renewed_at']);
|
||||
$this->assertSame('负责人', $store['data'][0]['store_manager']);
|
||||
$this->assertSame('fixture-account', $store['data'][0]['withdrawal_account']);
|
||||
$this->assertSame('TM-5', $trademarks['data'][0]['trademark_number']);
|
||||
$this->assertSame('家具', $trademarks['data'][0]['approved_goods']);
|
||||
$this->assertSame('测试持有人', $trademarks['data'][0]['final_holder']);
|
||||
}
|
||||
|
||||
public function test_dataset_is_not_ready_until_initial_full_sync_is_marked_complete(): void
|
||||
@ -89,16 +100,19 @@ class BusinessInformationMirrorRepositoryTest extends TestCase
|
||||
$table->string('legalRepresentative')->nullable(); $table->string('registeredAddress')->nullable(); $table->text('businessScope')->nullable();
|
||||
$table->string('accountSerialNumber')->nullable(); $table->string('companyStatus')->nullable(); $table->string('financialManager')->nullable();
|
||||
$table->string('supervisor')->nullable(); $table->string('taxCategoryAndType')->nullable();
|
||||
$table->dateTime('registeredCapitalVaildTime')->nullable();
|
||||
});
|
||||
$schema->create('ops_fys_shop_info', function (Blueprint $table): void {
|
||||
$table->integer('Id'); $table->dateTime('openTime')->nullable(); $table->string('erpShopName')->nullable(); $table->string('unifiedSocialCreditCode')->nullable();
|
||||
$table->string('trademarkNumber')->nullable(); $table->string('platform')->nullable(); $table->string('responsibleDepartment')->nullable();
|
||||
$table->string('shopHead')->nullable(); $table->string('shopState')->nullable(); $table->string('shopType')->nullable(); $table->string('channelType')->nullable();
|
||||
$table->string('sotreId')->nullable(); $table->dateTime('renewalTime')->nullable(); $table->string('shopBankAccount')->nullable();
|
||||
});
|
||||
$schema->create('ops_fys_trademark_info', function (Blueprint $table): void {
|
||||
$table->integer('Id'); $table->string('trademarkName')->nullable(); $table->string('internationalClassification')->nullable();
|
||||
$table->string('trademarkStatus')->nullable(); $table->dateTime('registrationDate')->nullable(); $table->dateTime('termOfValidity')->nullable();
|
||||
$table->text('remark')->nullable(); $table->integer('isDelete')->default(0);
|
||||
foreach (['trademarkNumber', 'approvedCommodities', 'transferenceUser', 'registrationAddress', 'custodyDepartment'] as $column) $table->string($column)->nullable();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@ use App\Business\UpstreamBusinessInformation;
|
||||
use Closure;
|
||||
use Mockery;
|
||||
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\InstallationDetailMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCache;
|
||||
use Modules\EcommerceFinance\Services\UpstreamQueryCacheFactory;
|
||||
use Tests\TestCase;
|
||||
@ -20,6 +21,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
string $page,
|
||||
string $scope,
|
||||
): void {
|
||||
config(['upstreams.local_configuration.enabled' => true]);
|
||||
$cache = new class extends UpstreamQueryCache {
|
||||
public array $calls = [];
|
||||
|
||||
@ -49,7 +51,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->once()->with($scope)->andReturnFalse();
|
||||
|
||||
$information = new class($caches, $mirror) extends UpstreamBusinessInformation {
|
||||
$information = new class($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class)) extends UpstreamBusinessInformation {
|
||||
public array $snapshots = [];
|
||||
|
||||
public function snapshot(string $scope, array $parameters): array
|
||||
@ -78,6 +80,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
string $page,
|
||||
string $scope,
|
||||
): void {
|
||||
config(['operations_replica.enabled' => false, 'upstreams.stored_configuration.enabled' => false]);
|
||||
$cache = new class extends UpstreamQueryCache {
|
||||
public function __construct() {}
|
||||
|
||||
@ -99,7 +102,7 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->app['request']->merge(['page' => 2]);
|
||||
$paginator = (new UpstreamBusinessInformation($caches, $mirror))
|
||||
$paginator = (new UpstreamBusinessInformation($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class)))
|
||||
->paginate($section, $page, 25);
|
||||
|
||||
$this->assertSame(2, $paginator->currentPage());
|
||||
@ -107,6 +110,19 @@ class UpstreamBusinessInformationTest extends TestCase
|
||||
$this->assertSame(10, $paginator->items()[0]->id);
|
||||
}
|
||||
|
||||
public function test_bootstrap_without_mirror_explains_shared_connection_instead_of_requesting_upstream_credentials(): void
|
||||
{
|
||||
config(['system_database.mode' => 'bootstrap', 'upstreams.local_configuration.enabled' => false]);
|
||||
$caches = Mockery::mock(UpstreamQueryCacheFactory::class);
|
||||
$caches->shouldNotReceive('forSource');
|
||||
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->once()->with('store-information.read')->andReturnFalse();
|
||||
$this->expectException(\Symfony\Component\HttpKernel\Exception\ServiceUnavailableHttpException::class);
|
||||
$this->expectExceptionMessage('无需另配上游 SQL Server');
|
||||
(new UpstreamBusinessInformation($caches, $mirror, Mockery::mock(InstallationDetailMirrorRepository::class)))
|
||||
->paginate('store-management', 'basic-information');
|
||||
}
|
||||
|
||||
public static function businessInformationPages(): array
|
||||
{
|
||||
return [
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user