diff --git a/app/Business/BusinessTableImporter.php b/app/Business/BusinessTableImporter.php new file mode 100644 index 00000000..311ab870 --- /dev/null +++ b/app/Business/BusinessTableImporter.php @@ -0,0 +1,181 @@ +getRealPath()); + [$rows, $headerRow, $columnMap] = $this->bestMatchingSheet($spreadsheet, $descriptor); + + if ($rows === null || $columnMap === []) { + throw ValidationException::withMessages([ + 'import' => '未找到可识别的表头,请使用当前页面显示的中文表头。', + ]); + } + + $created = 0; + $updated = 0; + $model = $descriptor->model; + $identityFields = TableRegistry::importIdentityFields($section, $page); + + DB::transaction(function () use ( + $rows, + $headerRow, + $columnMap, + $descriptor, + $section, + $page, + $tab, + $model, + $identityFields, + &$created, + &$updated, + ) { + foreach (array_slice($rows, $headerRow + 1) as $offset => $row) { + $data = []; + + foreach ($columnMap as $columnIndex => $field) { + $data[$field['name']] = $this->normalizeValue( + $row[$columnIndex] ?? null, + $field['type'], + $headerRow + $offset + 2, + $field['label'], + ); + } + + if (collect($data)->every(fn ($value) => $value === null || $value === '')) { + continue; + } + + if ($descriptor->tabFilterColumn && $tab) { + $tabValue = TableRegistry::tabValue($section, $page, $tab); + + if ($tabValue !== null) { + $data[$descriptor->tabFilterColumn] = $tabValue; + } + } + + foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) { + $data[$column] = $value; + } + + $data['company_id'] = company_id() ?: 1; + $identity = collect($identityFields) + ->filter(fn ($field) => array_key_exists($field, $data) && $data[$field] !== null && $data[$field] !== '') + ->mapWithKeys(fn ($field) => [$field => $data[$field]]) + ->all(); + + $record = count($identity) === count($identityFields) && $identity !== [] + ? $model::query()->where($identity)->first() + : null; + + if ($record) { + $record->update($data); + $updated++; + } else { + $model::create($data); + $created++; + } + } + }); + + if ($created + $updated === 0) { + throw ValidationException::withMessages([ + 'import' => '表格中没有可导入的数据行。', + ]); + } + + return compact('created', 'updated'); + } + + private function bestMatchingSheet($spreadsheet, TableDescriptor $descriptor): array + { + $fieldLookup = []; + + foreach ($descriptor->fields as $field) { + $fieldLookup[$this->normalizeHeader($field['name'])] = $field; + $fieldLookup[$this->normalizeHeader($field['label'])] = $field; + } + + $best = [null, null, []]; + $bestScore = 0; + + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $rows = $worksheet->toArray(null, true, false, false); + + foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) { + $columnMap = []; + + foreach ($row as $columnIndex => $heading) { + $key = $this->normalizeHeader($heading); + + if ($key !== '' && isset($fieldLookup[$key])) { + $columnMap[$columnIndex] = $fieldLookup[$key]; + } + } + + if (count($columnMap) > $bestScore) { + $best = [$rows, $rowIndex, $columnMap]; + $bestScore = count($columnMap); + } + } + } + + return $best; + } + + private function normalizeHeader($value): string + { + return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value))); + } + + private function normalizeValue($value, string $type, int $rowNumber, string $label) + { + if ($value === null || trim((string) $value) === '') { + return null; + } + + if ($type === 'date') { + try { + return is_numeric($value) + ? ExcelDate::excelToDateTimeObject($value)->format('Y-m-d') + : \Illuminate\Support\Carbon::parse($value)->format('Y-m-d'); + } catch (\Throwable $e) { + throw ValidationException::withMessages([ + 'import' => "第 {$rowNumber} 行“{$label}”不是有效日期。", + ]); + } + } + + if (in_array($type, ['number', 'currency'], true)) { + $number = str_replace([',', ',', '¥', '¥'], '', (string) $value); + + if (! is_numeric($number)) { + throw ValidationException::withMessages([ + 'import' => "第 {$rowNumber} 行“{$label}”不是有效数字。", + ]); + } + + return (float) $number; + } + + if ($type === 'boolean') { + return in_array(mb_strtolower(trim((string) $value)), ['1', '是', 'true', 'yes', 'y'], true) ? 1 : 0; + } + + return trim((string) $value); + } +} diff --git a/app/Business/TableDescriptor.php b/app/Business/TableDescriptor.php new file mode 100644 index 00000000..df671bee --- /dev/null +++ b/app/Business/TableDescriptor.php @@ -0,0 +1,19 @@ + '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/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/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, + ]; + } + + /** + * 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 + { + $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', + 'store-management/basic-information', + 'trademark-management/information', + ], 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-alipay' => '淘宝', + 'taobao-wechat' => '淘宝', + 'pinduoduo' => '拼多多', + 'jd-wallet' => '京东', + 'douyin' => '抖音', + 'dewu' => '得物', + 'vipshop' => '唯品会', + default => null, + }; + } + + return null; + } +} diff --git a/app/Business/UpstreamBusinessInformation.php b/app/Business/UpstreamBusinessInformation.php new file mode 100644 index 00000000..2eefb48a --- /dev/null +++ b/app/Business/UpstreamBusinessInformation.php @@ -0,0 +1,208 @@ + $this->companyPage($currentPage, $perPage), + 'store-management/basic-information' => $this->storePage($currentPage, $perPage), + 'trademark-management/information' => $this->trademarkPage($currentPage, $perPage), + default => ['data' => [], 'total' => 0], + }; + + return new LengthAwarePaginator( + array_map(static fn (array $row) => (object) $row, $payload['data']), + (int) $payload['total'], + $perPage, + $currentPage, + [ + 'path' => request()->url(), + 'query' => request()->query(), + ], + ); + } + + private function companyPage(int $page, int $perPage): array + { + return $this->remember('company-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) { + $offset = ($page - 1) * $perPage; + $rows = $query->select(<< array_map(fn (array $row) => $this->mapCompany($row), $rows), + 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_companyInfo'), + ]; + }); + } + + private function storePage(int $page, int $perPage): array + { + return $this->remember('store-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) { + $offset = ($page - 1) * $perPage; + $rows = $query->select(<< array_map(fn (array $row) => $this->mapStore($row), $rows), + 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_shopInfo'), + ]; + }); + } + + private function trademarkPage(int $page, int $perPage): array + { + return $this->remember('trademark-information.read', $page, $perPage, function (UpstreamReadOnlyQuery $query) use ($page, $perPage) { + $offset = ($page - 1) * $perPage; + $rows = $query->select(<< array_map(fn (array $row) => $this->mapTrademark($row), $rows), + 'total' => (int) $query->scalar('SELECT COUNT(*) FROM rpt.fys_TrademarkInfo WHERE isDelete = 0'), + ]; + }); + } + + private function remember(string $scope, int $page, int $perPage, callable $resolver): array + { + $cache = $this->caches->forSource(self::SOURCE); + + return $cache->remember($scope, compact('page', 'perPage'), function () use ($resolver) { + return $resolver(new UpstreamReadOnlyQuery(self::SOURCE)); + }); + } + + private function mapCompany(array $row): array + { + $row = $this->toUtf8($row); + $row['establishment_date'] = $this->date($row['establishment_date'] ?? null); + $row['company_status'] = match ((string) ($row['company_status'] ?? '')) { + '0' => '正常', + '1' => '已注销', + default => $row['company_status'] ?? null, + }; + + return $row; + } + + private function mapStore(array $row): array + { + $row = $this->toUtf8($row); + $row['opened_at'] = $this->date($row['opened_at'] ?? null); + $row['store_status'] = match ((string) ($row['store_status'] ?? '')) { + '0' => '正常', + '3' => '关闭中', + '6' => '已关闭', + default => $row['store_status'] ?? null, + }; + + return $row; + } + + private function mapTrademark(array $row): array + { + $row = $this->toUtf8($row); + $row['registration_date'] = $this->date($row['registration_date'] ?? null); + $row['expiry_date'] = $this->date($row['expiry_date'] ?? null); + $row['trademark_status'] = match ((string) ($row['trademark_status'] ?? '')) { + '0' => '未启用', + '1' => '有效', + default => $row['trademark_status'] ?? null, + }; + + return $row; + } + + private function toUtf8(array $row): array + { + foreach ($row as &$value) { + if (is_string($value) && ! mb_check_encoding($value, 'UTF-8')) { + $value = mb_convert_encoding($value, 'UTF-8', 'GB18030'); + } + } + unset($value); + + return $row; + } + + private function date($value): ?string + { + if ($value === null || $value === '') { + return null; + } + + return substr((string) $value, 0, 10); + } +} diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 05460d3a..6a8b1e1d 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -2,9 +2,13 @@ namespace App\Http\Controllers\Business; +use App\Business\BusinessTableImporter; +use App\Business\TableRegistry; +use App\Business\UpstreamBusinessInformation; use App\Models\Banking\MarketingAccountBill; use App\Models\StoreReceivables\StoreZhongcaiReconciliation; use App\Utilities\SidebarPermissions; +use Illuminate\Http\Request; use Illuminate\Routing\Controller; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\Paginator; @@ -36,6 +40,8 @@ class Pages extends Controller private readonly SupplierPayableReportRepository $supplierPayableReports, private readonly UpstreamQueryCacheFactory $upstreamCaches, ) { + $this->middleware('auth'); + $this->middleware('company.identify'); } /** @@ -45,6 +51,7 @@ class Pages extends Controller 'taobao-alipay', 'taobao-wechat', 'pinduoduo', 'jd-wallet', 'douyin', 'dewu', 'vipshop', ]; + public function home() { $section = collect(SidebarPermissions::navigationFor(user()))->first(); @@ -76,12 +83,7 @@ class Pages extends Controller if ($pages->isEmpty()) { abort_if($page !== null || $tab !== null, 404); - return view('business.pages.show', [ - 'section' => $sectionConfig, - 'page' => null, - 'tabs' => collect(), - 'activeTab' => null, - ]); + return view('business.pages.show', $this->viewData($sectionConfig)); } if ($page === null) { @@ -97,7 +99,6 @@ class Pages extends Controller abort_if(empty($pageConfig), 404); - // 如果该 page 是一个 children-only 的父级分组,访问它时跳到第一个子页面 if (! empty($pageConfig['children']) && empty($pageConfig['tabs'])) { return $this->redirectToPage($sectionConfig, $pageConfig['children'][0]); } @@ -107,18 +108,13 @@ class Pages extends Controller if ($tabs->isEmpty()) { abort_if($tab !== null, 404); - $content = $this->resolveContent($page, null); - - return view('business.pages.show', [ - 'section' => $sectionConfig, - 'page' => $pageConfig, - 'tabs' => $tabs, - 'activeTab' => null, - 'content' => $content, - 'edgeBanding' => $this->edgeBandingContent($page), - 'inventoryData' => $this->inventoryContent($page), - 'rawBoardData' => $this->rawBoardContent($page), - ]); + return view('business.pages.show', $this->viewData( + $sectionConfig, + $pageConfig, + $tabs, + null, + $this->resolveContent($page, null), + )); } if ($tab === null) { @@ -129,23 +125,94 @@ class Pages extends Controller abort_if(empty($activeTab), 404); - $content = $this->resolveContent($page, $tab); - - return view('business.pages.show', [ - 'section' => $sectionConfig, - 'page' => $pageConfig, - 'tabs' => $tabs, - 'activeTab' => $activeTab, - 'content' => $content, - 'edgeBanding' => $this->edgeBandingContent($page), - 'inventoryData' => $this->inventoryContent($page), - 'rawBoardData' => $this->rawBoardContent($page), - ]); + return view('business.pages.show', $this->viewData( + $sectionConfig, + $pageConfig, + $tabs, + $activeTab, + $this->resolveContent($page, $tab), + )); + } + + protected function viewData( + array $section, + ?array $page = null, + $tabs = null, + ?array $activeTab = null, + ?array $content = null, + ): array { + $descriptor = null; + $rows = collect(); + $tabValue = null; + $tabColumn = null; + + if ($page && $content === null) { + $descriptor = TableRegistry::describe( + $section['slug'], + $page['slug'], + $activeTab['slug'] ?? null, + ); + + if ($descriptor) { + if ($descriptor->readOnly) { + $rows = app(UpstreamBusinessInformation::class)->paginate( + $section['slug'], + $page['slug'], + 50, + ); + + return [ + 'section' => $section, + 'page' => $page, + 'tabs' => $tabs ?? collect(), + 'activeTab' => $activeTab, + 'content' => $content, + 'descriptor' => $descriptor, + 'rows' => $rows, + 'tabColumn' => null, + 'tabValue' => null, + 'edgeBanding' => $this->edgeBandingContent($page['slug']), + 'inventoryData' => $this->inventoryContent($page['slug']), + 'rawBoardData' => $this->rawBoardContent($page['slug']), + ]; + } + + $query = ($descriptor->model)::query(); + + if ($descriptor->tabFilterColumn && $activeTab) { + $tabValue = TableRegistry::tabValue($section['slug'], $page['slug'], $activeTab['slug']); + + if ($tabValue !== null) { + $query->where($descriptor->tabFilterColumn, $tabValue); + } + + $tabColumn = $descriptor->tabFilterColumn; + } + + foreach (TableRegistry::fixedFilters($section['slug'], $page['slug']) as $column => $value) { + $query->where($column, $value); + } + + $rows = $query->orderByDesc('id')->paginate(50)->withQueryString(); + } + } + + return [ + 'section' => $section, + 'page' => $page, + 'tabs' => $tabs ?? collect(), + 'activeTab' => $activeTab, + 'content' => $content, + 'descriptor' => $descriptor, + 'rows' => $rows, + 'tabColumn' => $tabColumn, + 'tabValue' => $tabValue, + 'edgeBanding' => $page ? $this->edgeBandingContent($page['slug']) : null, + 'inventoryData' => $page ? $this->inventoryContent($page['slug']) : null, + 'rawBoardData' => $page ? $this->rawBoardContent($page['slug']) : null, + ]; } - /** - * 根据页面和标签加载对应的业务内容数据 - */ protected function resolveContent(string $page, ?string $tab): ?array { return match ($page) { @@ -609,7 +676,7 @@ class Pages extends Controller /** * 种菜应收对账 - 加载对账数据(21字段) */ - protected function contentForZhongcaiReconciliation(): ?array + protected function contentForZhongcaiReconciliation(): array { return [ 'view' => 'store_receivables.zhongcai_reconciliations.partials.embedded-table', @@ -619,13 +686,9 @@ class Pages extends Controller ]; } - /** - * 营销账户账单 - 加载指定平台的数据 - * 淘系支付宝、拼多多使用独立的流水账单表,其他平台使用通用汇总账单表 - */ - protected function contentForMarketingAccountBills(string $tab): ?array + protected function contentForMarketingAccountBills(?string $tab): ?array { - if (!in_array($tab, self::MARKETING_PLATFORMS, true)) { + if (! in_array($tab, self::MARKETING_PLATFORMS, true)) { return null; } @@ -779,7 +842,6 @@ class Pages extends Controller ]; } - // 其他平台 - 使用通用汇总账单表(15字段) return [ 'view' => 'banking.marketing_account_bills.partials.embedded-table', 'platform' => $tab, @@ -790,6 +852,86 @@ class Pages extends Controller ]; } + public function store(Request $request, string $section, string $page, ?string $tab = null) + { + $descriptor = TableRegistry::describe($section, $page, $tab); + abort_if(! $descriptor, 404); + + $data = $request->validate($this->validationRules($descriptor)); + + if ($descriptor->tabFilterColumn && $tab && ($tabValue = TableRegistry::tabValue($section, $page, $tab))) { + $data[$descriptor->tabFilterColumn] = $tabValue; + } + + foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) { + $data[$column] = $value; + } + + ($descriptor->model)::create($data); + + return $this->redirectBack($section, $page, $tab); + } + + public function import( + Request $request, + BusinessTableImporter $importer, + string $section, + string $page, + ?string $tab = null, + ) { + $descriptor = TableRegistry::describe($section, $page, $tab); + abort_if(! $descriptor, 404); + + $request->validate([ + 'import' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'], + ]); + + $result = $importer->import($request->file('import'), $descriptor, $section, $page, $tab); + + return $this->redirectBack($section, $page, $tab)->with( + 'success', + "导入完成:新增 {$result['created']} 条,更新 {$result['updated']} 条。", + ); + } + + public function update(Request $request, string $section, string $page, $id, ?string $tab = null) + { + $descriptor = TableRegistry::describe($section, $page, $tab); + abort_if(! $descriptor, 404); + + $row = ($descriptor->model)::findOrFail($id); + $row->update($request->validate($this->validationRules($descriptor))); + + return $this->redirectBack($section, $page, $tab); + } + + public function destroy(string $section, string $page, $id, ?string $tab = null) + { + $descriptor = TableRegistry::describe($section, $page, $tab); + abort_if(! $descriptor, 404); + + ($descriptor->model)::findOrFail($id)->delete(); + + return $this->redirectBack($section, $page, $tab); + } + + protected function validationRules($descriptor): array + { + $rules = []; + + foreach ($descriptor->fields as $field) { + $rules[$field['name']] = match ($field['type']) { + 'number', 'currency' => ['nullable', 'numeric'], + 'boolean' => ['nullable', 'boolean'], + 'date' => ['nullable', 'date'], + 'select' => ['nullable', 'string', 'max:64'], + default => ['nullable', 'string', 'max:255'], + }; + } + + return $rules; + } + protected function redirectToPage(array $section, array $page) { $parameters = [ @@ -816,4 +958,19 @@ class Pages extends Controller return checkdate($month, $day, $year) ? $date : ''; } + + protected function redirectBack(string $section, string $page, ?string $tab) + { + $parameters = [ + 'company_id' => company_id() ?: 1, + 'section' => $section, + 'page' => $page, + ]; + + if ($tab) { + $parameters['tab'] = $tab; + } + + return redirect()->route('business.pages.show', $parameters)->with('success', true); + } } diff --git a/app/Listeners/Menu/ShowInAdmin.php b/app/Listeners/Menu/ShowInAdmin.php index b9da00ef..a32af23b 100644 --- a/app/Listeners/Menu/ShowInAdmin.php +++ b/app/Listeners/Menu/ShowInAdmin.php @@ -15,7 +15,7 @@ class ShowInAdmin $menu = $event->menu; foreach (SidebarPermissions::navigationFor(user()) as $sectionIndex => $section) { - $pages = $section['pages'] ?? []; + $pages = $this->sortByTitleLength($section['pages'] ?? []); $order = ($sectionIndex + 1) * 10; if (empty($pages)) { @@ -44,7 +44,7 @@ class ShowInAdmin $parameters['tab'] = $page['tabs'][0]['slug']; } - $children = $page['children'] ?? []; + $children = $this->sortByTitleLength($page['children'] ?? []); if (! empty($children)) { // 父级 page 渲染为 dropdown:标题可点击跳到第一个子页面,子项作为嵌套菜单 @@ -103,4 +103,24 @@ class ShowInAdmin ]); } } + + private function sortByTitleLength(array $items): array + { + $indexed = []; + + foreach (array_values($items) as $index => $item) { + $indexed[] = [ + 'item' => $item, + 'index' => $index, + 'length' => mb_strlen((string) ($item['title'] ?? '')), + ]; + } + + usort($indexed, static function (array $left, array $right): int { + return ($left['length'] <=> $right['length']) + ?: ($left['index'] <=> $right['index']); + }); + + return array_column($indexed, 'item'); + } } diff --git a/app/Models/Business/BankAccount.php b/app/Models/Business/BankAccount.php index 34d1922e..2a807a06 100644 --- a/app/Models/Business/BankAccount.php +++ b/app/Models/Business/BankAccount.php @@ -13,4 +13,18 @@ class BankAccount extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'account_name' => '账户名称', + 'account_type' => '账户类型', + 'bank_account_number' => '银行账号', + 'opening_bank' => '开户行', + 'ukey_password_maker' => 'UKey密码(制单)', + 'login_password_maker' => '登录密码(制单)', + 'ukey_password_reviewer' => 'UKey密码(审核)', + 'login_password_reviewer' => '登录密码(审核)', + ]; + } } diff --git a/app/Models/Business/BusinessModel.php b/app/Models/Business/BusinessModel.php new file mode 100644 index 00000000..552f8fd7 --- /dev/null +++ b/app/Models/Business/BusinessModel.php @@ -0,0 +1,68 @@ +where($builder->getModel()->getTable() . '.company_id', company_id()); + } + }); + + static::creating(function (self $model) { + if (empty($model->company_id)) { + $model->company_id = company_id(); + } + }); + } + + /** + * Ordered columns (id, then declared columns, then timestamps). + * Used by the Blade views to render the table headers and form fields. + */ + public static function declaredColumns(): array + { + static $cache = []; + + $key = static::class; + if (isset($cache[$key])) { + return $cache[$key]; + } + + /** @var \Illuminate\Database\Schema\SQLiteBuilder $schema */ + $schema = (new static())->getConnection()->getSchemaBuilder(); + $columns = $schema->getColumnListing((new static())->getTable()); + + // Re-order: primary key first, then declared columns, then timestamps. + $ordered = array_values(array_filter($columns, function (string $col) { + return ! in_array($col, ['id', 'company_id', 'created_at', 'updated_at'], true); + })); + + return $cache[$key] = ['id' => 'id'] + array_combine($ordered, $ordered); + } + + /** + * Column human-readable labels (overridable per model). + */ + public static function columnLabels(): array + { + return []; + } +} diff --git a/app/Models/Business/CompanyInformation.php b/app/Models/Business/CompanyInformation.php new file mode 100644 index 00000000..3cb17a77 --- /dev/null +++ b/app/Models/Business/CompanyInformation.php @@ -0,0 +1,54 @@ + 'integer', + 'subscribed_capital' => 'decimal:2', + 'establishment_date' => 'date:Y-m-d', + ]; + + public static function columnLabels(): array + { + return [ + 'unified_social_credit_code' => '统一社会信用代码', + 'company_name' => '公司名称', + 'subscribed_capital' => '认缴注册资本(万元)', + 'company_type' => '类型', + 'establishment_date' => '成立日期', + 'legal_representative' => '法定代表人', + 'registered_address' => '注册地址(住所)', + 'business_scope' => '经营范围', + 'basic_deposit_account_number' => '基本存款账户编号', + 'company_status' => '公司状态', + 'financial_officer' => '财务负责人', + 'supervisor' => '监事', + 'taxpayer_category' => '纳税类别及类型', + 'shareholders' => '股东', + ]; + } +} diff --git a/app/Models/Business/CompanyStore.php b/app/Models/Business/CompanyStore.php new file mode 100644 index 00000000..0fb6483e --- /dev/null +++ b/app/Models/Business/CompanyStore.php @@ -0,0 +1,28 @@ + 'decimal:2', + 'company_revenue' => 'decimal:2', + 'trigger_store_change' => 'boolean', + 'trigger_company_register' => 'boolean', + ]; + + public static function columnLabels(): array + { + return [ + 'company_name' => '公司名称', + 'store_name' => '店铺名称', + 'store_revenue' => '店铺业绩', + 'company_revenue' => '公司业绩', + 'trigger_store_change' => '是否触发店铺变更流程', + 'trigger_company_register' => '是否触发公司注册流程', + 'period' => '期间', + ]; + } +} diff --git a/app/Models/Business/CompanyStoreData.php b/app/Models/Business/CompanyStoreData.php index 398f35ef..f60ea410 100644 --- a/app/Models/Business/CompanyStoreData.php +++ b/app/Models/Business/CompanyStoreData.php @@ -15,4 +15,16 @@ class CompanyStoreData extends Model 'trigger_store_change' => 'boolean', 'trigger_company_registration' => 'boolean', ]; + + public static function columnLabels(): array + { + return [ + 'company_name' => '公司名称', + 'store_name' => '店铺名称', + 'store_performance' => '店铺业绩', + 'company_performance' => '公司业绩', + 'trigger_store_change' => '是否触发店铺变更流程', + 'trigger_company_registration' => '是否触发公司注册流程', + ]; + } } diff --git a/app/Models/Business/ExpressReceiving.php b/app/Models/Business/ExpressReceiving.php new file mode 100644 index 00000000..cf994fa7 --- /dev/null +++ b/app/Models/Business/ExpressReceiving.php @@ -0,0 +1,19 @@ + '快递名称', + 'receiving_type' => '收款类型', + 'receiving_name' => '收款名称', + 'account_number' => '收款账号', + 'bank_name' => '开户行', + ]; + } +} diff --git a/app/Models/Business/FundForecast.php b/app/Models/Business/FundForecast.php index 259202f8..eb32d32e 100644 --- a/app/Models/Business/FundForecast.php +++ b/app/Models/Business/FundForecast.php @@ -13,4 +13,53 @@ class FundForecast extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'budget_month' => '预算月份', + 'actual_month' => '实际月份', + 'store_balance_budget' => '店铺余额(预算)', + 'store_balance_actual' => '店铺余额(实际)', + 'bank_balance_budget' => '银行余额(预算)', + 'bank_balance_actual' => '银行余额(实际)', + 'other_balance_budget' => '其他余额(预算)', + 'other_balance_actual' => '其他余额(实际)', + 'expected_monthly_return_budget' => '预计回款(预算)', + 'expected_monthly_return_actual' => '预计回款(实际)', + 'income_total_budget' => '收入合计(预算)', + 'income_total_actual' => '收入合计(实际)', + 'prior_unpaid_budget' => '往期未付(预算)', + 'prior_unpaid_actual' => '往期未付(实际)', + 'advance_budget' => '借支(预算)', + 'advance_actual' => '借支(实际)', + 'supplier_budget' => '供应商(预算)', + 'supplier_actual' => '供应商(实际)', + 'spot_settlement_budget' => '现结(预算)', + 'spot_settlement_actual' => '现结(实际)', + 'courier_budget' => '快递(预算)', + 'courier_actual' => '快递(实际)', + 'paper_factory_budget' => '纸厂压贴(预算)', + 'paper_factory_actual' => '纸厂压贴(实际)', + 'edge_band_budget' => '封边条(预算)', + 'edge_band_actual' => '封边条(实际)', + 'board_budget' => '板材(预算)', + 'board_actual' => '板材(实际)', + 'salary_budget' => '工资(预算)', + 'salary_actual' => '工资(实际)', + 'daily_expense_budget' => '日常费用(预算)', + 'daily_expense_actual' => '日常费用(实际)', + 'planting_budget' => '种菜(预算)', + 'planting_actual' => '种菜(实际)', + 'red_packet_budget' => '红包(预算)', + 'red_packet_actual' => '红包(实际)', + 'corporate_deduction_budget' => '对公扣款(预算)', + 'corporate_deduction_actual' => '对公扣款(实际)', + 'expense_total_budget' => '支出合计(预算)', + 'expense_total_actual' => '支出合计(实际)', + 'bank_reservation' => '银行预留', + 'net_balance_budget' => '净结余(预算)', + 'net_balance_actual' => '净结余(实际)', + ]; + } } diff --git a/app/Models/Business/InvoiceBasic.php b/app/Models/Business/InvoiceBasic.php new file mode 100644 index 00000000..e40ea12d --- /dev/null +++ b/app/Models/Business/InvoiceBasic.php @@ -0,0 +1,27 @@ + 'decimal:2', + 'output_amount' => 'decimal:2', + 'input_amount' => 'decimal:2', + ]; + + public static function columnLabels(): array + { + return [ + 'store_name' => '店铺名称', + 'store_entity' => '店铺主体', + 'invoice_issuer' => '开票员', + 'invoice_limit' => '开票额度', + 'output_amount' => '销项金额', + 'input_amount' => '进项金额', + 'period' => '期间', + ]; + } +} diff --git a/app/Models/Business/StoreInformation.php b/app/Models/Business/StoreInformation.php new file mode 100644 index 00000000..d130b02e --- /dev/null +++ b/app/Models/Business/StoreInformation.php @@ -0,0 +1,57 @@ + 'integer', + 'opened_at' => 'date:Y-m-d', + ]; + + public static function columnLabels(): array + { + return [ + 'opened_at' => '开店时间', + 'erp_store_name' => 'ERP系统店铺名称', + 'unified_social_credit_code' => '公司社会统一信用码', + 'company_name' => '公司名称', + 'trademark_number' => '商标号', + 'trademark_name' => '商标名称', + 'platform' => '平台', + 'profit_center' => '店铺所属利润中心', + 'store_manager' => '店铺负责人', + 'store_status' => '店铺状态', + 'store_type' => '店铺类型', + 'channel_mode' => '渠道模式', + ]; + } + + public static function fieldTypes(): array + { + return [ + 'opened_at' => 'date', + 'platform' => 'text', + ]; + } +} diff --git a/app/Models/Business/StoreInvoice.php b/app/Models/Business/StoreInvoice.php new file mode 100644 index 00000000..5f55e5e2 --- /dev/null +++ b/app/Models/Business/StoreInvoice.php @@ -0,0 +1,28 @@ + 'decimal:2', + 'invoice_date' => 'date', + ]; + + public static function columnLabels(): array + { + return [ + 'store' => '店铺', + 'seller_name' => '销售方名称', + 'seller_tax_no' => '销售方税号', + 'buyer_name' => '购买方名称', + 'buyer_tax_no' => '购买方税号', + 'invoice_amount' => '开票金额', + 'invoice_category' => '类目', + 'invoice_type' => '开票类型', + 'invoice_date' => '开票日期', + ]; + } +} diff --git a/app/Models/Business/StoreReceivingAccount.php b/app/Models/Business/StoreReceivingAccount.php index 1591eeab..8916068e 100644 --- a/app/Models/Business/StoreReceivingAccount.php +++ b/app/Models/Business/StoreReceivingAccount.php @@ -13,4 +13,20 @@ class StoreReceivingAccount extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'store_platform' => '店铺平台', + 'store_name' => '店铺名称', + 'wallet_number' => '钱包号', + 'store_entity_name' => '店铺主体名称', + 'login_account' => '登录账号', + 'login_password' => '登录密码', + 'receiving_method' => '收款方式', + 'receiving_account_name' => '收款账户名称', + 'alipay_or_bank_account' => '支付宝或银行卡账号', + 'opening_bank' => '开户行', + ]; + } } diff --git a/app/Models/Business/SwActual.php b/app/Models/Business/SwActual.php new file mode 100644 index 00000000..0e467470 --- /dev/null +++ b/app/Models/Business/SwActual.php @@ -0,0 +1,107 @@ + 'decimal:2', + 'declared_revenue' => 'decimal:2', + 'goods_special_amount' => 'decimal:2', + 'goods_special_tax' => 'decimal:2', + 'goods_normal_amount' => 'decimal:2', + 'goods_normal_tax' => 'decimal:2', + 'goods_unbilled_amount' => 'decimal:2', + 'goods_unbilled_tax' => 'decimal:2', + 'goods_subtotal_amount' => 'decimal:2', + 'goods_subtotal_tax' => 'decimal:2', + 'service_special_amount' => 'decimal:2', + 'service_special_tax' => 'decimal:2', + 'service_normal_amount' => 'decimal:2', + 'service_normal_tax' => 'decimal:2', + 'service_unbilled_amount' => 'decimal:2', + 'service_unbilled_tax' => 'decimal:2', + 'service_subtotal_amount' => 'decimal:2', + 'service_subtotal_tax' => 'decimal:2', + 'total_revenue' => 'decimal:2', + 'total_tax' => 'decimal:2', + 'input_goods_amount' => 'decimal:2', + 'input_goods_tax' => 'decimal:2', + 'input_service_amount' => 'decimal:2', + 'input_service_tax' => 'decimal:2', + 'input_other_amount' => 'decimal:2', + 'input_other_tax' => 'decimal:2', + 'input_subtotal_amount' => 'decimal:2', + 'input_subtotal_tax' => 'decimal:2', + 'extra_credit' => 'decimal:2', + 'retained_credit' => 'decimal:2', + 'vat_payable' => 'decimal:2', + 'city_tax' => 'decimal:2', + 'education_surcharge' => 'decimal:2', + 'local_education_surcharge' => 'decimal:2', + 'stamp_tax' => 'decimal:2', + 'union_fee' => 'decimal:2', + 'individual_tax' => 'decimal:2', + 'income_tax' => 'decimal:2', + 'garbage_fee' => 'decimal:2', + 'other_penalty' => 'decimal:2', + 'total_tax_paid' => 'decimal:2', + 'tax_burden_rate' => 'decimal:4', + ]; + + public static function columnLabels(): array + { + return [ + 'region' => '区域', + 'company_type' => '公司类型', + 'contact_person' => '对接人', + 'bookkeeping_company' => '代账公司', + 'period' => '期间', + 'company_name' => '公司名称', + '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' => '税金合计', + 'tax_burden_rate' => '税负', + ]; + } +} diff --git a/app/Models/Business/SwActualStatistics.php b/app/Models/Business/SwActualStatistics.php index ba537388..f3e72bfd 100644 --- a/app/Models/Business/SwActualStatistics.php +++ b/app/Models/Business/SwActualStatistics.php @@ -13,4 +13,58 @@ class SwActualStatistics extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'region' => '区域', + 'company_type' => '公司类型', + 'contact_person' => '对接人', + 'bookkeeping_company' => '代账公司', + 'period' => '期间', + 'company_name' => '公司名称', + 'provided_performance' => '提供业绩', + 'actual_declared_performance' => '实际申报业绩', + 'goods_special_invoice_amount' => '商品收入(专票金额)', + 'goods_special_invoice_tax' => '商品收入(专票税额)', + 'goods_general_invoice_amount' => '商品收入(普票金额)', + 'goods_general_invoice_tax' => '商品收入(普票税额)', + 'goods_uninvoiced_amount' => '商品收入(未开票金额)', + 'goods_uninvoiced_tax' => '商品收入(未开票税额)', + 'goods_income_subtotal' => '商品收入小计', + 'goods_tax_subtotal' => '商品税金小计', + 'service_special_invoice_amount' => '服务收入(专票金额)', + 'service_special_invoice_tax' => '服务收入(专票税额)', + 'service_general_invoice_amount' => '服务收入(普票金额)', + 'service_general_invoice_tax' => '服务收入(普票税额)', + 'service_uninvoiced_amount' => '服务收入(未开票金额)', + 'service_uninvoiced_tax' => '服务收入(未开票税额)', + 'service_income_subtotal' => '服务收入小计', + 'service_tax_subtotal' => '服务税金小计', + 'total_income' => '合计收入', + 'total_tax' => '合计税金', + 'input_goods_amount' => '进项商品金额', + 'input_goods_tax' => '进项商品税额', + 'input_service_amount' => '进项服务费金额', + 'input_service_tax' => '进项服务费税额', + 'input_other_amount' => '进项其他金额', + 'input_other_tax' => '进项其他税额', + 'input_total_amount' => '进项金额合计', + 'input_total_tax' => '进项税额合计', + 'additional_deduction' => '加计抵减', + 'retained_tax' => '留抵税金', + 'monthly_vat' => '本月应交增值税', + 'urban_maintenance_tax' => '城建税金', + 'education_surcharge' => '教育费附加', + 'local_education_surcharge' => '地方教育费附加', + 'stamp_tax' => '印花税', + 'union_fund' => '工会经费', + 'personal_income_tax' => '个税', + 'corporate_income_tax' => '所得税', + 'garbage_fee' => '垃圾处理费', + 'other_late_fee' => '其他(滞纳金)', + 'total_paid_tax' => '税金合计', + 'tax_burden' => '税负', + ]; + } } diff --git a/app/Models/Business/SwPlanning.php b/app/Models/Business/SwPlanning.php index 2a8c7d84..fd507db8 100644 --- a/app/Models/Business/SwPlanning.php +++ b/app/Models/Business/SwPlanning.php @@ -13,4 +13,18 @@ class SwPlanning extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'company_name' => '公司名称', + 'final_tax_performance' => '最终税务业绩', + 'product_cost' => '产品成本', + 'logistics_cost' => '物流成本', + 'platform_invoice' => '平台开票', + 'labor_cost' => '人工成本', + 'other_expenses' => '其他费用', + 'profit' => '利润', + ]; + } } diff --git a/app/Models/Business/TaxData.php b/app/Models/Business/TaxData.php index 7bb3d9cb..3551a2b4 100644 --- a/app/Models/Business/TaxData.php +++ b/app/Models/Business/TaxData.php @@ -13,4 +13,20 @@ class TaxData extends Model protected $casts = [ 'company_id' => 'integer', ]; + + public static function columnLabels(): array + { + return [ + 'company_name' => '公司名称', + 'real_shipping_performance' => '公司真实发货业绩', + 'planting_performance' => '公司种菜业绩', + 'refund_amount' => '退款金额', + 'net_performance_income' => '业绩净收入', + 'tax_performance' => '税务业绩', + 'tax_refund' => '税务退款', + 'tax_net_income' => '税务净收入', + 'performance_difference' => '业绩差额', + 'final_tax_performance' => '最终报税业绩', + ]; + } } diff --git a/config/business_navigation.php b/config/business_navigation.php index 5892cbd5..134ff684 100644 --- a/config/business_navigation.php +++ b/config/business_navigation.php @@ -6,6 +6,7 @@ return [ 'title' => '公司管理', 'icon' => 'business', 'pages' => [ + ['slug' => 'basic-information', 'title' => '公司信息'], ['slug' => 'registration', 'title' => '公司注册'], ['slug' => 'changes', 'title' => '公司变更'], ['slug' => 'cancellation', 'title' => '公司注销'], @@ -19,7 +20,7 @@ return [ 'icon' => 'verified', 'pages' => [ ['slug' => 'registration', 'title' => '商标注册'], - ['slug' => 'changes', 'title' => '商标表更'], + ['slug' => 'changes', 'title' => '商标变更'], ['slug' => 'renewal', 'title' => '商标续展'], ['slug' => 'information', 'title' => '商标信息'], ], @@ -29,6 +30,7 @@ return [ 'title' => '店铺管理', 'icon' => 'storefront', 'pages' => [ + ['slug' => 'basic-information', 'title' => '店铺基础信息'], ['slug' => 'opening-process', 'title' => '开店流程'], ['slug' => 'changes', 'title' => '店铺变更'], ['slug' => 'closing-process', 'title' => '关店流程'], diff --git a/config/upstreams.php b/config/upstreams.php index fa6aa753..37e0a51b 100644 --- a/config/upstreams.php +++ b/config/upstreams.php @@ -28,6 +28,8 @@ return [ // 只限制冷查询和后台刷新;共享快照命中不占槽位。 'max_concurrency' => (int) env('UPSTREAM_MAX_CONCURRENCY', 4), 'slot_wait_ms' => (int) env('UPSTREAM_SLOT_WAIT_MS', 1500), + // 开发机可覆盖系统数据库中记录的 ODBC 驱动名。 + 'odbc_driver' => env('UPSTREAM_ODBC_DRIVER'), ], 'stored_configuration' => [ diff --git a/database/migrations/2026_07_21_000000_create_business_finance_tables.php b/database/migrations/2026_07_21_000000_create_business_finance_tables.php new file mode 100644 index 00000000..b44bac7c --- /dev/null +++ b/database/migrations/2026_07_21_000000_create_business_finance_tables.php @@ -0,0 +1,276 @@ +id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('company_name')->default('')->comment('公司名称'); + $table->decimal('real_ship_revenue', 18, 2)->default(0)->comment('公司真实发货业绩'); + $table->decimal('zhongcai_revenue', 18, 2)->default(0)->comment('公司种菜业绩'); + $table->decimal('refund_amount', 18, 2)->default(0)->comment('退款金额'); + $table->decimal('net_revenue', 18, 2)->default(0)->comment('业绩净收入'); + $table->decimal('tax_revenue', 18, 2)->default(0)->comment('税务业绩'); + $table->decimal('tax_refund', 18, 2)->default(0)->comment('税务退款'); + $table->decimal('tax_net_revenue', 18, 2)->default(0)->comment('税务净收入'); + $table->decimal('revenue_diff', 18, 2)->default(0)->comment('业绩差额'); + $table->decimal('final_tax_revenue', 18, 2)->default(0)->comment('最终报税业绩'); + $table->string('period')->default('')->comment('期间,例如 2026-07'); + $table->timestamps(); + }); + + // 2. 财务SW规划表 + Schema::create('ak_biz_sw_planning', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('company_name')->default(''); + $table->decimal('final_tax_revenue', 18, 2)->default(0)->comment('最终报税业绩'); + $table->decimal('product_cost', 18, 2)->default(0)->comment('商品成本'); + $table->decimal('logistics_cost', 18, 2)->default(0)->comment('物流成本'); + $table->decimal('platform_invoice', 18, 2)->default(0)->comment('平台来票'); + $table->decimal('labor_cost', 18, 2)->default(0)->comment('人工'); + $table->decimal('other_reimbursement', 18, 2)->default(0)->comment('其他报销'); + $table->decimal('profit', 18, 2)->default(0)->comment('利润'); + $table->string('period')->default(''); + $table->timestamps(); + }); + + // 3. 实际SW统计表(多级表头,扁平化存储) + Schema::create('biz_sw_actual', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('region')->default('')->comment('区域'); + $table->string('company_type')->default('')->comment('公司类型'); + $table->string('contact_person')->default('')->comment('对接人'); + $table->string('bookkeeping_company')->default('')->comment('代账公司'); + $table->string('period')->default('')->comment('期间'); + $table->string('company_name')->default('')->comment('公司名称'); + $table->decimal('provided_revenue', 18, 2)->default(0)->comment('提供业绩'); + $table->decimal('declared_revenue', 18, 2)->default(0)->comment('实际申报业绩'); + // 商品收入 13% + $table->decimal('goods_special_amount', 18, 2)->default(0)->comment('商品专票金额'); + $table->decimal('goods_special_tax', 18, 2)->default(0)->comment('商品专票税额'); + $table->decimal('goods_normal_amount', 18, 2)->default(0)->comment('商品普票金额'); + $table->decimal('goods_normal_tax', 18, 2)->default(0)->comment('商品普票税额'); + $table->decimal('goods_unbilled_amount', 18, 2)->default(0)->comment('商品未开票金额'); + $table->decimal('goods_unbilled_tax', 18, 2)->default(0)->comment('商品未开票税额'); + $table->decimal('goods_subtotal_amount', 18, 2)->default(0)->comment('商品收入小计'); + $table->decimal('goods_subtotal_tax', 18, 2)->default(0)->comment('商品税金小计'); + // 服务收入 6% + $table->decimal('service_special_amount', 18, 2)->default(0)->comment('服务专票金额'); + $table->decimal('service_special_tax', 18, 2)->default(0)->comment('服务专票税额'); + $table->decimal('service_normal_amount', 18, 2)->default(0)->comment('服务普票金额'); + $table->decimal('service_normal_tax', 18, 2)->default(0)->comment('服务普票税额'); + $table->decimal('service_unbilled_amount', 18, 2)->default(0)->comment('服务未开票金额'); + $table->decimal('service_unbilled_tax', 18, 2)->default(0)->comment('服务未开票税额'); + $table->decimal('service_subtotal_amount', 18, 2)->default(0)->comment('服务收入小计'); + $table->decimal('service_subtotal_tax', 18, 2)->default(0)->comment('服务税金小计'); + $table->decimal('total_revenue', 18, 2)->default(0)->comment('合计收入'); + $table->decimal('total_tax', 18, 2)->default(0)->comment('合计税金'); + // 进项发票 + $table->decimal('input_goods_amount', 18, 2)->default(0)->comment('进项商品金额'); + $table->decimal('input_goods_tax', 18, 2)->default(0)->comment('进项商品税额'); + $table->decimal('input_service_amount', 18, 2)->default(0)->comment('进项服务费金额'); + $table->decimal('input_service_tax', 18, 2)->default(0)->comment('进项服务费税额'); + $table->decimal('input_other_amount', 18, 2)->default(0)->comment('进项其他金额'); + $table->decimal('input_other_tax', 18, 2)->default(0)->comment('进项其他税额'); + $table->decimal('input_subtotal_amount', 18, 2)->default(0)->comment('进项金额合计'); + $table->decimal('input_subtotal_tax', 18, 2)->default(0)->comment('进项税额合计'); + $table->decimal('extra_credit', 18, 2)->default(0)->comment('加计抵减'); + $table->decimal('retained_credit', 18, 2)->default(0)->comment('留抵税金'); + $table->decimal('vat_payable', 18, 2)->default(0)->comment('本月应交增值税'); + $table->decimal('city_tax', 18, 2)->default(0)->comment('城建税金'); + $table->decimal('education_surcharge', 18, 2)->default(0)->comment('教育费附加'); + $table->decimal('local_education_surcharge', 18, 2)->default(0)->comment('地方教育费附加'); + $table->decimal('stamp_tax', 18, 2)->default(0)->comment('印花税'); + $table->decimal('union_fee', 18, 2)->default(0)->comment('工会经费'); + $table->decimal('individual_tax', 18, 2)->default(0)->comment('个税'); + $table->decimal('income_tax', 18, 2)->default(0)->comment('所得税'); + $table->decimal('garbage_fee', 18, 2)->default(0)->comment('垃圾处理费'); + $table->decimal('other_penalty', 18, 2)->default(0)->comment('其他(滞纳金)'); + $table->decimal('total_tax_paid', 18, 2)->default(0)->comment('税金合计'); + $table->decimal('tax_burden_rate', 8, 4)->default(0)->comment('税负'); + $table->timestamps(); + }); + + // 4. 公司店铺数据 + Schema::create('biz_company_stores', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('company_name')->default('')->comment('公司名称'); + $table->string('store_name')->default('')->comment('店铺名称'); + $table->decimal('store_revenue', 18, 2)->default(0)->comment('店铺业绩'); + $table->decimal('company_revenue', 18, 2)->default(0)->comment('公司业绩'); + $table->boolean('trigger_store_change')->default(false)->comment('是否触发店铺变更流程'); + $table->boolean('trigger_company_register')->default(false)->comment('是否触发公司注册流程'); + $table->string('period')->default(''); + $table->timestamps(); + }); + + // 5. 店铺变更统计表 + Schema::create('ak_biz_store_changes', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('store_name')->default('')->comment('店铺名称'); + $table->string('store_entity')->default('')->comment('店铺主体'); + $table->date('change_date')->nullable()->comment('变更时间'); + $table->string('receiving_account')->default('')->comment('收款账户信息'); + $table->string('previous_entity')->default('')->comment('历史主体'); + $table->timestamps(); + }); + + // 6. 发票基础信息+发票数据报表 + Schema::create('ak_biz_invoice_basic', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('store_name')->default('')->comment('店铺名称'); + $table->string('store_entity')->default('')->comment('店铺主体'); + $table->string('invoice_issuer')->default('')->comment('开票员'); + $table->decimal('invoice_limit', 18, 2)->default(0)->comment('开票额度'); + $table->decimal('output_amount', 18, 2)->default(0)->comment('销项金额'); + $table->decimal('input_amount', 18, 2)->default(0)->comment('进项金额'); + $table->string('period')->default(''); + $table->timestamps(); + }); + + // 7. 店铺后台开票 + Schema::create('ak_biz_store_invoice', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('store')->default('')->comment('店铺'); + $table->string('seller_name')->default('')->comment('销售方名称'); + $table->string('seller_tax_no')->default('')->comment('销售方税号'); + $table->string('buyer_name')->default('')->comment('购买方名称'); + $table->string('buyer_tax_no')->default('')->comment('购买方税号'); + $table->decimal('invoice_amount', 18, 2)->default(0)->comment('开票金额'); + $table->string('invoice_category')->default('')->comment('类目'); + $table->string('invoice_type')->default('')->comment('开票类型,普票/专票'); + $table->date('invoice_date')->nullable()->comment('开票日期'); + $table->timestamps(); + }); + + // 8. 资金预测表(月度资金收支预算表) + Schema::create('ak_biz_fund_forecast', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('period')->default('')->comment('预算填报月份'); + $table->string('actual_period')->default('')->comment('实际填报月份'); + // 收入明细 + $table->decimal('store_balance_budget', 18, 2)->default(0)->comment('店铺余额预算'); + $table->decimal('store_balance_actual', 18, 2)->default(0)->comment('店铺余额实际'); + $table->decimal('bank_balance_budget', 18, 2)->default(0)->comment('银行余额预算'); + $table->decimal('bank_balance_actual', 18, 2)->default(0)->comment('银行余额实际'); + $table->decimal('other_balance_budget', 18, 2)->default(0)->comment('其他账户余额预算'); + $table->decimal('other_balance_actual', 18, 2)->default(0)->comment('其他账户余额实际'); + $table->decimal('expected_inbound_budget', 18, 2)->default(0)->comment('预计本月回款预算'); + $table->decimal('expected_inbound_actual', 18, 2)->default(0)->comment('预计本月回款实际'); + $table->decimal('income_total_budget', 18, 2)->default(0)->comment('收入合计预算'); + $table->decimal('income_total_actual', 18, 2)->default(0)->comment('收入合计实际'); + // 支出明细 + $table->decimal('previous_unpaid_budget', 18, 2)->default(0)->comment('往期未付预算'); + $table->decimal('previous_unpaid_actual', 18, 2)->default(0)->comment('往期未付实际'); + $table->decimal('advance_budget', 18, 2)->default(0)->comment('借支预算'); + $table->decimal('advance_actual', 18, 2)->default(0)->comment('借支实际'); + $table->decimal('supplier_budget', 18, 2)->default(0)->comment('供应商预算'); + $table->decimal('supplier_actual', 18, 2)->default(0)->comment('供应商实际'); + $table->decimal('cash_budget', 18, 2)->default(0)->comment('现结预算'); + $table->decimal('cash_actual', 18, 2)->default(0)->comment('现结实际'); + $table->decimal('express_budget', 18, 2)->default(0)->comment('快递预算'); + $table->decimal('express_actual', 18, 2)->default(0)->comment('快递实际'); + $table->decimal('paper_press_budget', 18, 2)->default(0)->comment('纸厂压贴预算'); + $table->decimal('paper_press_actual', 18, 2)->default(0)->comment('纸厂压贴实际'); + $table->decimal('edge_banding_budget', 18, 2)->default(0)->comment('封边条预算'); + $table->decimal('edge_banding_actual', 18, 2)->default(0)->comment('封边条实际'); + $table->decimal('board_budget', 18, 2)->default(0)->comment('板材、三合板预算'); + $table->decimal('board_actual', 18, 2)->default(0)->comment('板材、三合板实际'); + $table->decimal('salary_budget', 18, 2)->default(0)->comment('工资预算'); + $table->decimal('salary_actual', 18, 2)->default(0)->comment('工资实际'); + $table->decimal('daily_expense_budget', 18, 2)->default(0)->comment('日常费用预算'); + $table->decimal('daily_expense_actual', 18, 2)->default(0)->comment('日常费用实际'); + $table->decimal('zhongcai_budget', 18, 2)->default(0)->comment('种菜预算'); + $table->decimal('zhongcai_actual', 18, 2)->default(0)->comment('种菜实际'); + $table->decimal('red_packet_budget', 18, 2)->default(0)->comment('红包预算'); + $table->decimal('red_packet_actual', 18, 2)->default(0)->comment('红包实际'); + $table->decimal('corporate_deduction_budget', 18, 2)->default(0)->comment('对公扣款预算'); + $table->decimal('corporate_deduction_actual', 18, 2)->default(0)->comment('对公扣款实际'); + $table->decimal('expense_total_budget', 18, 2)->default(0)->comment('支出合计预算'); + $table->decimal('expense_total_actual', 18, 2)->default(0)->comment('支出合计实际'); + // 资金结余 + $table->decimal('reserved_balance_budget', 18, 2)->default(0)->comment('银行账户、店铺预留预算'); + $table->decimal('reserved_balance_actual', 18, 2)->default(0)->comment('银行账户、店铺预留实际'); + $table->decimal('net_balance_budget', 18, 2)->default(0)->comment('本月净结余预算'); + $table->decimal('net_balance_actual', 18, 2)->default(0)->comment('本月净结余实际'); + $table->timestamps(); + }); + + // 9. 店铺收款账户信息 + Schema::create('ak_biz_store_receiving_accounts', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('platform')->default('')->comment('店铺平台'); + $table->string('store_name')->default('')->comment('店铺名称'); + $table->string('wallet_no')->default('')->comment('钱包号'); + $table->string('store_entity')->default('')->comment('店铺主体名称'); + $table->string('login_account')->default('')->comment('登录账号'); + $table->string('login_password')->default('')->comment('登录密码'); + $table->string('receiving_method')->default('')->comment('收款方式'); + $table->string('receiving_account_name')->default('')->comment('收款账户名称'); + $table->string('account_number')->default('')->comment('支付宝账号/银行账号'); + $table->string('bank_name')->default('')->comment('开户银行'); + $table->timestamps(); + }); + + // 10. 银行账户信息 + Schema::create('ak_biz_bank_accounts', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('account_name')->default('')->comment('账户名称'); + $table->string('account_type')->default('')->comment('账户类型:基本户/一般户/个人账户/个人微信/个人支付宝'); + $table->string('bank_account')->default('')->comment('银行账号'); + $table->string('bank_name')->default('')->comment('开户行'); + $table->string('maker_ukey_password')->default('')->comment('ukey密码(制单)'); + $table->string('maker_login_password')->default('')->comment('登录密码(制单)'); + $table->string('auditor_ukey_password')->default('')->comment('ukey密码(审核)'); + $table->string('auditor_login_password')->default('')->comment('登录密码(审核)'); + $table->timestamps(); + }); + + // 11. 快递收款系统 + Schema::create('ak_biz_express_receiving', function (Blueprint $table) { + $table->id(); + $table->foreignId('company_id')->constrained('companies')->cascadeOnDelete(); + $table->string('express_name')->default('')->comment('快递名称'); + $table->string('receiving_type')->default('')->comment('收款类型:对公/个人'); + $table->string('receiving_name')->default('')->comment('收款名称'); + $table->string('account_number')->default('')->comment('收款账号'); + $table->string('bank_name')->default('')->comment('开户行'); + $table->timestamps(); + }); + } + + public function down(): void + { + foreach ([ + 'ak_biz_tax_data', + 'ak_biz_sw_planning', + 'biz_sw_actual', + 'biz_company_stores', + 'ak_biz_store_changes', + 'ak_biz_invoice_basic', + 'ak_biz_store_invoice', + 'ak_biz_fund_forecast', + 'ak_biz_store_receiving_accounts', + 'ak_biz_bank_accounts', + 'ak_biz_express_receiving', + ] as $table) { + Schema::dropIfExists($table); + } + } +}; diff --git a/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php b/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php new file mode 100644 index 00000000..4a5e99f0 --- /dev/null +++ b/database/migrations/2026_07_22_000000_create_business_basic_information_tables.php @@ -0,0 +1,57 @@ +increments('id'); + $table->integer('company_id')->default(1)->index(); + $table->string('unified_social_credit_code')->nullable()->comment('统一社会信用代码'); + $table->string('company_name')->nullable()->comment('公司名称'); + $table->decimal('subscribed_capital', 15, 2)->nullable()->comment('认缴注册资本(万元)'); + $table->string('company_type')->nullable()->comment('类型'); + $table->date('establishment_date')->nullable()->comment('成立日期'); + $table->string('legal_representative')->nullable()->comment('法定代表人'); + $table->string('registered_address')->nullable()->comment('注册地址(住所)'); + $table->text('business_scope')->nullable()->comment('经营范围'); + $table->string('basic_deposit_account_number')->nullable()->comment('基本存款账户编号'); + $table->string('company_status')->nullable()->comment('公司状态'); + $table->string('financial_officer')->nullable()->comment('财务负责人'); + $table->string('supervisor')->nullable()->comment('监事'); + $table->string('taxpayer_category')->nullable()->comment('纳税类别及类型'); + $table->text('shareholders')->nullable()->comment('股东'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('business_store_information', function (Blueprint $table) { + $table->increments('id'); + $table->integer('company_id')->default(1)->index(); + $table->date('opened_at')->nullable()->comment('开店时间'); + $table->string('erp_store_name')->nullable()->comment('ERP系统店铺名称'); + $table->string('unified_social_credit_code')->nullable()->comment('公司社会统一信用码'); + $table->string('company_name')->nullable()->comment('公司名称'); + $table->string('trademark_number')->nullable()->comment('商标号'); + $table->string('trademark_name')->nullable()->comment('商标名称'); + $table->string('platform')->nullable()->comment('平台'); + $table->string('profit_center')->nullable()->comment('店铺所属利润中心'); + $table->string('store_manager')->nullable()->comment('店铺负责人'); + $table->string('store_status')->nullable()->comment('店铺状态'); + $table->string('store_type')->nullable()->comment('店铺类型'); + $table->string('channel_mode')->nullable()->comment('渠道模式'); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('business_store_information'); + Schema::dropIfExists('business_company_information'); + } +}; diff --git a/modules/EcommerceFinance/Services/UpstreamOdbcConnection.php b/modules/EcommerceFinance/Services/UpstreamOdbcConnection.php index 3a6975f8..fcd9183d 100644 --- a/modules/EcommerceFinance/Services/UpstreamOdbcConnection.php +++ b/modules/EcommerceFinance/Services/UpstreamOdbcConnection.php @@ -145,7 +145,7 @@ PHP; $db = trim((string) ($connection['database'] ?? '')); $user = trim((string) ($connection['username'] ?? '')); $pass = (string) ($connection['password'] ?? ''); - $driver = trim((string) ($connection['odbc_driver'] ?? 'FreeTDS'), '{} '); + $driver = self::resolveDriver((string) ($connection['odbc_driver'] ?? 'FreeTDS')); $tdsVersion = trim((string) ($connection['tds_version'] ?? '7.4')); $encryption = strtolower(trim((string) ($connection['encryption'] ?? 'off'))); $connectTimeout = min(max((int) ($connection['connect_timeout'] ?? 5), 1), 30); @@ -167,17 +167,30 @@ PHP; $applicationIntent = $applicationIntent === '' ? '' : 'ReadOnly'; - $dsn = sprintf( - 'odbc:Driver={%s};Server=%s;Port=%d;Database=%s;UID=%s;PWD=%s;TDS_Version=%s;ClientCharset=UTF-8;Connect Timeout=%d', - str_replace('}', '}}', $driver), - self::quoteOdbcValue($host), - $port, - self::quoteOdbcValue($db), - self::quoteOdbcValue($user), - self::quoteOdbcValue($pass), - $tdsVersion, - $connectTimeout - ); + if (strcasecmp($driver, 'FreeTDS') === 0) { + $dsn = sprintf( + 'odbc:Driver={%s};Server=%s;Port=%d;Database=%s;UID=%s;PWD=%s;TDS_Version=%s;ClientCharset=UTF-8;Connect Timeout=%d', + str_replace('}', '}}', $driver), + self::quoteOdbcValue($host), + $port, + self::quoteOdbcValue($db), + self::quoteOdbcValue($user), + self::quoteOdbcValue($pass), + $tdsVersion, + $connectTimeout + ); + } else { + $dsn = sprintf( + 'odbc:Driver={%s};Server=%s;Database=%s;UID=%s;PWD=%s;Connection Timeout=%d', + str_replace('}', '}}', $driver), + self::quoteOdbcValue($host . ',' . $port), + self::quoteOdbcValue($db), + self::quoteOdbcValue($user), + self::quoteOdbcValue($pass), + $connectTimeout + ); + } + $dsn .= self::encryptionAttributes($driver, $encryption); if ($applicationIntent !== '') { @@ -238,4 +251,21 @@ PHP; ? $attributes : $attributes . ';TrustServerCertificate=yes'; } + + private static function resolveDriver(string $configured): string + { + $override = trim((string) config('upstreams.runtime.odbc_driver', ''), '{} '); + + if ($override !== '') { + return $override; + } + + $configured = trim($configured, '{} '); + + if (PHP_OS_FAMILY === 'Windows' && strcasecmp($configured, 'FreeTDS') === 0) { + return 'SQL Server'; + } + + return $configured; + } } diff --git a/resources/views/business/pages/show.blade.php b/resources/views/business/pages/show.blade.php index 11dc6cae..ef40d2e6 100644 --- a/resources/views/business/pages/show.blade.php +++ b/resources/views/business/pages/show.blade.php @@ -1,6 +1,6 @@ - {{ $page['title'] ?? $section['title'] }} + {{ $activeTab['title'] ?? ($page['title'] ?? $section['title']) }} @@ -102,12 +102,272 @@ box-shadow: none; } - .business-blank-page { - min-height: 34rem; - margin-top: 1.5rem; + .biz-card { + background: #ffffff; border: 1px solid #eef0f4; border-radius: 1rem; + box-shadow: 0 6px 18px rgba(31, 41, 55, 0.04); + } + + .biz-alert { + margin-bottom: 1rem; + padding: 0.75rem 1rem; + border: 1px solid #bbf7d0; + border-radius: 0.5rem; + background: #f0fdf4; + color: #166534; + font-size: 0.875rem; + } + + .biz-alert-error { + border-color: #fecaca; + background: #fef2f2; + color: #991b1b; + } + + .biz-import-form { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .biz-import-input { + max-width: 15rem; + font-size: 0.75rem; + } + + .biz-card + .biz-card { + margin-top: 1.25rem; + } + + .biz-card-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem 1.5rem; + border-bottom: 1px solid #f3f4f6; + } + + .biz-card-title { + font-size: 1rem; + font-weight: 700; + color: #111827; + display: flex; + align-items: center; + gap: 0.5rem; + } + + .biz-card-actions { + display: flex; + align-items: center; + gap: 0.5rem; + } + + .biz-table-wrap { + overflow-x: auto; + } + + .biz-table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + font-size: 0.875rem; + } + + .biz-table thead th { + position: sticky; + top: 0; + background: #f9fafb; + color: #4b5563; + font-weight: 600; + text-align: left; + padding: 0.75rem 1rem; + border-bottom: 1px solid #e5e7eb; + white-space: nowrap; + } + + .biz-table tbody td { + padding: 0.65rem 1rem; + border-bottom: 1px solid #f3f4f6; + color: #1f2937; + white-space: nowrap; + } + + .biz-table tbody tr:hover td { + background: #f9fafb; + } + + .biz-table .num { + text-align: right; + font-variant-numeric: tabular-nums; + color: #111827; + } + + .biz-table .actions-cell { + white-space: nowrap; + text-align: right; + } + + .biz-empty { + padding: 4rem 1rem; + text-align: center; + color: #6b7280; + } + + .biz-empty .material-icons-outlined { + font-size: 3rem; + color: #d1d5db; + } + + .biz-btn { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.4rem 0.85rem; + border-radius: 0.5rem; + font-size: 0.8125rem; + font-weight: 600; + transition: background 150ms ease, color 150ms ease; + border: 1px solid transparent; + cursor: pointer; + } + + .biz-btn-primary { + background: #4f46e5; + color: #ffffff; + } + + .biz-btn-primary:hover { + background: #4338ca; + } + + .biz-btn-ghost { + background: transparent; + color: #4b5563; + border-color: #e5e7eb; + } + + .biz-btn-ghost:hover { + background: #f3f4f6; + } + + .biz-btn-danger-ghost { + background: transparent; + color: #dc2626; + border-color: #fecaca; + } + + .biz-btn-danger-ghost:hover { + background: #fef2f2; + } + + .biz-badge { + display: inline-block; + padding: 0.15rem 0.55rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; + } + + .biz-badge-yes { background: #dcfce7; color: #15803d; } + .biz-badge-no { background: #f3f4f6; color: #6b7280; } + + /* Modal */ + .biz-modal { + position: fixed; + inset: 0; + background: rgba(15, 23, 42, 0.45); + display: none; + align-items: flex-start; + justify-content: center; + padding: 4rem 1rem; + z-index: 60; + overflow-y: auto; + } + + .biz-modal.is-open { + display: flex; + } + + .biz-modal-card { background: #ffffff; + border-radius: 1rem; + box-shadow: 0 20px 60px rgba(15, 23, 42, 0.25); + width: 100%; + max-width: 720px; + padding: 1.5rem; + } + + .biz-modal-title { + font-size: 1.125rem; + font-weight: 700; + color: #111827; + margin-bottom: 1rem; + } + + .biz-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0.85rem; + } + + .biz-form-grid .full { + grid-column: span 2; + } + + .biz-field-label { + display: block; + font-size: 0.75rem; + font-weight: 600; + color: #6b7280; + margin-bottom: 0.25rem; + } + + .biz-field-input, + .biz-field-select, + .biz-field-textarea { + width: 100%; + border: 1px solid #e5e7eb; + border-radius: 0.5rem; + padding: 0.5rem 0.65rem; + font-size: 0.875rem; + color: #111827; + background: #ffffff; + } + + .biz-field-input:focus, + .biz-field-select:focus, + .biz-field-textarea:focus { + border-color: #6366f1; + outline: 2px solid rgba(99, 102, 241, 0.25); + } + + .biz-modal-foot { + display: flex; + justify-content: flex-end; + gap: 0.5rem; + margin-top: 1.25rem; + } + + .biz-pagination { + display: flex; + gap: 0.25rem; + padding: 0.75rem 1.5rem; + border-top: 1px solid #f3f4f6; + color: #4b5563; + font-size: 0.85rem; + align-items: center; + justify-content: space-between; + } + + .biz-pagination a, + .biz-pagination span { + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; + } + + .biz-pagination a:hover { + background: #f3f4f6; } .business-content-section { @@ -134,6 +394,13 @@ 'my-6', 'business-page--marketing-account-bills' => $isMarketingAccountBillsPage, ])> + @if (is_string(session('success'))) +
{{ session('success') }}
+ @endif + + @if ($errors->has('import')) + + @endif @if ($tabs->isNotEmpty())