Версия 2 с выгрузкой
This commit is contained in:
parent
8b7201500e
commit
4c092e2535
381
index.html
381
index.html
@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Валидатор БИН и ИИН — MVP</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/xlsx-js-style@1.2.0/dist/xlsx.bundle.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4f6fa;
|
||||
@ -90,7 +90,7 @@
|
||||
.hint { color: var(--muted); font-size: 13px; }
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
@ -201,7 +201,7 @@
|
||||
|
||||
<header>
|
||||
<h1>Валидатор БИН и ИИН в налоговых регистрах</h1>
|
||||
<p>Автоматическое определение типа по 5-му знаку · ФЛК-проверка локально · сверка с реестром КГД (demo-данные)</p>
|
||||
<p>Автоопределение типа по 5-му знаку · ФЛК локально · сверка с КГД (demo) · возраст компании · экспорт в .xlsx с подсветкой</p>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
@ -248,6 +248,11 @@
|
||||
<div class="lbl">КГД: внимание</div>
|
||||
<div class="sub">только для БИН</div>
|
||||
</div>
|
||||
<div class="stat warn">
|
||||
<div class="num" id="statAge">0</div>
|
||||
<div class="lbl">Возрастные риски</div>
|
||||
<div class="sub" id="statAgeSub">свежие фирмы, дети, >100 лет</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
@ -263,7 +268,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="secondary" id="exportBtn">Экспорт в CSV</button>
|
||||
<button class="secondary" id="exportBtn">Экспорт в Excel (.xlsx)</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -278,6 +283,7 @@
|
||||
<th>ФЛК</th>
|
||||
<th>Подтип</th>
|
||||
<th>Дата</th>
|
||||
<th>Возраст</th>
|
||||
<th>КГД: статус</th>
|
||||
<th>КГД: наименование</th>
|
||||
<th>Комментарий</th>
|
||||
@ -345,7 +351,32 @@
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
КГД-сверка (реестр налогоплательщиков): в этом MVP используется встроенный
|
||||
demo-словарь из нескольких компаний. В продакшене подменяется на вызов
|
||||
api.kgd.gov.kz или Playwright-сценарий.</pre>
|
||||
api.kgd.gov.kz или Playwright-сценарий.
|
||||
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Возрастные риски (для валидных номеров):
|
||||
|
||||
Для БИН (по дате регистрации):
|
||||
< 3 мес. 🚨 «свежая фирма» — возможный признак однодневки
|
||||
3–12 мес. ⚠️ «молодая» — повышенное внимание
|
||||
> 12 мес. без флага
|
||||
|
||||
Для ИИН (по дате рождения):
|
||||
< 16 лет 🚨 не может быть руководителем
|
||||
> 100 лет 🚨 проверьте корректность данных
|
||||
|
||||
──────────────────────────────────────────────────────────────────────
|
||||
Экспорт результатов в .xlsx:
|
||||
|
||||
1) Если был загружен Excel — открывается исходный файл, ячейки в столбце
|
||||
с БИН/ИИН подсвечиваются цветом (зелёный/жёлтый/красный) прямо
|
||||
по своим местам. Справа добавляются служебные колонки: Тип, ФЛК,
|
||||
Подтип, Дата, Возраст, КГД статус, КГД наименование, Комментарий.
|
||||
|
||||
2) В demo-режиме создаётся новый файл с этими же колонками.
|
||||
|
||||
Внимание: при экспорте оригинальные форматы ячеек (шрифты, цвета шапки)
|
||||
заменяются на стандартные — данные сохраняются, оформление нет.</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@ -646,26 +677,94 @@ function parseSheet(rows) {
|
||||
return { items, info: { ...det, letter: colLetter(det.colIdx) } };
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Возраст и риск-флаги
|
||||
// =============================================================
|
||||
function pluralRu(n, forms) {
|
||||
// forms = ["год","года","лет"]
|
||||
const n100 = Math.abs(n) % 100;
|
||||
if (n100 >= 11 && n100 <= 14) return forms[2];
|
||||
const n10 = n100 % 10;
|
||||
if (n10 === 1) return forms[0];
|
||||
if (n10 >= 2 && n10 <= 4) return forms[1];
|
||||
return forms[2];
|
||||
}
|
||||
|
||||
function computeAge(res, today = new Date()) {
|
||||
if (!res.is_valid) return null;
|
||||
const Y = today.getFullYear(), M = today.getMonth() + 1, D = today.getDate();
|
||||
|
||||
if (res.kind === "БИН") {
|
||||
const months = (Y - res.year) * 12 + (M - res.month);
|
||||
if (months < 0) {
|
||||
return { kind: "БИН", months, label: `в будущем (${res.date_str})`, risk: "impossible_bin" };
|
||||
}
|
||||
const years = Math.floor(months / 12);
|
||||
const rem = months % 12;
|
||||
let label;
|
||||
if (years === 0) label = `${months} мес`;
|
||||
else if (rem === 0) label = `${years} ${pluralRu(years, ["год","года","лет"])}`;
|
||||
else label = `${years} ${pluralRu(years, ["г.","г.","г."])} ${rem} мес`;
|
||||
|
||||
let risk = null;
|
||||
if (months < 3) risk = "fresh";
|
||||
else if (months < 12) risk = "young_company";
|
||||
return { kind: "БИН", months, label, risk };
|
||||
}
|
||||
|
||||
if (res.kind === "ИИН") {
|
||||
let age = Y - res.year;
|
||||
if (M < res.month || (M === res.month && D < res.day)) age--;
|
||||
let risk = null, label;
|
||||
if (age < 0) { risk = "impossible_iin"; label = "в будущем"; }
|
||||
else if (age < 16) { risk = "underage"; label = `${age} ${pluralRu(age, ["год","года","лет"])}`; }
|
||||
else if (age > 100) { risk = "overage"; label = `${age} ${pluralRu(age, ["год","года","лет"])}`; }
|
||||
else { label = `${age} ${pluralRu(age, ["год","года","лет"])}`; }
|
||||
return { kind: "ИИН", years: age, label, risk };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const AGE_RISK_TEXT = {
|
||||
fresh: "Компания зарегистрирована менее 3 мес. назад — возможный признак однодневки",
|
||||
young_company: "Компания младше 1 года — повышенное внимание при проверке",
|
||||
impossible_bin: "Дата регистрации в будущем — ошибка данных",
|
||||
underage: "Возраст менее 16 лет — не может быть руководителем/контрагентом",
|
||||
overage: "Возраст более 100 лет — проверьте корректность данных",
|
||||
impossible_iin: "Дата рождения в будущем — ошибка данных",
|
||||
};
|
||||
|
||||
function ageEmoji(risk) {
|
||||
if (!risk) return "";
|
||||
if (risk === "young_company") return "⚠️";
|
||||
return "🚨";
|
||||
}
|
||||
|
||||
// =============================================================
|
||||
// Состояние и рендер
|
||||
// =============================================================
|
||||
let currentResults = []; // [{ rowNum, res, kgd }]
|
||||
let currentResults = []; // [{ rowNum, res, kgd, age }]
|
||||
let originalBuffer = null;
|
||||
let originalFileName = null;
|
||||
let lastColumnInfo = null;
|
||||
|
||||
function runChecks(items) {
|
||||
currentResults = items.map(({ rowNum, value }) => {
|
||||
const res = validateId(value);
|
||||
const kgd = kgdLookup(res);
|
||||
return { rowNum, res, kgd };
|
||||
const age = computeAge(res);
|
||||
return { rowNum, res, kgd, age };
|
||||
});
|
||||
renderResults();
|
||||
}
|
||||
|
||||
function rowClass(res, kgd) {
|
||||
function rowClass(res, kgd, age) {
|
||||
if (!res.is_valid) return "row-err";
|
||||
if (res.kind === "БИН" && kgd) {
|
||||
if (!kgd.in_registry) return "row-warn";
|
||||
if (kgd.status !== "Действующий") return "row-warn";
|
||||
}
|
||||
if (age && age.risk) return "row-warn";
|
||||
return "row-ok";
|
||||
}
|
||||
|
||||
@ -701,9 +800,10 @@ function renderResults() {
|
||||
document.getElementById("resultsSection").style.display = "block";
|
||||
document.getElementById("emptyState").style.display = "none";
|
||||
|
||||
let okBin = 0, okIin = 0, errCount = 0, kgdProblems = 0;
|
||||
let okBin = 0, okIin = 0, errCount = 0, kgdProblems = 0, ageRisks = 0;
|
||||
let freshCnt = 0, underageCnt = 0, overageCnt = 0;
|
||||
|
||||
currentResults.forEach(({ rowNum, res, kgd }) => {
|
||||
currentResults.forEach(({ rowNum, res, kgd, age }) => {
|
||||
if (res.is_valid) {
|
||||
if (res.kind === "БИН") okBin++; else if (res.kind === "ИИН") okIin++;
|
||||
} else {
|
||||
@ -713,13 +813,28 @@ function renderResults() {
|
||||
!kgd.in_registry || kgd.status !== "Действующий"
|
||||
);
|
||||
if (kgdProblem) kgdProblems++;
|
||||
if (age && age.risk) {
|
||||
ageRisks++;
|
||||
if (age.risk === "fresh" || age.risk === "young_company") freshCnt++;
|
||||
if (age.risk === "underage") underageCnt++;
|
||||
if (age.risk === "overage" || age.risk === "impossible_iin" || age.risk === "impossible_bin") overageCnt++;
|
||||
}
|
||||
|
||||
const dateCell = res.kind === "БИН" && res.date_str ? res.date_str
|
||||
: res.kind === "ИИН" && res.date_str ? res.date_str
|
||||
const dateCell = res.date_str ?? "—";
|
||||
const ageCell = age
|
||||
? `${escapeHtml(age.label)} ${ageEmoji(age.risk)}`
|
||||
: "—";
|
||||
|
||||
// Комментарий: ошибка ФЛК ИЛИ возрастной риск
|
||||
let commentHtml = "";
|
||||
if (!res.is_valid) {
|
||||
commentHtml = `<span class="err-msg">${escapeHtml(res.error_msg || "")}</span>`;
|
||||
} else if (age && age.risk) {
|
||||
commentHtml = `<span class="meta">${escapeHtml(AGE_RISK_TEXT[age.risk] || "")}</span>`;
|
||||
}
|
||||
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = rowClass(res, kgd);
|
||||
tr.className = rowClass(res, kgd, age);
|
||||
tr.innerHTML = `
|
||||
<td>${rowNum}</td>
|
||||
<td class="mono">${escapeHtml(res.original)}</td>
|
||||
@ -728,9 +843,10 @@ function renderResults() {
|
||||
<td>${flcBadge(res)}</td>
|
||||
<td>${res.subtype ? escapeHtml(res.subtype) : "—"}</td>
|
||||
<td>${escapeHtml(dateCell)}</td>
|
||||
<td>${ageCell}</td>
|
||||
<td>${kgdBadge(res, kgd)}</td>
|
||||
<td>${res.kind === "БИН" && res.is_valid && kgd && kgd.name ? escapeHtml(kgd.name) : "—"}</td>
|
||||
<td>${res.is_valid ? "" : `<span class="err-msg">${escapeHtml(res.error_msg || "")}</span>`}</td>
|
||||
<td>${commentHtml}</td>
|
||||
`;
|
||||
body.appendChild(tr);
|
||||
});
|
||||
@ -740,6 +856,14 @@ function renderResults() {
|
||||
document.getElementById("statValidSub").textContent = `БИН: ${okBin} · ИИН: ${okIin}`;
|
||||
document.getElementById("statErr").textContent = errCount;
|
||||
document.getElementById("statKgd").textContent = kgdProblems;
|
||||
document.getElementById("statAge").textContent = ageRisks;
|
||||
const subBits = [];
|
||||
if (freshCnt) subBits.push(`свежих: ${freshCnt}`);
|
||||
if (underageCnt) subBits.push(`<16 лет: ${underageCnt}`);
|
||||
if (overageCnt) subBits.push(`аномалии: ${overageCnt}`);
|
||||
document.getElementById("statAgeSub").textContent = subBits.length
|
||||
? subBits.join(" · ")
|
||||
: "свежие фирмы, дети, >100 лет";
|
||||
}
|
||||
|
||||
function showColumnInfo(info, fileName) {
|
||||
@ -784,6 +908,9 @@ document.getElementById("fileInput").addEventListener("change", async (e) => {
|
||||
);
|
||||
return;
|
||||
}
|
||||
originalBuffer = buf;
|
||||
originalFileName = file.name;
|
||||
lastColumnInfo = info;
|
||||
showColumnInfo(info, file.name);
|
||||
runChecks(items);
|
||||
} catch (err) {
|
||||
@ -796,37 +923,48 @@ document.getElementById("fileInput").addEventListener("change", async (e) => {
|
||||
|
||||
document.getElementById("sampleBtn").addEventListener("click", () => {
|
||||
document.getElementById("columnInfo").style.display = "none";
|
||||
originalBuffer = null;
|
||||
originalFileName = null;
|
||||
lastColumnInfo = null;
|
||||
|
||||
// Демо-набор: БИН, ИИН и ошибки
|
||||
// Демо-набор: БИН, ИИН, возрастные риски и ошибки
|
||||
const demo = [
|
||||
// Валидные БИН
|
||||
{ rowNum: 2, value: "941240000193" }, // ЮЛ, в реестре «Действующий»
|
||||
{ rowNum: 3, value: "020340000013" }, // ЮЛ, в реестре «Действующий»
|
||||
{ rowNum: 4, value: "980540000199" }, // ЮЛ, в реестре «Действующий»
|
||||
{ rowNum: 5, value: "991240001200" }, // ЮЛ, в реестре «Действующий»
|
||||
{ rowNum: 6, value: "060460000013" }, // ИП, в реестре «Приостановлен»
|
||||
{ rowNum: 7, value: "920150000029" }, // Филиал, в реестре «Ликвидирован»
|
||||
{ rowNum: 8, value: "050640001236" }, // ЮЛ, валиден ФЛК, НЕ найден в КГД
|
||||
// Валидные БИН — давно работающие компании
|
||||
{ rowNum: 2, value: "941240000193" }, // ЮЛ, КГД действующий
|
||||
{ rowNum: 3, value: "020340000013" }, // ЮЛ, КГД действующий
|
||||
{ rowNum: 4, value: "980540000199" }, // ЮЛ, КГД действующий
|
||||
{ rowNum: 5, value: "991240001200" }, // ЮЛ, КГД действующий
|
||||
{ rowNum: 6, value: "060460000013" }, // ИП, КГД «Приостановлен»
|
||||
{ rowNum: 7, value: "920150000029" }, // Филиал, КГД «Ликвидирован»
|
||||
{ rowNum: 8, value: "050640001236" }, // ЮЛ, не в реестре КГД
|
||||
// Возрастной риск — свежая фирма (зарегистрирована недавно)
|
||||
{ rowNum: 9, value: "260340000002" }, // БИН март 2026 → ~2 мес 🚨 fresh
|
||||
// Валидные ИИН
|
||||
{ rowNum: 9, value: "850615110054" }, // М, 15.06.1985
|
||||
{ rowNum: 10, value: "920320200124" }, // Ж, 20.03.1992
|
||||
{ rowNum: 11, value: "031108300114" }, // М, 08.11.2003
|
||||
{ rowNum: 12, value: "100214400127" }, // Ж, 14.02.2010
|
||||
// Ошибки
|
||||
{ rowNum: 13, value: "01031540001" }, // ERROR_LENGTH (11 цифр)
|
||||
{ rowNum: 14, value: "020340000451" }, // БИН с битой контрольной цифрой
|
||||
{ rowNum: 15, value: "941340000193" }, // БИН: месяц 13 → ERROR_BIN_MONTH
|
||||
{ rowNum: 16, value: "850632110054" }, // ИИН: день 32 → ERROR_IIN_DAY
|
||||
{ rowNum: 17, value: "850615070054" }, // ИИН: G=0 → ERROR_IIN_GENDER
|
||||
{ rowNum: 18, value: "941270000193" }, // 5-й знак=7 → ERROR_TYPE_DIGIT
|
||||
{ rowNum: 19, value: "941 240 000 193" }, // с пробелами → нормализуется → OK
|
||||
{ rowNum: 20, value: "" }, // EMPTY
|
||||
{ rowNum: 10, value: "850615110054" }, // М, 15.06.1985
|
||||
{ rowNum: 11, value: "920320200124" }, // Ж, 20.03.1992
|
||||
{ rowNum: 12, value: "031108300114" }, // М, 08.11.2003
|
||||
{ rowNum: 13, value: "100214400127" }, // Ж, 14.02.2010
|
||||
// Возрастной риск — несовершеннолетний / «120+»
|
||||
{ rowNum: 14, value: "150315400128" }, // Ж, 15.03.2015 → 11 лет 🚨 underage
|
||||
{ rowNum: 15, value: "200615110129" }, // М, 15.06.1920 → 105 лет 🚨 overage
|
||||
// Ошибки ФЛК
|
||||
{ rowNum: 16, value: "01031540001" }, // ERROR_LENGTH (11 цифр)
|
||||
{ rowNum: 17, value: "020340000451" }, // БИН с битой контрольной цифрой
|
||||
{ rowNum: 18, value: "941340000193" }, // БИН: месяц 13
|
||||
{ rowNum: 19, value: "850632110054" }, // ИИН: день 32
|
||||
{ rowNum: 20, value: "850615070054" }, // ИИН: G=0
|
||||
{ rowNum: 21, value: "941270000193" }, // 5-й знак=7
|
||||
{ rowNum: 22, value: "941 240 000 193" }, // нормализуется → OK
|
||||
{ rowNum: 23, value: "" }, // EMPTY
|
||||
];
|
||||
runChecks(demo);
|
||||
});
|
||||
|
||||
document.getElementById("clearBtn").addEventListener("click", () => {
|
||||
currentResults = [];
|
||||
originalBuffer = null;
|
||||
originalFileName = null;
|
||||
lastColumnInfo = null;
|
||||
document.getElementById("columnInfo").style.display = "none";
|
||||
renderResults();
|
||||
});
|
||||
@ -838,7 +976,8 @@ document.getElementById("addBinBtn").addEventListener("click", () => {
|
||||
const nextRow = (currentResults.at(-1)?.rowNum ?? 1) + 1;
|
||||
const res = validateId(v);
|
||||
const kgd = kgdLookup(res);
|
||||
currentResults.push({ rowNum: nextRow, res, kgd });
|
||||
const age = computeAge(res);
|
||||
currentResults.push({ rowNum: nextRow, res, kgd, age });
|
||||
renderResults();
|
||||
input.value = "";
|
||||
input.focus();
|
||||
@ -848,37 +987,147 @@ document.getElementById("manualBin").addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter") document.getElementById("addBinBtn").click();
|
||||
});
|
||||
|
||||
// =============================================================
|
||||
// Экспорт в .xlsx с подсветкой ячеек
|
||||
// =============================================================
|
||||
const XLSX_FILL = {
|
||||
ok: { fill: { patternType: "solid", fgColor: { rgb: "FFDCFCE7" } } },
|
||||
warn: { fill: { patternType: "solid", fgColor: { rgb: "FFFEF3C7" } } },
|
||||
err: { fill: { patternType: "solid", fgColor: { rgb: "FFFEE2E2" } } },
|
||||
head: {
|
||||
fill: { patternType: "solid", fgColor: { rgb: "FFE2E8F0" } },
|
||||
font: { bold: true },
|
||||
},
|
||||
};
|
||||
const XLSX_BORDER = {
|
||||
border: {
|
||||
top: { style: "thin", color: { rgb: "FFCBD5E1" } },
|
||||
bottom: { style: "thin", color: { rgb: "FFCBD5E1" } },
|
||||
left: { style: "thin", color: { rgb: "FFCBD5E1" } },
|
||||
right: { style: "thin", color: { rgb: "FFCBD5E1" } },
|
||||
}
|
||||
};
|
||||
|
||||
function fillForRow(res, kgd, age) {
|
||||
if (!res.is_valid) return XLSX_FILL.err;
|
||||
const kgdProblem = res.kind === "БИН" && kgd && (
|
||||
!kgd.in_registry || kgd.status !== "Действующий"
|
||||
);
|
||||
if (kgdProblem) return XLSX_FILL.warn;
|
||||
if (age && age.risk) return XLSX_FILL.warn;
|
||||
return XLSX_FILL.ok;
|
||||
}
|
||||
|
||||
const EXTRA_COLS = [
|
||||
{ name: "Тип", get: r => r.res.kind ?? "" },
|
||||
{ name: "ФЛК", get: r => r.res.is_valid ? "OK" : (r.res.error_code ?? "ERROR") },
|
||||
{ name: "Подтип", get: r => r.res.subtype ?? "" },
|
||||
{ name: "Дата", get: r => r.res.date_str ?? "" },
|
||||
{ name: "Возраст", get: r => r.age?.label ?? "" },
|
||||
{ name: "КГД статус", get: r => r.res.kind === "БИН" && r.res.is_valid && r.kgd
|
||||
? (r.kgd.in_registry ? r.kgd.status : "Не найден")
|
||||
: "" },
|
||||
{ name: "КГД наименование", get: r => r.res.kind === "БИН" && r.res.is_valid && r.kgd && r.kgd.name
|
||||
? r.kgd.name : "" },
|
||||
{ name: "Комментарий", get: r => r.res.error_msg
|
||||
?? (r.age?.risk ? AGE_RISK_TEXT[r.age.risk] ?? "" : "")
|
||||
?? "" },
|
||||
];
|
||||
|
||||
function setCell(sheet, r, c, value, style) {
|
||||
const ref = XLSX.utils.encode_cell({ r, c });
|
||||
const v = value == null ? "" : value;
|
||||
// Numeric vs string
|
||||
const isNum = typeof v === "number" && Number.isFinite(v);
|
||||
sheet[ref] = { t: isNum ? "n" : "s", v, w: isNum ? undefined : String(v) };
|
||||
if (style) sheet[ref].s = { ...style, ...XLSX_BORDER };
|
||||
}
|
||||
|
||||
document.getElementById("exportBtn").addEventListener("click", () => {
|
||||
if (currentResults.length === 0) return;
|
||||
const header = [
|
||||
"№","Исходное значение","Нормализованный","Тип","ФЛК","Код ошибки",
|
||||
"Подтип","Дата","КГД статус","КГД наименование","Комментарий"
|
||||
];
|
||||
const rows = currentResults.map(({ rowNum, res, kgd }) => [
|
||||
rowNum,
|
||||
res.original,
|
||||
res.normalized,
|
||||
res.kind ?? "",
|
||||
res.is_valid ? "OK" : "ERROR",
|
||||
res.error_code ?? "",
|
||||
res.subtype ?? "",
|
||||
res.date_str ?? "",
|
||||
res.kind === "БИН" && res.is_valid && kgd ? (kgd.in_registry ? kgd.status : "Не найден") : "",
|
||||
res.kind === "БИН" && res.is_valid && kgd && kgd.name ? kgd.name : "",
|
||||
res.error_msg ?? "",
|
||||
]);
|
||||
const csv = [header, ...rows]
|
||||
.map(r => r.map(c => {
|
||||
const s = String(c ?? "");
|
||||
return /[",;\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||
}).join(";"))
|
||||
.join("\r\n");
|
||||
const blob = new Blob(["" + csv], { type: "text/csv;charset=utf-8;" });
|
||||
const a = document.createElement("a");
|
||||
a.href = URL.createObjectURL(blob);
|
||||
a.download = `bin_iin_validation_${new Date().toISOString().slice(0,10)}.csv`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(a.href);
|
||||
|
||||
let wb, sheet, binCol, hasHeaderRow, headerRowIdx, sheetName;
|
||||
|
||||
if (originalBuffer && lastColumnInfo) {
|
||||
// Перечитываем исходный файл, чтобы сохранить расположение строк
|
||||
wb = XLSX.read(originalBuffer, { type: "array" });
|
||||
sheetName = wb.SheetNames[0];
|
||||
sheet = wb.Sheets[sheetName];
|
||||
binCol = lastColumnInfo.colIdx;
|
||||
hasHeaderRow = lastColumnInfo.hasHeader;
|
||||
headerRowIdx = hasHeaderRow ? 0 : null;
|
||||
} else {
|
||||
// Демо-режим — создаём свежий лист
|
||||
wb = XLSX.utils.book_new();
|
||||
const aoa = [["№", "БИН / ИИН"]];
|
||||
currentResults.forEach((r, i) => aoa.push([i + 1, r.res.original]));
|
||||
sheet = XLSX.utils.aoa_to_sheet(aoa);
|
||||
sheetName = "Результаты";
|
||||
XLSX.utils.book_append_sheet(wb, sheet, sheetName);
|
||||
binCol = 1; // столбец B
|
||||
hasHeaderRow = true;
|
||||
headerRowIdx = 0;
|
||||
}
|
||||
|
||||
const range = XLSX.utils.decode_range(sheet["!ref"] || "A1");
|
||||
// Доп. столбцы: пропускаем одну пустую колонку
|
||||
const extraStart = range.e.c + 2;
|
||||
|
||||
// Заголовки доп. столбцов (если есть строка заголовка)
|
||||
if (headerRowIdx !== null) {
|
||||
EXTRA_COLS.forEach((col, i) => {
|
||||
setCell(sheet, headerRowIdx, extraStart + i, col.name, XLSX_FILL.head);
|
||||
});
|
||||
}
|
||||
|
||||
let maxRow = range.e.r;
|
||||
|
||||
// Для демо-режима свой mapping rowNum→строка в xlsx
|
||||
currentResults.forEach((item, i) => {
|
||||
const { rowNum, res, kgd, age } = item;
|
||||
const rowIdx = originalBuffer ? rowNum - 1 : (i + 1); // demo: i+1 (header в 0)
|
||||
|
||||
// 1) Подсветка ячейки с БИН/ИИН
|
||||
const fill = fillForRow(res, kgd, age);
|
||||
const binRef = XLSX.utils.encode_cell({ r: rowIdx, c: binCol });
|
||||
if (!sheet[binRef]) sheet[binRef] = { t: "s", v: res.original, w: res.original };
|
||||
sheet[binRef].s = { ...fill, ...XLSX_BORDER };
|
||||
|
||||
// 2) Доп. столбцы — каждое значение в своей ячейке, общий цвет строки
|
||||
EXTRA_COLS.forEach((col, j) => {
|
||||
const val = col.get(item);
|
||||
setCell(sheet, rowIdx, extraStart + j, val, fill);
|
||||
});
|
||||
|
||||
if (rowIdx > maxRow) maxRow = rowIdx;
|
||||
});
|
||||
|
||||
// Обновляем диапазон листа
|
||||
sheet["!ref"] = XLSX.utils.encode_range({
|
||||
s: { r: 0, c: 0 },
|
||||
e: { r: maxRow, c: extraStart + EXTRA_COLS.length - 1 }
|
||||
});
|
||||
|
||||
// Ширины столбцов: минимум для исходных + удобные для extras
|
||||
sheet["!cols"] = sheet["!cols"] || [];
|
||||
for (let c = 0; c <= extraStart + EXTRA_COLS.length - 1; c++) {
|
||||
if (!sheet["!cols"][c]) sheet["!cols"][c] = { wch: 14 };
|
||||
}
|
||||
const extraWidths = [8, 22, 22, 14, 18, 16, 30, 50];
|
||||
EXTRA_COLS.forEach((_, i) => {
|
||||
sheet["!cols"][extraStart + i] = { wch: extraWidths[i] || 18 };
|
||||
});
|
||||
|
||||
// Имя файла: <исходное>_проверено.xlsx или validation_<date>.xlsx
|
||||
const stamp = new Date().toISOString().slice(0, 10);
|
||||
const baseName = (originalFileName || "")
|
||||
.replace(/\.(xlsx|xls|csv)$/i, "")
|
||||
.trim();
|
||||
const fileName = baseName
|
||||
? `${baseName}_проверено_${stamp}.xlsx`
|
||||
: `bin_iin_проверка_${stamp}.xlsx`;
|
||||
|
||||
XLSX.writeFile(wb, fileName);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user