fix: city regex requires mandatory dot+space — no more Гор→ор, Годная→одная

This commit is contained in:
root 2026-05-28 11:05:38 +00:00
parent 9e992d8280
commit 50f504c2ff
5 changed files with 39 additions and 26 deletions

View File

@ -1,3 +1,4 @@
import { CRIMEAN_CITIES } from "../../constants/cities.js";
import React from "react"; import React from "react";
import { import {
getOrderGroupDeliveryHalfDay, getOrderGroupDeliveryHalfDay,
@ -29,27 +30,28 @@ const extractCity = (address) => {
const trimmed = address.trim(); const trimmed = address.trim();
if (!trimmed) return null; if (!trimmed) return null;
const cityMatch = trimmed.match(/(?:г\.?\s*|г\s+)([А-ЯЁA-Z][а-яёa-zA-Z\s\-]+?)(?:\s*[,;.]|$)/i); const cityMatch = trimmed.match(/(?:г\.\s+|гор\.\s+|пос\.\s+|с\.\s+|дер\.\s+|пгт\.\s+|город\s+|село\s+|г\s+)([А-ЯЁA-Z][а-яёa-zA-Z\s\-]+?)(?:\s*[,;.]|\s|$)/i);
if (cityMatch) { if (cityMatch) {
return cityMatch[1].trim(); return cityMatch[1].trim();
} }
const knownCities = [
"Севастополь", "Ялта", "Симферополь", "Феодосия", "Евпатория", for (const city of CRIMEAN_CITIES) {
"Керчь", "Алушта", "Бахчисарай", "Судак", "Инкерман",
"Джанкой", "Красногвардейское", "Раздольное", "Черноморское",
];
for (const city of knownCities) {
if (trimmed.toLowerCase().includes(city.toLowerCase())) { if (trimmed.toLowerCase().includes(city.toLowerCase())) {
return city; return city;
} }
} }
const firstSegment = trimmed.split(/[,;]/)[0].trim(); // Бахчисарайский р-н Бахчисарай
if (firstSegment.length > 2 && firstSegment.length < 30 && !/^\d/.test(firstSegment)) { const district = trimmed.match(/([А-ЯЁа-яё]+)ский\s*(?:р-н|район)/i);
return firstSegment; if (district) {
const base = district[1];
for (const city of CRIMEAN_CITIES) {
if (city.toLowerCase().startsWith(base.toLowerCase())) return city;
}
} }
// no match null (caller falls back to Севастополь)
return null; return null;
}; };

View File

@ -36,6 +36,7 @@ export const OrdersTable = ({
filters, filters,
setFilters, setFilters,
statusOptions, statusOptions,
cities = [],
}) => { }) => {
return ( return (
<Panel className="p-0"> <Panel className="p-0">
@ -51,7 +52,7 @@ export const OrdersTable = ({
</div> </div>
{filters && setFilters ? ( {filters && setFilters ? (
<OrderFilters filters={filters} setFilters={setFilters} statusOptions={statusOptions} /> <OrderFilters filters={filters} setFilters={setFilters} statusOptions={statusOptions} cities={cities} />
) : null} ) : null}
</div> </div>

12
src/constants/cities.js Normal file
View File

@ -0,0 +1,12 @@
export const CRIMEAN_CITIES = [
"Севастополь","Ялта","Алушта","Евпатория","Саки","Феодосия",
"Керчь","Симферополь","Бахчисарай","Судак","Белогорск",
"Красноперекопск","Джанкой","Щёлкино","Гаспра","Гурзуф",
"Кореиз","Ливадия","Массандра","Ореанда","Симеиз",
"Форос","Партенит","Мисхор","Отрадное","Санаторное",
"Васильевка","Куйбышево","Инкерман","Балаклава",
"Утёс","Резниково","Заветное","Хмельницкое","Мирновка",
"Новосёловка","Гвардейское","Красногвардейское",
"Раздольное","Черноморское","Ленино","Советский",
"Нижнегорский","Первомайское","Октябрьское",
];

View File

@ -83,6 +83,14 @@ export const DashboardPage = () => {
loadError, loadError,
} = useOrderGroups(); } = useOrderGroups();
const cities = React.useMemo(() => {
const set = new Set();
for (const g of allOrderGroups) {
if (g.city) set.add(g.city);
}
return [...set].sort();
}, [allOrderGroups]);
const openGroupPage = React.useCallback((groupId) => { const openGroupPage = React.useCallback((groupId) => {
navigate("/dashboard/group/" + groupId); navigate("/dashboard/group/" + groupId);
}, [navigate]); }, [navigate]);
@ -132,7 +140,7 @@ export const DashboardPage = () => {
if (activeSection === "orders") { if (activeSection === "orders") {
return ( return (
<div className="space-y-6 xl:space-y-8"> <div className="space-y-6 xl:space-y-8">
<OrdersTable orderGroups={filteredOrderGroups} selectedOrderGroupId={selectedOrderGroupId} onOpenOrder={openGroupPage} filters={filters} setFilters={setFilters} statusOptions={statusOptions} /> <OrdersTable orderGroups={filteredOrderGroups} selectedOrderGroupId={selectedOrderGroupId} onOpenOrder={openGroupPage} filters={filters} setFilters={setFilters} statusOptions={statusOptions} cities={cities} />
</div> </div>
); );
} }
@ -144,7 +152,7 @@ export const DashboardPage = () => {
} }
return ( return (
<div className="space-y-6 xl:space-y-8"> <div className="space-y-6 xl:space-y-8">
<OrdersTable orderGroups={filteredOrderGroups} selectedOrderGroupId={selectedOrderGroupId} onOpenOrder={openGroupPage} filters={filters} setFilters={setFilters} statusOptions={statusOptions} /> <OrdersTable orderGroups={filteredOrderGroups} selectedOrderGroupId={selectedOrderGroupId} onOpenOrder={openGroupPage} filters={filters} setFilters={setFilters} statusOptions={statusOptions} cities={cities} />
</div> </div>
); );
}; };

View File

@ -1,4 +1,5 @@
import { safeSupabaseCall } from "../safeSupabaseCall"; import { safeSupabaseCall } from "../safeSupabaseCall";
import { CRIMEAN_CITIES } from "../../constants/cities.js";
import { logAction } from "./actionLogService"; import { logAction } from "./actionLogService";
import logger from "../../utils/logger"; import logger from "../../utils/logger";
import { hasSupabaseConfig, supabase } from "../../supabaseClient"; import { hasSupabaseConfig, supabase } from "../../supabaseClient";
@ -90,22 +91,11 @@ export const mapOrderGroupRowToDeliveryGroup = (row) => {
const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders); const deliveryAddress = normalizeText(row.delivery_address) || extractAddressFromSourceOrders(row.source_orders);
const customerAddress = normalizeText(row.customer_address) || ""; const customerAddress = normalizeText(row.customer_address) || "";
const CRIMEAN_CITIES = [
"Севастополь","Ялта","Алушта","Евпатория","Саки","Феодосия",
"Керчь","Симферополь","Бахчисарай","Судак","Белогорск",
"Красноперекопск","Джанкой","Щёлкино","Гаспра","Гурзуф",
"Кореиз","Ливадия","Массандра","Ореанда","Симеиз",
"Форос","Партенит","Мисхор","Отрадное","Санаторное",
"Васильевка","Куйбышево","Инкерман","Балаклава",
"Утёс","Резниково","Заветное","Хмельницкое","Мирновка",
"Новосёловка","Гвардейское","Красногвардейское",
"Раздольное","Черноморское","Ленино","Советский",
"Нижнегорский","Первомайское","Октябрьское",
];
const extractCity = (addr) => { const extractCity = (addr) => {
if (!addr) return ""; if (!addr) return "";
// 1) explicit marker: г. Ялта, пгт. Куйбышево, etc. // 1) explicit marker: г. Ялта, пгт. Куйбышево, etc.
const m = addr.match(/(?:г\.|гор\.?|пос\.|с\.|село|дер\.|пгт|город)\s*([А-ЯЁа-яёA-Za-z\-\s]+?)(?:[,\\s]|$)/i); const m = addr.match(/(?:г\.\s|гор\.\s|пос\.\s|с\.\s|дер\.\s|пгт\.\s|город\s|село\s|г\s)\s*([А-ЯЁа-яёA-Za-z\-\s]+?)(?:[,\\s]|$)/i);
if (m) return m[1].trim(); if (m) return m[1].trim();
// 2) known city name anywhere in address (case-insensitive) // 2) known city name anywhere in address (case-insensitive)
const lower = addr.toLowerCase(); const lower = addr.toLowerCase();