feat: add entrance+floor fields, fix geocoder false-positive on locality match

- clients.js: new client + edit client forms get подъезд and этаж fields (optional)
- clients.js: address string includes подъезд/этаж when filled
- clients.js: splitAddress parses подъезд/этаж from stored address string
- clients.js: geocoder now checks result.kind — only shows ✓ for house/street precision;
  locality/province match shows warning "улица не найдена" without saving coords
- podbor.css: addr-grid grows to 3 rows (город/улица, дом/кв, подъезд/этаж)
- index.html: cache bump → v=20260514j

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
wasrusgen 2026-05-15 21:38:13 +03:00
parent 44799362c1
commit bedef30465
3 changed files with 87 additions and 44 deletions

View File

@ -76,6 +76,14 @@ const Clients = (function () {
<span class="field-sublabel">Кв./офис</span> <span class="field-sublabel">Кв./офис</span>
<input type="text" id="ad_apt" placeholder="12" inputmode="numeric"> <input type="text" id="ad_apt" placeholder="12" inputmode="numeric">
</label> </label>
<label class="field addr-entrance">
<span class="field-sublabel">Подъезд</span>
<input type="text" id="ad_entrance" placeholder="1" inputmode="numeric">
</label>
<label class="field addr-floor">
<span class="field-sublabel">Этаж</span>
<input type="text" id="ad_floor" placeholder="3" inputmode="numeric">
</label>
</div> </div>
<span class="field-error" id="errAddr"></span> <span class="field-error" id="errAddr"></span>
<div class="geo-status" id="geoStatus"></div> <div class="geo-status" id="geoStatus"></div>
@ -133,17 +141,24 @@ const Clients = (function () {
}); });
const name = (form.querySelector("#fn").value || "").trim(); const name = (form.querySelector("#fn").value || "").trim();
const phoneRaw = (form.querySelector("#ph").value || "").trim(); const phoneRaw = (form.querySelector("#ph").value || "").trim();
const adCity = (form.querySelector("#ad_city").value || "").trim(); const adCity = (form.querySelector("#ad_city").value || "").trim();
const adStreet = (form.querySelector("#ad_street").value|| "").trim(); const adStreet = (form.querySelector("#ad_street").value || "").trim();
const adHouse = (form.querySelector("#ad_house").value || "").trim(); const adHouse = (form.querySelector("#ad_house").value || "").trim();
const adApt = (form.querySelector("#ad_apt").value || "").trim(); const adApt = (form.querySelector("#ad_apt").value || "").trim();
const adEntrance = (form.querySelector("#ad_entrance").value|| "").trim();
const adFloor = (form.querySelector("#ad_floor").value || "").trim();
const note = (form.querySelector("#nt").value || "").trim(); const note = (form.querySelector("#nt").value || "").trim();
const contract_no = (form.querySelector("#cn").value || "").trim(); const contract_no = (form.querySelector("#cn").value || "").trim();
const contract_date = (form.querySelector("#cd").value || "").trim(); const contract_date = (form.querySelector("#cd").value || "").trim();
// Собираем адрес из полей // Собираем адрес из полей
const address = [adCity, adStreet, adHouse ? "д. " + adHouse : "", adApt ? "кв. " + adApt : ""] const address = [
.filter(Boolean).join(", "); adCity, adStreet,
adHouse ? "д. " + adHouse : "",
adApt ? "кв. " + adApt : "",
adEntrance ? "подъезд " + adEntrance : "",
adFloor ? "этаж " + adFloor : "",
].filter(Boolean).join(", ");
// Валидация // Валидация
if (!name || name.length < 2) { if (!name || name.length < 2) {
@ -179,9 +194,15 @@ const Clients = (function () {
}); });
const geoData = await geoRes.json(); const geoData = await geoRes.json();
if (geoData.ok && geoData.result) { if (geoData.ok && geoData.result) {
gps_lat = geoData.result.lat; const kind = (geoData.result.kind || "").toLowerCase();
gps_lng = geoData.result.lng; const precise = ["house", "street", "entrance", "building"].includes(kind);
geoEl.innerHTML = `<span class="geo-ok">✓ ${escHtml(geoData.result.formatted || address)}</span>`; if (precise) {
gps_lat = geoData.result.lat;
gps_lng = geoData.result.lng;
geoEl.innerHTML = `<span class="geo-ok">✓ ${escHtml(geoData.result.formatted || address)}</span>`;
} else {
geoEl.innerHTML = `<span class="geo-warn">⚠ Улица не найдена — геокодер вернул «${escHtml(geoData.result.formatted || "")}». Проверьте написание улицы. Сохраняем без координат.</span>`;
}
} else { } else {
geoEl.innerHTML = `<span class="geo-warn">⚠ Адрес не найден в геокодере — проверьте написание. Сохраняем без координат.</span>`; geoEl.innerHTML = `<span class="geo-warn">⚠ Адрес не найден в геокодере — проверьте написание. Сохраняем без координат.</span>`;
} }
@ -240,23 +261,22 @@ const Clients = (function () {
}); });
} }
// Разбирает сохранённый адрес «Город, Улица, д. NN, кв. MM» обратно в поля. // Разбирает сохранённый адрес «Город, Улица, д. NN, кв. MM, подъезд P, этаж F» обратно в поля.
function splitAddress(combined) { function splitAddress(combined) {
if (!combined) return { city: "Санкт-Петербург", street: "", house: "", apt: "" }; if (!combined) return { city: "Санкт-Петербург", street: "", house: "", apt: "", entrance: "", floor: "" };
let s = combined.trim(); let s = combined.trim();
let apt = ""; const grab = (re) => { const m = s.match(re); if (m) { s = s.replace(m[0], ""); return m[1]; } return ""; };
const aptMatch = s.match(/,\s*кв\.?\s*([^\s,]+)/i); const floor = grab(/,\s*этаж\s+([^\s,]+)/i);
if (aptMatch) { apt = aptMatch[1]; s = s.replace(aptMatch[0], ""); } const entrance = grab(/,\s*подъезд\s+([^\s,]+)/i);
let house = ""; const apt = grab(/,\s*кв\.?\s*([^\s,]+)/i);
const houseMatch = s.match(/,\s*д\.?\s*([^\s,]+)/i); const house = grab(/,\s*д\.?\s*([^\s,]+)/i);
if (houseMatch) { house = houseMatch[1]; s = s.replace(houseMatch[0], ""); }
s = s.replace(/,$/, "").trim(); s = s.replace(/,$/, "").trim();
const parts = s.split(",").map(p => p.trim()).filter(Boolean); const parts = s.split(",").map(p => p.trim()).filter(Boolean);
let city = "", street = ""; let city = "", street = "";
if (parts.length >= 2) { city = parts[0]; street = parts.slice(1).join(", "); } if (parts.length >= 2) { city = parts[0]; street = parts.slice(1).join(", "); }
else if (parts.length === 1) { city = parts[0]; } else if (parts.length === 1) { city = parts[0]; }
if (!city) city = "Санкт-Петербург"; if (!city) city = "Санкт-Петербург";
return { city, street, house, apt }; return { city, street, house, apt, entrance, floor };
} }
function normalizePhone(raw) { function normalizePhone(raw) {
@ -751,6 +771,14 @@ const Clients = (function () {
<span class="field-sublabel">Кв./офис</span> <span class="field-sublabel">Кв./офис</span>
<input type="text" id="ed_apt" value="${escAttr(addrParts.apt)}" placeholder="12" inputmode="numeric"> <input type="text" id="ed_apt" value="${escAttr(addrParts.apt)}" placeholder="12" inputmode="numeric">
</label> </label>
<label class="field addr-entrance">
<span class="field-sublabel">Подъезд</span>
<input type="text" id="ed_entrance" value="${escAttr(addrParts.entrance)}" placeholder="1" inputmode="numeric">
</label>
<label class="field addr-floor">
<span class="field-sublabel">Этаж</span>
<input type="text" id="ed_floor" value="${escAttr(addrParts.floor)}" placeholder="3" inputmode="numeric">
</label>
</div> </div>
<span class="field-error" id="ed_errAddr"></span> <span class="field-error" id="ed_errAddr"></span>
<div class="geo-status" id="ed_geoStatus"></div> <div class="geo-status" id="ed_geoStatus"></div>
@ -781,12 +809,14 @@ const Clients = (function () {
}); });
form.querySelector("#ed_save").addEventListener("click", async () => { form.querySelector("#ed_save").addEventListener("click", async () => {
const fn = form.querySelector("#ed_fn").value.trim(); const fn = form.querySelector("#ed_fn").value.trim();
const ph = form.querySelector("#ed_ph").value.trim(); const ph = form.querySelector("#ed_ph").value.trim();
const edCity = (form.querySelector("#ed_city").value || "").trim(); const edCity = (form.querySelector("#ed_city").value || "").trim();
const edStreet = (form.querySelector("#ed_street").value || "").trim(); const edStreet = (form.querySelector("#ed_street").value || "").trim();
const edHouse = (form.querySelector("#ed_house").value || "").trim(); const edHouse = (form.querySelector("#ed_house").value || "").trim();
const edApt = (form.querySelector("#ed_apt").value || "").trim(); const edApt = (form.querySelector("#ed_apt").value || "").trim();
const edEntrance = (form.querySelector("#ed_entrance").value || "").trim();
const edFloor = (form.querySelector("#ed_floor").value || "").trim();
const cno = form.querySelector("#ed_cno").value.trim(); const cno = form.querySelector("#ed_cno").value.trim();
const cdate = form.querySelector("#ed_cdate").value.trim(); const cdate = form.querySelector("#ed_cdate").value.trim();
const errName = form.querySelector("#ed_errName"); const errName = form.querySelector("#ed_errName");
@ -809,8 +839,13 @@ const Clients = (function () {
return; return;
} }
const address = [edCity, edStreet, edHouse ? "д. " + edHouse : "", edApt ? "кв. " + edApt : ""] const address = [
.filter(Boolean).join(", "); edCity, edStreet,
edHouse ? "д. " + edHouse : "",
edApt ? "кв. " + edApt : "",
edEntrance ? "подъезд " + edEntrance : "",
edFloor ? "этаж " + edFloor : "",
].filter(Boolean).join(", ");
const btn = form.querySelector("#ed_save"); const btn = form.querySelector("#ed_save");
btn.disabled = true; btn.textContent = "Проверяем адрес…"; btn.disabled = true; btn.textContent = "Проверяем адрес…";
@ -830,9 +865,15 @@ const Clients = (function () {
}); });
const geoData = await geoRes.json(); const geoData = await geoRes.json();
if (geoData.ok && geoData.result) { if (geoData.ok && geoData.result) {
gps_lat = geoData.result.lat; const kind = (geoData.result.kind || "").toLowerCase();
gps_lng = geoData.result.lng; const precise = ["house", "street", "entrance", "building"].includes(kind);
geoEl.innerHTML = `<span class="geo-ok">✓ ${escHtml(geoData.result.formatted || address)}</span>`; if (precise) {
gps_lat = geoData.result.lat;
gps_lng = geoData.result.lng;
geoEl.innerHTML = `<span class="geo-ok">✓ ${escHtml(geoData.result.formatted || address)}</span>`;
} else {
geoEl.innerHTML = `<span class="geo-warn">⚠ Улица не найдена — геокодер вернул «${escHtml(geoData.result.formatted || "")}». Проверьте написание улицы. Сохраняем без координат.</span>`;
}
} else { } else {
geoEl.innerHTML = `<span class="geo-warn">⚠ Адрес не найден — сохраняем без координат.</span>`; geoEl.innerHTML = `<span class="geo-warn">⚠ Адрес не найден — сохраняем без координат.</span>`;
} }

View File

@ -3328,8 +3328,10 @@
margin-top: 6px; margin-top: 6px;
} }
.addr-grid .field { margin: 0; } .addr-grid .field { margin: 0; }
.addr-house { grid-column: 1; } .addr-house { grid-column: 1; }
.addr-apt { grid-column: 2; } .addr-apt { grid-column: 2; }
.addr-entrance { grid-column: 1; }
.addr-floor { grid-column: 2; }
.field-sublabel { .field-sublabel {
display: block; display: block;
font-size: 11px; font-size: 11px;

View File

@ -12,14 +12,14 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap"> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Geist:wght@400;500;600&family=Newsreader:ital,wght@0,400..600;1,400..600&family=Instrument+Serif:ital@0;1&family=JetBrains+Mono:wght@400;500&family=Cormorant+Garamond:ital,wght@1,400;1,500;1,600&family=Caveat:wght@500;700&display=swap">
<script src="https://telegram.org/js/telegram-web-app.js"></script> <script src="https://telegram.org/js/telegram-web-app.js"></script>
<link rel="stylesheet" href="assets/styles.css?v=20260514i"> <link rel="stylesheet" href="assets/styles.css?v=20260514j">
<link rel="stylesheet" href="assets/podbor.css?v=20260514i"> <link rel="stylesheet" href="assets/podbor.css?v=20260514j">
</head> </head>
<body> <body>
<!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск --> <!-- Splash — лого @wasrusgen1 + опилки (16) + вращающийся диск -->
<div class="loader splash" id="splash"> <div class="loader splash" id="splash">
<div class="brand-logo-wrap"> <div class="brand-logo-wrap">
<img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514i" alt="@wasrusgen1"> <img class="brand-logo" src="assets/wasrusgen-logo.svg?v=20260514j" alt="@wasrusgen1">
<div class="splash-dust" aria-hidden="true"> <div class="splash-dust" aria-hidden="true">
<span class="dust d1"></span> <span class="dust d2"></span> <span class="dust d1"></span> <span class="dust d2"></span>
<span class="dust d3"></span> <span class="dust d4"></span> <span class="dust d3"></span> <span class="dust d4"></span>
@ -35,15 +35,15 @@
<div class="brand-tagline-gold">CRM</div> <div class="brand-tagline-gold">CRM</div>
</div> </div>
<main id="app"></main> <main id="app"></main>
<script src="assets/icons.js?v=20260514i"></script> <script src="assets/icons.js?v=20260514j"></script>
<script src="assets/podbor.config.js?v=20260514i"></script> <script src="assets/podbor.config.js?v=20260514j"></script>
<script src="assets/podbor.picts.js?v=20260514i"></script> <script src="assets/podbor.picts.js?v=20260514j"></script>
<script src="assets/podbor.js?v=20260514i"></script> <script src="assets/podbor.js?v=20260514j"></script>
<script src="assets/clients.js?v=20260514i"></script> <script src="assets/clients.js?v=20260514j"></script>
<script src="assets/zamer-picts.js?v=20260514i"></script> <script src="assets/zamer-picts.js?v=20260514j"></script>
<script src="assets/measurements.js?v=20260514i"></script> <script src="assets/measurements.js?v=20260514j"></script>
<script src="assets/request.js?v=20260514i"></script> <script src="assets/request.js?v=20260514j"></script>
<script src="assets/assembly.js?v=20260514i"></script> <script src="assets/assembly.js?v=20260514j"></script>
<script src="assets/app.js?v=20260514i"></script> <script src="assets/app.js?v=20260514j"></script>
</body> </body>
</html> </html>