'date', 'opened_at' => 'date', 'establishment_date' => 'date', 'registration_date' => 'date', 'expiry_date' => 'date', 'invoice_date' => 'date', 'trigger_store_change' => 'boolean', 'trigger_company_register' => 'boolean', 'invoice_type' => 'select', 'account_type' => 'select', 'receiving_method' => 'select', 'platform' => 'select', 'receiving_type' => 'select', 'notes' => 'textarea', 'business_scope' => 'textarea', 'shareholders' => 'textarea', 'subscribed_capital' => 'number', ]; private const SELECT_OPTIONS = [ 'invoice_type' => ['普票', '专票'], 'account_type' => ['基本户', '一般户', '个人账户', '个人微信', '个人支付宝'], 'receiving_method' => ['支付宝', '银行卡'], 'platform' => ['京东', '抖音', '拼多多', '淘宝', '天猫', '淘工厂', '唯品会', '得物'], 'receiving_type' => ['对公', '个人'], ]; private const COLUMN_LABELS = [ 'unified_social_credit_code' => '统一社会信用代码', 'company_name' => '公司名称', 'subscribed_capital' => '认缴注册资本(万元)', 'company_type' => '类型', 'legal_representative' => '法定代表人', 'establishment_date' => '成立日期', 'registration_status' => '注册状态', 'registered_address' => '注册地址(住所)', 'business_scope' => '经营范围', 'basic_deposit_account_number' => '基本存款账户编号', 'company_status' => '公司状态', 'financial_officer' => '财务负责人', 'supervisor' => '监事', 'taxpayer_category' => '纳税类别及类型', 'shareholders' => '股东', 'notes' => '备注', 'registration_type' => '注册类型', 'change_type' => '变更类型', 'change_status' => '变更状态', 'change_date' => '变更日期', 'cancellation_status' => '注销状态', 'trademark_name' => '商标名称', 'trademark_type' => '商标类型', 'trademark_status' => '商标状态', 'registration_date' => '注册日期', 'expiry_date' => '到期日期', 'opened_at' => '开店时间', 'erp_store_name' => 'ERP系统店铺名称', 'trademark_number' => '商标号', 'platform' => '平台', 'profit_center' => '店铺所属利润中心', 'store_manager' => '店铺负责人', 'store_status' => '店铺状态', 'store_type' => '店铺类型', 'channel_mode' => '渠道模式', 'store_name' => '店铺名称', 'store_platform' => '店铺平台', 'opening_status' => '开店状态', 'closing_status' => '关店状态', 'store_entity' => '店铺主体', 'receiving_account_info' => '收款账户信息', 'historical_entity' => '历史主体', ]; /** * Currency columns (rendered with two decimals and right-aligned). */ private const CURRENCY_HINTS = [ 'real_ship_revenue', 'zhongcai_revenue', 'refund_amount', 'net_revenue', 'tax_revenue', 'tax_refund', 'tax_net_revenue', 'revenue_diff', 'final_tax_revenue', 'product_cost', 'logistics_cost', 'platform_invoice', 'labor_cost', 'other_reimbursement', 'profit', 'provided_revenue', 'declared_revenue', 'goods_special_amount', 'goods_special_tax', 'goods_normal_amount', 'goods_normal_tax', 'goods_unbilled_amount', 'goods_unbilled_tax', 'goods_subtotal_amount', 'goods_subtotal_tax', 'service_special_amount', 'service_special_tax', 'service_normal_amount', 'service_normal_tax', 'service_unbilled_amount', 'service_unbilled_tax', 'service_subtotal_amount', 'service_subtotal_tax', 'total_revenue', 'total_tax', 'input_goods_amount', 'input_goods_tax', 'input_service_amount', 'input_service_tax', 'input_other_amount', 'input_other_tax', 'input_subtotal_amount', 'input_subtotal_tax', 'extra_credit', 'retained_credit', 'vat_payable', 'city_tax', 'education_surcharge', 'local_education_surcharge', 'stamp_tax', 'union_fee', 'individual_tax', 'income_tax', 'garbage_fee', 'other_penalty', 'total_tax_paid', 'store_revenue', 'company_revenue', 'invoice_limit', 'output_amount', 'input_amount', 'invoice_amount', ]; /** * Map (section_slug / page_slug) -> Model class. * Tabs reuse the same model but filter by an extra column. */ private static function routeMap(): array { return [ 'company-management/basic-information' => CompanyInformation::class, 'company-management/subject-operating-report' => StoreInformation::class, 'company-management/registration' => CompanyRegistration::class, 'company-management/changes' => CompanyChange::class, 'company-management/cancellation' => CompanyCancellation::class, 'company-management/tax-data' => TaxData::class, 'company-management/store-data' => CompanyStoreData::class, 'trademark-management/registration' => Trademark::class, 'trademark-management/changes' => Trademark::class, 'trademark-management/renewal' => Trademark::class, 'trademark-management/information' => Trademark::class, 'store-management/basic-information' => StoreInformation::class, 'store-management/store-accounts' => StoreInformation::class, 'store-management/store-report' => StoreInformation::class, 'store-management/opening-process' => StoreOpening::class, 'store-management/changes' => StoreChange::class, 'store-management/closing-process' => StoreClosing::class, 'financial-sw/planning' => SwPlanning::class, 'financial-sw/actual-statistics' => SwActualStatistics::class, 'store-management/change-log' => StoreChangeLog::class, 'invoice-management/basic-information' => InvoiceBasic::class, 'invoice-management/store-backend-invoicing' => StoreInvoice::class, 'fund-management/fund-forecast' => FundForecast::class, 'fund-management/store-receiving-accounts' => StoreReceivingAccount::class, 'fund-management/bank-accounts' => BankAccount::class, 'fund-management/shared-company-accounts' => BankAccount::class, // 重用 BankAccount,仅类型过滤 'billing-transactions/store-receiving-balance' => StoreReceivingAccount::class, 'financial-operations/installation-details' => InstallationDetail::class, ]; } /** * Resolve the model class for a given navigation triple. * Returns null if no model is registered (page is purely descriptive). */ public static function modelClassFor(string $section, string $page, ?string $tab = null): ?string { $key = "{$section}/{$page}"; $map = self::routeMap(); // For store-receiving-accounts tabs, we route by platform (tab slug). if ($key === 'fund-management/store-receiving-accounts') { return StoreReceivingAccount::class; } return $map[$key] ?? null; } /** * Build the descriptor used by the Blade view to render the table. */ public static function describe(string $section, string $page, ?string $tab = null): ?TableDescriptor { if ($section === 'fund-management' && $page === 'store-receiving-accounts') { return new TableDescriptor( model: StoreReceivingAccount::class, table: (new StoreReceivingAccount())->getTable(), fields: self::storeReceivingAccountFields($tab), tab: $tab, tabFilterColumn: $tab ? 'store_platform' : null, readOnly: false, ); } $modelClass = self::modelClassFor($section, $page, $tab); if (! $modelClass) { return null; } $instance = new $modelClass(); $labels = method_exists($modelClass, 'columnLabels') ? $modelClass::columnLabels() : []; $fieldTypes = method_exists($modelClass, 'fieldTypes') ? $modelClass::fieldTypes() : []; $columns = self::isUpstreamReadOnly($section, $page) ? $instance->getFillable() : $instance->getConnection()->getSchemaBuilder()->getColumnListing($instance->getTable()); // Drop meta columns from the rendered table. $visible = array_values(array_filter($columns, function (string $col) { return ! in_array($col, ['id', 'company_id', 'created_at', 'updated_at', 'deleted_at'], true); })); $fields = []; foreach ($visible as $col) { $fields[] = [ 'name' => $col, 'label' => $labels[$col] ?? self::COLUMN_LABELS[$col] ?? ucfirst(str_replace('_', ' ', $col)), 'type' => $fieldTypes[$col] ?? self::fieldType($col), 'options' => self::selectOptions($col), 'currency' => in_array($col, self::CURRENCY_HINTS, true), ]; } return new TableDescriptor( model: $modelClass, table: $instance->getTable(), fields: $fields, tab: $tab, tabFilterColumn: $tab ? self::tabFilterColumn($section, $page) : null, readOnly: self::isUpstreamReadOnly($section, $page), ); } private static function isUpstreamReadOnly(string $section, string $page): bool { return in_array("{$section}/{$page}", [ 'company-management/basic-information', 'company-management/subject-operating-report', 'store-management/basic-information', 'store-management/store-accounts', 'store-management/store-report', 'trademark-management/information', 'financial-operations/installation-details', ], true); } private static function fieldType(string $column): string { return self::FIELD_TYPES[$column] ?? ( str_contains($column, '_budget') || str_contains($column, '_actual') ? 'currency' : (in_array($column, self::CURRENCY_HINTS, true) ? 'currency' : 'text') ); } private static function selectOptions(string $column): ?array { return self::SELECT_OPTIONS[$column] ?? null; } /** * For pages with tabs, the column used to filter by tab slug. */ private static function tabFilterColumn(string $section, string $page): ?string { $key = "{$section}/{$page}"; return match ($key) { 'fund-management/store-receiving-accounts' => 'store_platform', default => null, }; } /** * For "shared-company-accounts" page we only show bank accounts that are * marked as shared (account_type == '基本户' by convention). */ public static function fixedFilters(string $section, string $page): array { $key = "{$section}/{$page}"; return match ($key) { 'fund-management/shared-company-accounts' => ['account_type' => '基本户'], default => [], }; } public static function importIdentityFields(string $section, string $page): array { return match ("{$section}/{$page}") { 'company-management/basic-information' => ['unified_social_credit_code'], 'trademark-management/information', 'trademark-management/registration', 'trademark-management/changes', 'trademark-management/renewal' => ['trademark_name'], 'store-management/basic-information' => ['erp_store_name', 'platform'], default => [], }; } /** * Resolve a tab slug to its display title from the navigation config. */ public static function tabTitle(string $section, string $page, string $tabSlug): ?string { foreach (BusinessNavigation::all() as $navSection) { if ($navSection['slug'] !== $section) { continue; } foreach ($navSection['pages'] ?? [] as $navPage) { if (($navPage['slug'] ?? null) !== $page) { continue; } foreach ($navPage['tabs'] ?? [] as $tab) { if (($tab['slug'] ?? null) === $tabSlug) { return $tab['title'] ?? $tabSlug; } } } } return null; } /** * Map tab slug -> filter value for the tab filter column. * For example the "taobao-alipay" tab maps to "淘宝" platform. */ public static function tabValue(string $section, string $page, string $tabSlug): ?string { $key = "{$section}/{$page}"; if ($key === 'fund-management/store-receiving-accounts') { return match ($tabSlug) { 'taobao' => '淘宝', 'jd' => '京东', 'douyin' => '抖音', 'pinduoduo' => '拼多多', default => null, }; } return null; } /** @return list */ public static function tabValues(string $section, string $page, string $tabSlug): array { if ($section !== 'fund-management' || $page !== 'store-receiving-accounts') { $value = self::tabValue($section, $page, $tabSlug); return $value === null ? [] : [$value]; } return match ($tabSlug) { 'taobao' => ['淘宝', '阿里巴巴', '天猫', '淘宝分销', '唯品会'], 'jd' => ['京东', '京喜'], 'douyin' => ['抖音'], 'pinduoduo' => ['拼多多'], default => [], }; } /** @return list,currency:bool}> */ private static function storeReceivingAccountFields(?string $tab): array { $text = static fn (string $name, string $label): array => [ 'name' => $name, 'label' => $label, 'type' => 'text', 'options' => null, 'currency' => false, ]; $select = static fn (string $name, string $label, array $options): array => [ 'name' => $name, 'label' => $label, 'type' => 'select', 'options' => $options, 'currency' => false, ]; $fields = []; if ($tab === 'taobao' || $tab === 'jd') { $fields[] = $select('store_platform', '类型', self::tabValues('fund-management', 'store-receiving-accounts', $tab)); } $fields[] = $text('erp_store_name', 'ERP店铺名称'); $fields[] = $text('shop_name', '店铺名称'); if ($tab === 'jd') { $fields[] = $text('store_id', '店铺id'); } $fields[] = $select('store_status', '店铺状态', ['正常', '关店中', '已关店']); $fields[] = $text('store_entity_name', '店铺主体'); $fields[] = $text('original_store_entity', '原店铺主体'); if ($tab === 'taobao') { $fields[] = $select('account_status', '状态', ['已维护', '待维护']); $fields[] = $text('login_account', '登录账号'); $fields[] = $text('login_password', '登录密码'); $fields[] = $text('payment_password', '支付密码'); $fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']); $fields[] = $text('withdrawal_account_name', '提现账户名称'); $fields[] = $text('alipay_or_bank_account', '支付宝账号'); } elseif ($tab === 'jd') { $fields[] = $text('login_account', '登录账号'); $fields[] = $text('login_password', '登录密码'); $fields[] = $text('payment_verification_method', '支付验证方式'); $fields[] = $text('financial_account', '金融账号'); $fields[] = $text('financial_password', '金融密码'); $fields[] = $text('payment_verification_method_secondary', '支付验证方式'); $fields[] = $select('account_status', '状态', ['已维护', '待维护']); $fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']); $fields[] = $text('withdrawal_account_name', '提现账户名称'); $fields[] = $text('bank_card_last_four', '银行卡账号(后四位)'); } else { $fields[] = $select('account_status', '状态', ['已维护', '待维护']); $fields[] = $text('login_account', '登录账号'); $fields[] = $text('login_password', '登录密码'); $fields[] = $text('payment_password', '支付密码'); $fields[] = $select('withdrawal_path', '提现路径', ['公户', '网商银行', '支付宝']); $fields[] = $text('withdrawal_account_name', '提现账户名称'); $fields[] = $text('bank_card_last_four', '银行卡账号(后四位)'); } return $fields; } }