diff --git a/api/common/helpers.php b/api/common/helpers.php index b529491..30ad0f7 100644 --- a/api/common/helpers.php +++ b/api/common/helpers.php @@ -90,3 +90,115 @@ function buildUpdate($data) } return [implode(',', $sets), array_values($data)]; } + +/** 数字字符串或 null(空串转 null,避免写入空值) */ +function numOrNull($v) +{ + $v = trim((string)$v); + return ($v === '') ? null : $v; +} + +/** 字符串或 null(空串转 null) */ +function strOrNull($v) +{ + if ($v === null) { + return null; + } + $v = trim((string)$v); + return ($v === '') ? null : $v; +} + +/** + * 保存企业年度财务明细(整表替换:先删后插) + * @param PDO $pdo + * @param int $companyId + * @param array $financials JSON 解码后的数组 + */ +function saveCompanyFinancials($pdo, $companyId, $financials) +{ + $del = $pdo->prepare("DELETE FROM company_financials WHERE company_id = ?"); + $del->execute([$companyId]); + if (empty($financials)) { + return; + } + + $ins = $pdo->prepare( + "INSERT INTO company_financials + (company_id, fiscal_year, employee_count, total_revenue, net_profit, total_assets, + total_liabilities, owner_equity, gross_margin, net_margin, debt_ratio, financial_report_url, data_source) + VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" + ); + $seen = []; + foreach ($financials as $f) { + if (!is_array($f)) { + continue; + } + $year = (int)($f['fiscal_year'] ?? 0); + if ($year < 1990 || $year > 2100) { + continue; + } + if (isset($seen[$year])) { + continue; // 同一财年只保留一条 + } + $seen[$year] = true; + $ins->execute([ + $companyId, + $year, + numOrNull($f['employee_count'] ?? null), + numOrNull($f['total_revenue'] ?? null), + numOrNull($f['net_profit'] ?? null), + numOrNull($f['total_assets'] ?? null), + numOrNull($f['total_liabilities'] ?? null), + numOrNull($f['owner_equity'] ?? null), + numOrNull($f['gross_margin'] ?? null), + numOrNull($f['net_margin'] ?? null), + numOrNull($f['debt_ratio'] ?? null), + strOrNull($f['financial_report_url'] ?? null), + strOrNull($f['data_source'] ?? null) ?? '手动录入', + ]); + } +} + +/** + * 保存企业资质认证(整表替换:先删后插) + * @param PDO $pdo + * @param int $companyId + * @param array $certifications JSON 解码后的数组 + */ +function saveCompanyCertifications($pdo, $companyId, $certifications) +{ + $del = $pdo->prepare("DELETE FROM company_certifications WHERE company_id = ?"); + $del->execute([$companyId]); + if (empty($certifications)) { + return; + } + + $valid = $pdo->prepare("SELECT id FROM certification_types WHERE id = ?"); + $ins = $pdo->prepare( + "INSERT INTO company_certifications + (company_id, certification_type_id, certificate_number, issue_date, expiry_date) + VALUES (?,?,?,?,?)" + ); + $seen = []; + foreach ($certifications as $ct) { + if (!is_array($ct)) { + continue; + } + $tid = (int)($ct['certification_type_id'] ?? 0); + if ($tid <= 0 || isset($seen[$tid])) { + continue; + } + $valid->execute([$tid]); + if (!$valid->fetch()) { + continue; // 认证类型不存在 + } + $seen[$tid] = true; + $ins->execute([ + $companyId, + $tid, + strOrNull($ct['certificate_number'] ?? null), + strOrNull($ct['issue_date'] ?? null), + strOrNull($ct['expiry_date'] ?? null), + ]); + } +} diff --git a/api/company/add.php b/api/company/add.php index 374ee0c..aa1a212 100644 --- a/api/company/add.php +++ b/api/company/add.php @@ -26,5 +26,32 @@ $pdo = DB::getInstance()->getPdo(); $pdo->prepare("INSERT INTO companies $sql")->execute($params); $newId = (int)$pdo->lastInsertId(); +// 财务信息 / 资质认证(可选,整表替换) +if (array_key_exists('financials', $_POST) || array_key_exists('certifications', $_POST)) { + $pdo->beginTransaction(); + try { + if (array_key_exists('financials', $_POST)) { + $financials = json_decode($_POST['financials'], true); + if (!is_array($financials)) { + $pdo->rollBack(); + Response::error('财务信息格式错误', 400); + } + saveCompanyFinancials($pdo, $newId, $financials); + } + if (array_key_exists('certifications', $_POST)) { + $certs = json_decode($_POST['certifications'], true); + if (!is_array($certs)) { + $pdo->rollBack(); + Response::error('资质认证格式错误', 400); + } + saveCompanyCertifications($pdo, $newId, $certs); + } + $pdo->commit(); + } catch (Exception $e) { + $pdo->rollBack(); + Response::error('关联信息保存失败:' . $e->getMessage()); + } +} + logCurrent('add', 'company', 'companies', $newId, $data); Response::success(['id' => $newId], '新增成功'); diff --git a/api/company/cert_types.php b/api/company/cert_types.php new file mode 100644 index 0000000..11392e2 --- /dev/null +++ b/api/company/cert_types.php @@ -0,0 +1,15 @@ +getPdo(); +$types = $pdo->query("SELECT id, name FROM certification_types WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll(); + +Response::success(['types' => $types]); diff --git a/api/company/detail.php b/api/company/detail.php index aaa3dfe..d195cb0 100644 --- a/api/company/detail.php +++ b/api/company/detail.php @@ -48,6 +48,19 @@ $relations = $pdo->prepare( ); $relations->execute([$id]); +// 资质认证(瞪羚企业等,JOIN 字典表取名称) +$certs = $pdo->prepare( + "SELECT cc.certification_type_id, ct.name AS certification_name, cc.certificate_number, + cc.issue_date, cc.expiry_date + FROM company_certifications cc + INNER JOIN certification_types ct ON ct.id = cc.certification_type_id + WHERE cc.company_id = ? + ORDER BY ct.sort_order, ct.id" +); +$certs->execute([$id]); + +$certTypes = $pdo->query("SELECT id, name FROM certification_types WHERE is_active = 1 ORDER BY sort_order, id")->fetchAll(); + // 关联联系人(工作经历 + 联系方式) $contacts = $pdo->prepare( "SELECT p.id, p.full_name, @@ -71,4 +84,6 @@ Response::success([ 'financials' => $financials->fetchAll(), 'relations' => $relations->fetchAll(), 'contacts' => $contacts->fetchAll(), + 'certifications' => $certs->fetchAll(), + 'certification_types' => $certTypes, ]); diff --git a/api/company/employees.php b/api/company/employees.php index bac7aaa..33cfa9c 100644 --- a/api/company/employees.php +++ b/api/company/employees.php @@ -38,6 +38,10 @@ if ($jobLevel !== '') { $where[] = 'pwe.job_level = ?'; $params[] = $jobLevel; } +if (isset($_REQUEST['is_current']) && $_REQUEST['is_current'] !== '') { + $where[] = 'pwe.is_current = ?'; + $params[] = (int)$_REQUEST['is_current'] ? 1 : 0; +} $whereSql = implode(' AND ', $where); // 总数 diff --git a/api/company/products.php b/api/company/products.php new file mode 100644 index 0000000..ce088fd --- /dev/null +++ b/api/company/products.php @@ -0,0 +1,41 @@ +getPdo(); + +$chk = $pdo->prepare("SELECT COUNT(*) FROM companies WHERE id = ?"); +$chk->execute([$companyId]); +if ((int)$chk->fetchColumn() === 0) { + Response::error('企业不存在'); +} + +$stmt = $pdo->prepare("SELECT COUNT(*) FROM company_products WHERE company_id = ? AND is_active = 1"); +$stmt->execute([$companyId]); +$total = (int)$stmt->fetchColumn(); + +$offset = ($page - 1) * $limit; +$stmt = $pdo->prepare( + "SELECT id, category_name, category_description, is_core, is_active + FROM company_products + WHERE company_id = ? AND is_active = 1 + ORDER BY is_core DESC, id DESC + LIMIT $limit OFFSET $offset" +); +$stmt->execute([$companyId]); + +Response::success(['list' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'limit' => $limit]); diff --git a/api/company/update.php b/api/company/update.php index 8b34137..3879f6f 100644 --- a/api/company/update.php +++ b/api/company/update.php @@ -19,7 +19,9 @@ if ($id <= 0) { $data = extractFields(COMPANY_FIELDS); unset($data['display_name']); // 显示名称不允许通过编辑接口置空/改名,如需改名请走完整字段 -if (empty($data)) { +$hasFinancials = array_key_exists('financials', $_POST); +$hasCertifications = array_key_exists('certifications', $_POST); +if (empty($data) && !$hasFinancials && !$hasCertifications) { Response::error('没有需要更新的字段', 400); } @@ -32,9 +34,40 @@ if (!$old) { Response::error('企业不存在'); } -[$sets, $params] = buildUpdate($data); -$params[] = $id; -$pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params); +if (!empty($data)) { + [$sets, $params] = buildUpdate($data); + $params[] = $id; + $pdo->prepare("UPDATE companies SET $sets WHERE id = ?")->execute($params); +} -logCurrent('update', 'company', 'companies', $id, ['before' => $old, 'after' => $data]); +// 财务信息 / 资质认证(可选,整表替换) +if ($hasFinancials || $hasCertifications) { + $pdo->beginTransaction(); + try { + if ($hasFinancials) { + $financials = json_decode($_POST['financials'], true); + if (!is_array($financials)) { + $pdo->rollBack(); + Response::error('财务信息格式错误', 400); + } + saveCompanyFinancials($pdo, $id, $financials); + } + if ($hasCertifications) { + $certs = json_decode($_POST['certifications'], true); + if (!is_array($certs)) { + $pdo->rollBack(); + Response::error('资质认证格式错误', 400); + } + saveCompanyCertifications($pdo, $id, $certs); + } + $pdo->commit(); + } catch (Exception $e) { + $pdo->rollBack(); + Response::error('关联信息保存失败:' . $e->getMessage()); + } +} + +if (!empty($data)) { + logCurrent('update', 'company', 'companies', $id, ['before' => $old, 'after' => $data]); +} Response::success(null, '更新成功'); diff --git a/api/person/companies.php b/api/person/companies.php index 4ba7dd6..3a47f81 100644 --- a/api/person/companies.php +++ b/api/person/companies.php @@ -25,12 +25,20 @@ if ((int)$chk->fetchColumn() === 0) { Response::error('人员不存在'); } +$where = ['pwe.person_id = ?', 'pwe.is_active = 1']; +$params = [$personId]; +if (isset($_REQUEST['is_current']) && $_REQUEST['is_current'] !== '') { + $where[] = 'pwe.is_current = ?'; + $params[] = (int)$_REQUEST['is_current'] ? 1 : 0; +} +$whereSql = implode(' AND ', $where); + $stmt = $pdo->prepare( "SELECT COUNT(*) FROM person_work_experiences pwe INNER JOIN companies c ON c.id = pwe.company_id - WHERE pwe.person_id = ? AND pwe.is_active = 1" + WHERE $whereSql" ); -$stmt->execute([$personId]); +$stmt->execute($params); $total = (int)$stmt->fetchColumn(); $offset = ($page - 1) * $limit; @@ -42,11 +50,11 @@ $stmt = $pdo->prepare( pwe.start_date, pwe.end_date, pwe.is_current FROM person_work_experiences pwe INNER JOIN companies c ON c.id = pwe.company_id - WHERE pwe.person_id = ? AND pwe.is_active = 1 + WHERE $whereSql ORDER BY pwe.is_current DESC, pwe.start_date DESC LIMIT $limit OFFSET $offset" ); -$stmt->execute([$personId]); +$stmt->execute($params); $list = $stmt->fetchAll(); Response::success(['list' => $list, 'total' => $total, 'page' => $page, 'limit' => $limit]); diff --git a/api/person/products.php b/api/person/products.php new file mode 100644 index 0000000..c8e84e7 --- /dev/null +++ b/api/person/products.php @@ -0,0 +1,48 @@ +getPdo(); + +$chk = $pdo->prepare("SELECT COUNT(*) FROM persons WHERE id = ?"); +$chk->execute([$personId]); +if ((int)$chk->fetchColumn() === 0) { + Response::error('人员不存在'); +} + +$join = "FROM person_work_experiences pwe + INNER JOIN company_products cp ON cp.company_id = pwe.company_id AND cp.is_active = 1 + INNER JOIN companies c ON c.id = pwe.company_id + WHERE pwe.person_id = ? AND pwe.is_active = 1"; + +$stmt = $pdo->prepare("SELECT COUNT(DISTINCT cp.id) $join"); +$stmt->execute([$personId]); +$total = (int)$stmt->fetchColumn(); + +$offset = ($page - 1) * $limit; +$stmt = $pdo->prepare( + "SELECT DISTINCT cp.id, cp.category_name, cp.category_description, cp.is_core, cp.is_active, + CASE WHEN c.name_zh IS NOT NULL AND c.name_zh <> '' THEN c.name_zh + ELSE COALESCE(c.name_en, c.display_name) END AS company_name + $join + ORDER BY company_name, cp.id DESC + LIMIT $limit OFFSET $offset" +); +$stmt->execute([$personId]); + +Response::success(['list' => $stmt->fetchAll(), 'total' => $total, 'page' => $page, 'limit' => $limit]); diff --git a/static/css/style.css b/static/css/style.css index 7deacd7..6f9ea54 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -462,6 +462,67 @@ table.grid .ops a:hover { text-decoration: underline; } color: #fff; } +/* ---------- 编辑弹窗 Tab 卡(工商信息/财务信息/资质认证) ---------- */ +.edit-tabs { border: 1px solid #e5e9f0; border-radius: 6px; } +.edit-tabs-head { + display: flex; + border-bottom: 1px solid #e5e9f0; + background: #f7f9fc; + border-radius: 6px 6px 0 0; +} +.edit-tabs-head .edit-tab { + padding: 10px 24px; + font-size: 13px; + color: #5a6472; + cursor: pointer; + border-right: 1px solid #e5e9f0; + user-select: none; + background: transparent; +} +.edit-tabs-head .edit-tab:hover { color: #2a5298; } +.edit-tabs-head .edit-tab.active { + background: #fff; + color: #2a5298; + font-weight: bold; + border-bottom: 2px solid #2a5298; + margin-bottom: -1px; +} +.edit-tabs-body { + padding: 14px 16px; + background: #fff; + border-radius: 0 0 6px 6px; + max-height: 62vh; + overflow-y: auto; +} +.fin-year { + border: 1px solid #eef0f4; + border-radius: 6px; + padding: 10px 14px; + margin-bottom: 10px; + background: #fbfcfe; +} +.fin-year-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } +.fin-year-head b { font-size: 13px; color: #1e2a3a; font-weight: bold; } +.fin-year-head input { + width: 100px; + border: 1px solid #d9dee8; + border-radius: 4px; + padding: 5px 8px; + font-size: 13px; + margin-left: 6px; +} +.cert-row { display: flex; gap: 8px; margin-bottom: 8px; align-items: center; flex-wrap: wrap; } +.cert-row select, .cert-row input { + border: 1px solid #d9dee8; + border-radius: 4px; + padding: 7px 8px; + font-size: 13px; + background: #fff; +} +.cert-row select { width: 200px; } +.cert-row input[type="text"] { width: 170px; } +.cert-row input[type="date"] { width: 140px; } + /* ---------- 页脚(版权/备案) ---------- */ .footer { padding: 14px 22px; diff --git a/static/js/company.js b/static/js/company.js index b708ddc..d9f3a7c 100644 --- a/static/js/company.js +++ b/static/js/company.js @@ -29,7 +29,6 @@ $(function () { ' ' + ' ' + ' ' + - ' ' + ' ' + '
| ID | 公司名称 | 行业 | 行业细分 | 国家 | 业务角色 | 注册号 | 法人 | 上市 | 创建时间 | 操作 | ' + ' 详情' + ' 员工' + + ' 产品' + ' 编辑' + - ' 删除' + ' | '; }); if (!rows.length) { @@ -87,18 +86,26 @@ $(function () { }); /* ---------- 新增/编辑 ---------- */ - $('#btn-add').on('click', function () { openForm(null); }); + $('#btn-add').on('click', function () { + httpGet('company/cert_types.php').then(function (d) { + openForm(null, [], [], d.types || []); + }); + }); window.editCompany = function (id) { httpGet('company/detail.php', { id: id }).then(function (d) { - openForm(d.company); + openForm(d.company, d.financials, d.certifications, d.certification_types); }); }; - function openForm(company) { + function openForm(company, financials, certifications, certTypes) { var isEdit = !!company; var c = company || {}; - var content = - ''; var idx = Dialog.open({ title: isEdit ? '编辑企业' : '新增企业', - area: ['760px', 'auto'], + area: ['820px', 'auto'], content: content, btn: false }); $('#form-cancel').on('click', function () { Dialog.close(idx); }); + $('#add-fin-row').on('click', function () { $('#fin-rows').append(finYearRowHtml(null)); }); + $('#add-cert-row').on('click', function () { $('#cert-rows').append(certRowHtml(null, certTypes)); }); $('#company-form').on('submit', function (e) { e.preventDefault(); @@ -143,6 +199,45 @@ $(function () { Dialog.error('公司名称或英文名称至少填写一项'); return; } + + // 年度财务明细(整表替换) + var financials = []; + $('#fin-rows .fin-year').each(function () { + var $y = $(this); + var year = $.trim($y.find('[name="fin-fiscal_year"]').val()); + if (!year) return; + financials.push({ + fiscal_year: year, + employee_count: $y.find('[name="fin-employee_count"]').val(), + total_revenue: $y.find('[name="fin-total_revenue"]').val(), + net_profit: $y.find('[name="fin-net_profit"]').val(), + total_assets: $y.find('[name="fin-total_assets"]').val(), + total_liabilities: $y.find('[name="fin-total_liabilities"]').val(), + owner_equity: $y.find('[name="fin-owner_equity"]').val(), + gross_margin: $y.find('[name="fin-gross_margin"]').val(), + net_margin: $y.find('[name="fin-net_margin"]').val(), + debt_ratio: $y.find('[name="fin-debt_ratio"]').val(), + financial_report_url: $y.find('[name="fin-financial_report_url"]').val(), + data_source: $y.find('[name="fin-data_source"]').val() + }); + }); + fd.append('financials', JSON.stringify(financials)); + + // 资质认证(整表替换) + var certs = []; + $('#cert-rows .cert-row').each(function () { + var $r = $(this); + var tid = $r.find('[name="cert-type"]').val(); + if (!tid) return; + certs.push({ + certification_type_id: tid, + certificate_number: $r.find('[name="cert-number"]').val(), + issue_date: $r.find('[name="cert-issue"]').val(), + expiry_date: $r.find('[name="cert-expiry"]').val() + }); + }); + fd.append('certifications', JSON.stringify(certs)); + httpPost(isEdit ? 'company/update.php' : 'company/add.php', fd, true).then(function () { Dialog.success('保存成功', function () { Dialog.close(idx); @@ -152,6 +247,56 @@ $(function () { }); } + /* ---------- 编辑弹窗 Tab 切换 ---------- */ + window.switchEditTab = function (el) { + var $tabs = $(el).closest('.edit-tabs'); + $tabs.find('.edit-tab').removeClass('active'); + $(el).addClass('active'); + $tabs.find('.edit-pane').hide(); + $('#edit-pane-' + $(el).data('tab')).show(); + }; + + /** 年度财务明细行模板 */ + function finYearRowHtml(f) { + f = f || {}; + var empty = function (v) { return (v === null || v === undefined) ? '' : v; }; + return '
|---|