Virtonomica: Вывоз на склад

Массовый вывоз товара из магазина на склады в 1 клик.

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
// ==UserScript==
// @name          Virtonomica: Вывоз на склад
// @namespace     virtonomica
// @author        mr_Sumkin
// @description   Массовый вывоз товара из магазина на склады в 1 клик.
// @version       1.04
// @include       http*://virtonomic*.*/*/main/unit/view/*/sale
// @include       http*://virtonomic*.*/*/main/unit/view/*/trading_hall
// @require       https://code.jquery.com/jquery-1.11.1.min.js
// @require       https://cdnjs.cloudflare.com/ajax/libs/lz-string/1.4.4/lz-string.js
// ==/UserScript==
$ = jQuery = jQuery.noConflict(true);
let $xioDebug = true;
let Realm = getRealmOrError();
let GameDate = parseGameDate(document);
let Export2WareStoreKeyCode = "e2w";
let ProdCatStoreKeyCode = "prct"; // список продуктов с категориями. сделал неким отдельным ключиком, вдруг будет скрипт читающий эту же табличку
let TMStoreKeyCode = "prtm"; // список ТМ продуктов
let EnablePriceMgmnt = true; // если выключить то кнопки изменения цен исчезнут
let PriceStep = 2; // шаг изменения цены в %
let EnableExport2w = true; // если выключить то функции экспорт ВСЕ перестанут работать
// упрощаем себе жисть, подставляем имя скрипта всегда в сообщении
function log(msg, ...args) {
    msg = "export2ware: " + msg;
    logDebug(msg, ...args);
}
function run_async() {
    return __awaiter(this, void 0, void 0, function* () {
        let Url_rx = {
            // для юнита
            unit_sale: /\/[a-z]+\/(?:main|window)\/unit\/view\/\d+\/sale\/?/i,
            unit_trade_hall: /\/[a-z]+\/(?:main|window)\/unit\/view\/\d+\/trading_hall\/?/i,
        };
        let $html = $(document);
        // определяем где мы находимся. для трейдхолла может не быть вообще товара, тады нет таблицы.
        let onTradehall = Url_rx.unit_trade_hall.test(document.location.pathname) && $html.find("table.grid").length > 0;
        let onWareSale = Url_rx.unit_sale.test(document.location.pathname) && parseUnitType($html) == UnitTypes.warehouse;
        if (onTradehall && EnableExport2w)
            yield tradehallExport_async();
        if (onTradehall && EnablePriceMgmnt)
            tradehallPrice();
        if (onWareSale)
            wareSale();
        function tradehallExport_async() {
            return __awaiter(this, void 0, void 0, function* () {
                //  задаем стили для выделения
                //$("<style>")
                //    .prop("type", "text/css")
                //    .html(`.e2wSelected { background-color: rgb(255, 210, 170) !important; }`)
                //    .appendTo(document.head);
                let selClassName = "selected"; // класс которым будем выделять строки доступные для экспорта
                // собираем данные с хранилища
                let exportWares = restoreWare();
                let tm = yield getTMProducts_async();
                let prodCatDict = yield getProdWithCategories_async();
                let getCategory = (pid) => nullCheck(prodCatDict[pid]).product_category_name;
                // для каждого товара удалим штатные события дабы свои работали нормально
                // и удалим обработчик на общую галку. она будет работать иначе
                let $rows = closestByTagName($html.find("table.grid a.popup"), "tr");
                $rows.removeAttr("onmouseover").removeAttr("onmouseout").removeClass("selected");
                let $globalChbx = oneOrError($html, "#allProduct").removeAttr("onclick");
                ;
                let $form = $html.find("form");
                let [, thItems] = parseUnitTradeHall(document, document.location.pathname);
                let dataCache = [];
                for (let thItem of thItems) {
                    let $r = oneOrError($form, `input[name='${thItem.name}']`).closest("tr");
                    let $cbx = oneOrError($r, "input:checkbox");
                    let brand = tm[thItem.product.img];
                    let brand_id = brand == null ? 0 : brand.brand_id; // простой товар будет иметь 0. кривые бренды пойдут с null
                    dataCache.push({
                        available: thItem.stock.available,
                        sold: thItem.stock.sold,
                        category: getCategory(thItem.product.id),
                        pid: thItem.product.id,
                        row: $r,
                        cbx: $cbx,
                        brand_id: brand_id,
                        prod_name: thItem.product.name
                    });
                }
                // формируем словарь Отдел => pid[] для отрисовки селекта отделов. Пустые товары выкидываем.
                let catsDict = {};
                for (let item of dataCache) {
                    if (item.available <= 0)
                        continue;
                    if (catsDict[item.category] == null)
                        catsDict[item.category] = [];
                    catsDict[item.category].push(item.pid);
                }
                // оставляем только склады на которые можно что то вывезти
                exportWares = filterWares(exportWares, dataCache.map((v, i, a) => v.pid));
                // для вывода в селект, формируем для каждого склада список отделов для которых склад может быть использован
                // subid => {category => goodsCount}
                let wareCats = {};
                for (let subid in exportWares) {
                    let wp = exportWares[subid];
                    let cats = categories(wp.products, prodCatDict);
                    if (dictKeys(cats).length <= 0)
                        throw new Error(`что то пошло не так, список категорий для склада ${wp.name} пустой`);
                    wareCats[subid] = cats;
                }
                // размещение селектов
                //
                // cелект для списка отделов
                let $catSel = $(`<select id="exportCats"><option value="off"> ------ </option></select>`);
                for (let catName in catsDict) {
                    let cnt = catsDict[catName];
                    let opt = `<option value="${catName}">${catName} (${cnt.length})</option>`;
                    $catSel.append(opt);
                }
                // cелект со складами
                let $wareSel = $(`<select id="exportWares"><option value="off"> ------ </option></select>`);
                for (let key in exportWares) {
                    let subid = parseInt(key);
                    let wp = exportWares[subid];
                    let cats = wareCats[subid];
                    // из словаря категорий формируем массив строк для удобства
                    let catsStrArr = [];
                    for (let catName in cats)
                        catsStrArr.push(`${catName} (${cats[catName]})`);
                    // список всех категорий в тултип суем
                    let tooltipStr = `${wp.city} (${wp.spec})\n-----------\n${catsStrArr.join("\n")}`;
                    let opt = `<option value="${subid}" title="${tooltipStr}">${wp.name} (${wp.spec})</option>`;
                    $wareSel.append(opt);
                }
                // суем селекты и подравниваем ширину
                let $div = $(`<div style="margin:10px 0 0 10px; display:inline-block"></div>`).append($catSel, "<br>", $wareSel);
                $html.find("table.list").before($div);
                let ws = $div.find("select").map((i, el) => $(el).width()).get();
                $div.find("select").width(Math.max(100, ...ws)); // минимально 100 пикселей
                // выбор селекта отделов, автоматом формирует селект складов для данного отдела
                $catSel.on("change", function (event) {
                    //console.log("catSel.changed fired");
                    let $s = $(this);
                    let selectedCat = $s.val();
                    // все склады не содержащие нужные отделы, скроем из селекта
                    $wareSel.trigger("catChanged", selectedCat);
                });
                // обновился выбор категории, обновить надо список складов
                $wareSel.on("catChanged", function (event, selectedCat) {
                    //console.log("wareSe.catChanged fired");
                    let $s = $(this);
                    let val = $s.val(); // off || ware
                    let $options = $s.find("option");
                    // если сброс категории, то покажем все склады
                    if (selectedCat == "off") {
                        $options.show();
                        if (val != "off") {
                            $s.val("off");
                            $s.trigger("change");
                        }
                        return;
                    }
                    // скроем часть складов которые недоступны для категории
                    $options.each((i, e) => {
                        let $o = $(e);
                        if ($o.val() == "off")
                            return;
                        let subid = parseInt($o.val());
                        if (wareCats[subid][selectedCat] == null)
                            $o.hide();
                        else
                            $o.show();
                    });
                    if (val != "off") {
                        $s.val("off");
                        $s.trigger("change");
                    }
                });
                $wareSel.on("change", function (event) {
                    //console.log("wareSel.changed fired");
                    let $s = $(this);
                    let val = $s.val(); //off || subid
                    let selectedCat = $catSel.val();
                    if (val == "off") {
                        // убираем подсветку и разрешаем тыкать все галочки что есть
                        for (let obj of dataCache) {
                            obj.row.removeClass(selClassName);
                            obj.cbx.prop("checked", false).prop("disabled", false);
                        }
                        return;
                    }
                    else {
                        // подсвечивать будем только в соответствии с выбранным отделом, ну или все если отдел не выбран
                        let subid = parseInt(val);
                        let wp = exportWares[subid];
                        for (let obj of dataCache) {
                            let wareHasIt = wp.products.find((v, i) => obj.pid == v.id) != null;
                            let exportable = wareHasIt && obj.available > 0 && (selectedCat == "off" || selectedCat == obj.category);
                            if (exportable) {
                                obj.row.toggleClass(selClassName, true);
                                obj.cbx.prop("checked", true).prop("disabled", false);
                            }
                            else {
                                obj.row.removeClass(selClassName);
                                obj.cbx.prop("checked", false).prop("disabled", true);
                            }
                        }
                    }
                });
                // глобальный чекбокс надо заставить работать иначе, чтобы он учитывал наши ограничения
                //
                $globalChbx.on("click", function (event) {
                    let $chbx = $(this);
                    let checked = $chbx.prop("checked") == true;
                    let selectedCat = $catSel.val();
                    let selectedWare = $wareSel.val();
                    // если выбран отдел, то только в рамках отдела. А если выбран склад то только в рамках склада
                    if (checked) {
                        if (selectedCat == "off" && selectedWare == "off") {
                            // просто выделяем все продукты
                            for (let item of dataCache)
                                item.cbx.prop("checked", true);
                        }
                        else if (selectedCat != "off" && selectedWare == "off") {
                            // выделяем только выбранный отдел
                            let cache = dataCache.filter((v, i, a) => v.category == selectedCat);
                            for (let item of cache)
                                item.cbx.prop("checked", true);
                        }
                        else if (selectedWare != "off") {
                            // тут тупо полагаемся на класс строки, так как при выборе склада всегда будет выделение
                            let cache = dataCache.filter((v, i, a) => v.row.hasClass(selClassName));
                            for (let item of cache)
                                item.cbx.prop("checked", true);
                        }
                    }
                    else {
                        for (let item of dataCache)
                            item.cbx.prop("checked", false);
                    }
                });
                // Кнопка экспорта и сам экспорт
                //
                // вниз страницы суем кнопку экспорта
                let $expAllBtn = $(`<input type="button" id="doExport" class="button160 e2w"  data-mult="0" value="Вывезти всё">`);
                let $exp2Btn = $(`<input type="button" id="doExport2" class="button160 e2w" data-mult="2" value="Оставить 2*sold">`);
                let $exp3Btn = $(`<input type="button" id="doExport3" class="button160 e2w" data-mult="3" value="Оставить 3*sold">`);
                $html.find("table.buttonset_noborder input.button205").after($expAllBtn, $exp2Btn, $exp3Btn);
                $html.find("table.buttonset_noborder").on("click", "input.e2w", null, function (event) {
                    return __awaiter(this, void 0, void 0, function* () {
                        // если не выбрано в селекте и нет галочек то нечего экспортить. хрен
                        if ($wareSel.val() == "off") {
                            alert("Не выбран склад на который вывозить товары.\nЭкспорт невозможен.");
                            return;
                        }
                        let pids = dataCache.filter((v, i, a) => v.cbx.prop("checked") == true);
                        if (pids.length <= 0) {
                            alert("Не выбрано ни одного товара.\nЭкспорт невозможен.");
                            return;
                        }
                        // чето выбрано, надо таки экспортить
                        //console.log(pids.map((v, i, a) => prodCatDict[v].name));
                        // subid
                        let n = extractIntPositive(document.location.pathname);
                        if (n == null)
                            throw new Error(`на нашел subid юнита в ссылке ${document.location.pathname}`);
                        let subid = n[0];
                        let wareSubid = numberfyOrError($wareSel.val());
                        // теперь надо множитель дернуть для остатка
                        let mult = numberfyOrError($(this).data("mult"), -1);
                        for (let obj of pids) {
                            if (obj.brand_id == null)
                                alert(`Товар ${obj.prod_name} будет вывезен. Код бренда не найден.`);
                            let url = `/${Realm}/window/unit/view/${subid}/product_move_to_warehouse/${obj.pid}/${obj.brand_id}`;
                            let data = {
                                qty: obj.available - obj.sold * mult,
                                unit: wareSubid,
                                doit: "Ok"
                            };
                            if (data.qty <= 0)
                                continue;
                            yield tryPost_async(url, data); // если задать неадекватный адрес склада то молча не вывозит и все.
                            //console.log(data);
                        }
                        document.location.reload();
                    });
                });
            });
        }
        function tradehallPrice() {
            let $inputs = $html.find("input[name^='productData[price]'");
            $inputs.each((i, e) => {
                let $inp = $(e);
                $inp.before(`<input type='button' class="pm" data-oper='dec' value='-'>`);
                $inp.after(`<input type='button' class="pm" data-oper='inc' value='+'>`);
            });
            oneOrError($html, "table.grid").on("click", "input.pm", null, function (event) {
                //console.log("input.pm click fired");
                let $btn = $(this);
                let oper = $btn.data("oper");
                let $price = oneOrError($btn.closest("td"), "input:text");
                let price = numberfyOrError($price.val());
                price = oper == "inc" ? price * (1 + PriceStep / 100) : price * (1 - PriceStep / 100);
                $price.val(price.toFixed(2));
            });
        }
        function wareSale() {
            // subid 
            let n = extractIntPositive(document.location.pathname);
            if (n == null)
                throw new Error(`на нашел subid юнита в ссылке ${document.location.pathname}`);
            let subid = n[0];
            // name
            let [name, city] = parseUnitNameCity($html);
            let dict = restoreWare();
            let isSaved = dict[subid] != null;
            let saveWare = () => {
                // собираем всю инфу по товарам которые может хранить собственно сей склад и подготавливаем Запись
                let [, saleItems] = parseWareSaleNew(document, document.location.pathname);
                let products = dictValues(saleItems).map((v, i, a) => v.product);
                dict[subid] = {
                    name: name,
                    city: city,
                    products: products,
                    spec: spec
                };
                storeWare(dict);
            };
            let deleteWare = () => {
                delete dict[subid];
                storeWare(dict);
            };
            // если спецуха изменилась, то обновим данные по складу
            let spec = oneOrError($html, "table:has(a.popup[href*='speciality_change'])").find("td").eq(2).text().trim();
            if (isSaved && dict[subid].spec != spec)
                saveWare();
            // рисуем кнопки хуёпки
            let html = `<input type="checkbox" ${isSaved ? "checked" : ""} id="saveWare" style="margin-left:30px"><label for="saveWare">Запомнить склад</label>`;
            $html.find("label[for='filter-empty-qty']").after(html);
            $html.find("#saveWare").on("click", (event) => {
                let $cbx = $(event.target);
                if ($cbx.prop("checked"))
                    saveWare();
                else
                    deleteWare();
            });
        }
    });
}
/**
 * Сохраняет данные в хранилище
 * @param dict
 */
function storeWare(dict) {
    nullCheck(dict);
    let storageKey = buildStoreKey(Realm, Export2WareStoreKeyCode);
    localStorage[storageKey] = LZString.compress(JSON.stringify(dict));
}
/**
 * Возвращает считанный словарь складов для экспорта либо пустой словарь если ничего нет
 */
function restoreWare() {
    // читаем с хранилища, есть ли данные по складу там
    let storageKey = buildStoreKey(Realm, Export2WareStoreKeyCode);
    let data = localStorage.getItem(storageKey);
    return data == null ? {} : JSON.parse(LZString.decompress(data));
}
/**
 * Читает с локалстораджа данные по розничным товарам с категориями.
   Если там нет, то тащит и сохраняет на будущее
 */
function getProdWithCategories_async() {
    return __awaiter(this, void 0, void 0, function* () {
        let storageKey = buildStoreKey(Realm, ProdCatStoreKeyCode);
        let data = localStorage.getItem(storageKey);
        let todayStr = dateToShort(nullCheck(GameDate));
        // если сегодня уже данные засейвили, тогда вернем их
        // обновлять будем раз в день насильно. вдруг введут новые продукты вся херня
        if (data != null) {
            let [dateStr, prods] = JSON.parse(LZString.decompress(data));
            if (todayStr == dateStr)
                return prods;
        }
        // если данных по категориям еще нет, надо их дернуть и записать в хранилище на будущее
        let url = formatStr(`/api/{0}/main/product/goods`, Realm);
        log("Список всех розничных продуктов устарел. Обновляем.");
        let jsonObj = yield tryGetJSON_async(url);
        let prods = parseRetailProductsAPI(jsonObj, url);
        localStorage[storageKey] = LZString.compress(JSON.stringify([todayStr, prods]));
        return prods;
    });
}
/**
 * формирует общий список ТМ товаров включая франшизы и пишет в хранилище. бдит за обновлением
 */
function getTMProducts_async() {
    return __awaiter(this, void 0, void 0, function* () {
        let storageKey = buildStoreKey(Realm, TMStoreKeyCode);
        let data = localStorage.getItem(storageKey);
        let today = nullCheck(GameDate);
        // обновлять будем раз в виртогод насильно. вдруг введут новые продукты вся херня
        if (data != null) {
            let [dateStr, tm] = JSON.parse(LZString.decompress(data));
            if (today.getFullYear() == dateFromShort(dateStr).getFullYear())
                return tm;
        }
        log("Список всех ТМ продуктов устарел. Обновляем.");
        let urlTM = formatStr(`/{0}/main/globalreport/tm/info`, Realm);
        let html = yield tryGet_async(urlTM);
        let tm = parseTM(html, urlTM);
        let urlFranchise = formatStr(`/{0}/main/franchise_market/list`, Realm);
        html = yield tryGet_async(urlFranchise);
        let franchise = parseFranchise(html, urlTM);
        // теперь для списка ТМ надо дополнить франшизные продукты так как там не было brand_id для них
        for (let img in franchise) {
            let f = franchise[img];
            let t = tm[img];
            if (t == null)
                throw new Error(`что то пошло не так, среди брендов не нашли франшизу ${f.img}`);
            t.brand_id = f.brand_id;
        }
        let todayStr = dateToShort(today);
        localStorage[storageKey] = LZString.compress(JSON.stringify([todayStr, tm]));
        return tm;
    });
}
/**
 * Для заданного набора продуктов выдает список розничных отделов c кол-вом продуктов в каждом
 * @param prods список продуктов
 * @param prodDict словарь содержащий данные по принадлежности продуктов к розничным отделам
 */
function categories(prods, prodCatDict) {
    let cats = {};
    for (let p of prods) {
        let papi = prodCatDict[p.id];
        if (papi == null)
            throw new Error(`В словаре всех розничных продуктов не найден товар pid:${p.id} img:${p.img}`);
        cats[papi.product_category_name] = cats[papi.product_category_name] == null
            ? 1
            : cats[papi.product_category_name] + 1;
    }
    return cats;
}
/**
 * оставляем только склады которые могут хранить хоть 1 продукт из списка
   оставляем только те продукты на складе которые есть в магазине.
   ориентир по pid поэтому бренды будут оставаться даже если в магазе их нет. пофиг
   исходный словарь не затрагивает. формирует новый
 * @param waresDict  список складов
 * @param pids список pid искомых продуктов
 */
function filterWares(waresDict, pids) {
    let resDict = {};
    for (let subid in waresDict) {
        let wp = waresDict[subid];
        let ints = intersect(wp.products.map((v, i, a) => v.id), pids);
        if (ints.length <= 0)
            continue;
        // если есть на складе хранение брендованных, то их pid дублирует обычные товары и число хранения будет больше
        // ровно на число брендованных товаров
        resDict[subid] = {
            name: wp.name,
            city: wp.city,
            spec: wp.spec,
            products: wp.products.filter((v, i, a) => isOneOf(v.id, ints))
        };
    }
    return resDict;
}
// всякий мусор для работы всего
//
var UnitTypes;
// всякий мусор для работы всего
//
(function (UnitTypes) {
    UnitTypes[UnitTypes["unknown"] = 0] = "unknown";
    UnitTypes[UnitTypes["animalfarm"] = 1] = "animalfarm";
    UnitTypes[UnitTypes["farm"] = 2] = "farm";
    UnitTypes[UnitTypes["lab"] = 3] = "lab";
    UnitTypes[UnitTypes["mill"] = 4] = "mill";
    UnitTypes[UnitTypes["mine"] = 5] = "mine";
    UnitTypes[UnitTypes["office"] = 6] = "office";
    UnitTypes[UnitTypes["oilpump"] = 7] = "oilpump";
    UnitTypes[UnitTypes["orchard"] = 8] = "orchard";
    UnitTypes[UnitTypes["sawmill"] = 9] = "sawmill";
    UnitTypes[UnitTypes["shop"] = 10] = "shop";
    UnitTypes[UnitTypes["seaport"] = 11] = "seaport";
    UnitTypes[UnitTypes["warehouse"] = 12] = "warehouse";
    UnitTypes[UnitTypes["workshop"] = 13] = "workshop";
    UnitTypes[UnitTypes["villa"] = 14] = "villa";
    UnitTypes[UnitTypes["fishingbase"] = 15] = "fishingbase";
    UnitTypes[UnitTypes["service_light"] = 16] = "service_light";
    UnitTypes[UnitTypes["fitness"] = 17] = "fitness";
    UnitTypes[UnitTypes["laundry"] = 18] = "laundry";
    UnitTypes[UnitTypes["hairdressing"] = 19] = "hairdressing";
    UnitTypes[UnitTypes["medicine"] = 20] = "medicine";
    UnitTypes[UnitTypes["restaurant"] = 21] = "restaurant";
    UnitTypes[UnitTypes["power"] = 22] = "power";
    UnitTypes[UnitTypes["coal_power"] = 23] = "coal_power";
    UnitTypes[UnitTypes["incinerator_power"] = 24] = "incinerator_power";
    UnitTypes[UnitTypes["oil_power"] = 25] = "oil_power";
    UnitTypes[UnitTypes["sun_power"] = 26] = "sun_power";
    UnitTypes[UnitTypes["fuel"] = 27] = "fuel";
    UnitTypes[UnitTypes["repair"] = 28] = "repair";
    UnitTypes[UnitTypes["apiary"] = 29] = "apiary";
    UnitTypes[UnitTypes["educational"] = 30] = "educational";
    UnitTypes[UnitTypes["kindergarten"] = 31] = "kindergarten";
    UnitTypes[UnitTypes["network"] = 32] = "network";
    UnitTypes[UnitTypes["it"] = 33] = "it";
    UnitTypes[UnitTypes["cellular"] = 34] = "cellular";
})(UnitTypes || (UnitTypes = {}));
var SalePolicies;
(function (SalePolicies) {
    SalePolicies[SalePolicies["nosale"] = 0] = "nosale";
    SalePolicies[SalePolicies["any"] = 1] = "any";
    SalePolicies[SalePolicies["some"] = 2] = "some";
    SalePolicies[SalePolicies["company"] = 3] = "company";
    SalePolicies[SalePolicies["corporation"] = 4] = "corporation";
})(SalePolicies || (SalePolicies = {}));
class ArgumentError extends Error {
    constructor(argument, message) {
        let msg = "argument";
        if (message)
            msg += " " + message;
        super(msg);
    }
}
function dictKeysN(dict) {
    return Object.keys(dict).map((v, i, arr) => parseInt(v));
}
function dictKeys(dict) {
    return Object.keys(dict);
}
function dictValues(dict) {
    let res = [];
    for (let key in dict)
        res.push(dict[key]);
    return res;
}
function dictValuesN(dict) {
    let res = [];
    for (let key in dict)
        res.push(dict[key]);
    return res;
}
function unique(array) {
    let res = [];
    for (let i = 0; i < array.length; i++) {
        let item = array[i];
        if (array.indexOf(item) === i)
            res.push(item);
    }
    return res;
}
function intersect(a, b) {
    // чтобы быстрее бегал indexOf в A кладем более длинный массив
    if (b.length > a.length) {
        let t = b;
        b = a;
        a = t;
    }
    // находим пересечение с дублями
    let intersect = [];
    for (let item of a) {
        if (b.indexOf(item) >= 0)
            intersect.push(item);
    }
    // если надо удалить дубли, удаляем
    return unique(intersect);
}
function isOneOf(item, arr) {
    if (arr.length <= 0)
        return false;
    return arr.indexOf(item) >= 0;
}
function getRealm() {
    // https://*virtonomic*.*/*/main/globalreport/marketing/by_trade_at_cities/*
    // https://*virtonomic*.*/*/window/globalreport/marketing/by_trade_at_cities/*
    let rx = new RegExp(/https:\/\/virtonomic[A-Za-z]+\.[a-zA-Z]+\/([a-zA-Z]+)\/.+/ig);
    let m = rx.exec(document.location.href);
    if (m == null)
        return null;
    return m[1];
}
function getRealmOrError() {
    let realm = getRealm();
    if (realm === null)
        throw new Error("Не смог определить реалм по ссылке " + document.location.href);
    return realm;
}
function getOnlyText(item) {
    // просто children() не отдает текстовые ноды.
    let $childrenNodes = item.contents();
    let res = [];
    for (let i = 0; i < $childrenNodes.length; i++) {
        let el = $childrenNodes.get(i);
        if (el.nodeType === 3)
            res.push($(el).text()); // так как в разных браузерах текст запрашивается по разному, 
        // универсальный способ запросить через jquery
    }
    return res;
}
function monthFromStr(str) {
    let mnth = ["январ", "феврал", "март", "апрел", "ма", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр"];
    for (let i = 0; i < mnth.length; i++) {
        if (str.indexOf(mnth[i]) === 0)
            return i;
    }
    return null;
}
function extractDate(str) {
    let dateRx = /^(\d{1,2})\s+([а-я]+)\s+(\d{1,4})/i;
    let m = dateRx.exec(str);
    if (m == null)
        return null;
    let d = parseInt(m[1]);
    let mon = monthFromStr(m[2]);
    if (mon == null)
        return null;
    let y = parseInt(m[3]);
    return new Date(y, mon, d);
}
function parseGameDate(html) {
    let $html = $(html);
    try {
        // вытащим текущую дату, потому как сохранять данные будем используя ее
        let $date = $html.find("div.date_time");
        if ($date.length !== 1)
            return null;
        //throw new Error("Не получилось получить текущую игровую дату");
        let currentGameDate = extractDate(getOnlyText($date)[0].trim());
        if (currentGameDate == null)
            return null;
        //throw new Error("Не получилось получить текущую игровую дату");
        return currentGameDate;
    }
    catch (err) {
        throw err;
    }
}
function logDebug(msg, ...args) {
    if (!$xioDebug)
        return;
    console.log(msg, ...args);
}
function buildStoreKey(realm, code, subid) {
    if (code.length === 0)
        throw new RangeError("Параметр code не может быть равен '' ");
    if (realm != null && realm.length === 0)
        throw new RangeError("Параметр realm не может быть равен '' ");
    if (subid != null && realm == null)
        throw new RangeError("Как бы нет смысла указывать subid и не указывать realm");
    let res = "^*"; // уникальная ботва которую добавляем ко всем своим данным
    if (realm != null)
        res += "_" + realm;
    if (subid != null)
        res += "_" + subid;
    res += "_" + code;
    return res;
}
function nullCheck(val) {
    if (val == null)
        throw new Error(`nullCheck Error`);
    return val;
}
function numberCheck(value) {
    if (!isFinite(value))
        throw new ArgumentError("value", `${value} не является числом.`);
    return value;
}
function stringCheck(value) {
    if (typeof (value) != "string")
        throw new ArgumentError("value", `${value} не является строкой.`);
    return value;
}
function dateToShort(date) {
    let d = date.getDate();
    let m = date.getMonth() + 1;
    let yyyy = date.getFullYear();
    let dStr = d < 10 ? "0" + d : d.toString();
    let mStr = m < 10 ? "0" + m : m.toString();
    return `${dStr}.${mStr}.${yyyy}`;
}
function formatStr(str, ...args) {
    let res = str.replace(/{(\d+)}/g, (match, number) => {
        if (args[number] == null)
            throw new Error(`плейсхолдер ${number} не имеет значения`);
        return args[number];
    });
    return res;
}
function parseJSON(jsonStr) {
    let obj = JSON.parse(jsonStr, (k, v) => {
        if (v === "t")
            return true;
        if (v === "f")
            return false;
        return (typeof v === "object" || isNaN(v)) ? v : parseFloat(v);
    });
    return obj;
}
function tryGetJSON_async(url, retries = 10, timeout = 1000) {
    return __awaiter(this, void 0, void 0, function* () {
        let $deffered = $.Deferred();
        $.ajax({
            url: url,
            type: "GET",
            cache: false,
            dataType: "text",
            success: (jsonStr, status, jqXHR) => {
                let obj = parseJSON(jsonStr);
                $deffered.resolve(obj);
            },
            error: function (jqXHR, textStatus, errorThrown) {
                retries--;
                if (retries <= 0) {
                    let err = new Error(`can't get ${this.url}\nstatus: ${jqXHR.status}\ntextStatus: ${jqXHR.statusText}\nerror: ${errorThrown}`);
                    $deffered.reject(err);
                    return;
                }
                //logDebug(`ошибка запроса ${this.url} осталось ${retries} попыток`);
                let _this = this;
                setTimeout(() => {
                    $.ajax(_this);
                }, timeout);
            }
        });
        return $deffered.promise();
    });
}
function tryPost_async(url, form, retries = 10, timeout = 1000) {
    return __awaiter(this, void 0, void 0, function* () {
        let $deferred = $.Deferred();
        $.ajax({
            url: url,
            data: form,
            type: "POST",
            success: (data, status, jqXHR) => $deferred.resolve(data),
            error: function (jqXHR, textStatus, errorThrown) {
                retries--;
                if (retries <= 0) {
                    let err = new Error(`can't post ${this.url}\nstatus: ${jqXHR.status}\ntextStatus: ${jqXHR.statusText}\nerror: ${errorThrown}`);
                    $deferred.reject(err);
                    return;
                }
                //logDebug(`ошибка запроса ${this.url} осталось ${retries} попыток`);
                let _this = this;
                setTimeout(() => {
                    $.ajax(_this);
                }, timeout);
            }
        });
        return $deferred.promise();
    });
}
function tryGet_async(url, retries = 10, timeout = 1000) {
    return __awaiter(this, void 0, void 0, function* () {
        let $deffered = $.Deferred();
        $.ajax({
            url: url,
            type: "GET",
            success: (data, status, jqXHR) => $deffered.resolve(data),
            error: function (jqXHR, textStatus, errorThrown) {
                retries--;
                if (retries <= 0) {
                    let err = new Error(`can't get ${this.url}\nstatus: ${jqXHR.status}\ntextStatus: ${jqXHR.statusText}\nerror: ${errorThrown}`);
                    $deffered.reject(err);
                    return;
                }
                let _this = this;
                setTimeout(() => {
                    $.ajax(_this);
                }, timeout);
            }
        });
        return $deffered.promise();
    });
}
function parseTM(html, url) {
    let $html = $(html);
    try {
        let $imgs = isWindow($html, url)
            ? $html.filter("table.grid").find("img")
            : $html.find("table.grid").find("img");
        if ($imgs.length <= 0)
            throw new Error("Не найдено ни одного ТМ товара.");
        let dict = {};
        $imgs.each((i, el) => {
            let $img = $(el);
            let $tdText = $img.closest("td").next("td");
            // /img/products/brand/krakow.gif  - виртономская франшиза
            // /img/products/vera/brand/0909/tarlka.gif
            // /img/products/olga/brand/4100738.gif - реальный ТМ товар. Номер это номер ТМ по факту. надо парсить его
            let img = $img.attr("src");
            let [symbol,] = extractFile(img);
            let brand_id = numberfy(symbol);
            let brandName = $tdText.find("b").text().trim(); // может быть и пустым
            let prodName = getOnlyText($tdText).join("").trim();
            if (prodName.length <= 0)
                throw new Error("ошибка извлечения имени товара франшизы для " + img);
            dict[img] = {
                img: img,
                product_name: prodName,
                brand_name: brandName.length > 0 ? brandName : "noname",
                brand_id: brand_id > 0 ? brand_id : null,
                is_franchise: brand_id <= 0
            };
        });
        return dict;
    }
    catch (err) {
        throw err;
    }
}
function parseFranchise(html, url) {
    let $html = $(html);
    try {
        let $tbl = oneOrError($html, "form > table.list");
        let $rows = $tbl.find("tr.even, tr.odd");
        if ($rows.length < 1)
            throw new Error(`Не найдено ни одной франшизы по ссылке ${url}`);
        let dict = {};
        $rows.each((i, el) => {
            let $r = $(el);
            let $tds = $r.children("td");
            // brand_id
            let m = nullCheck(extractIntPositive($tds.eq(1).find("a").attr("href")));
            let brand_id = m[0];
            if (brand_id <= 0)
                throw new Error(`id франшизы не могут быть < 0. ${$tds.eq(1).find("a").attr("href")}`);
            let brandName = $tds.eq(2).text().trim();
            if (brandName.length <= 0)
                throw new Error(`имя франшизы ${brand_id} не может быть пустым`);
            // /img/products/vera/brand/0909/tarlka.gif
            let img = $tds.eq(2).find("img").attr("src");
            let prodName = $tds.eq(4).text().trim();
            if (prodName.length <= 0)
                throw new Error("ошибка извлечения имени товара франшизы для " + img);
            dict[img] = {
                img: img,
                product_name: prodName,
                brand_name: brandName,
                brand_id: brand_id > 0 ? brand_id : null,
                is_franchise: true
            };
        });
        return dict;
    }
    catch (err) {
        throw err;
    }
}
function parseRetailProductsAPI(jsonObj, url) {
    try {
        let res = {};
        let data = jsonObj;
        for (let pid in data) {
            let item = data[pid];
            if (item.product_symbol.length <= 0)
                throw new Error(`пустая строка вместо символа продукта для ${item.product_name}`);
            let p = {
                product_id: numberCheck(item.product_id),
                product_name: stringCheck(item.product_name),
                product_symbol: stringCheck(item.product_symbol),
                img: `/img/products/${item.product_symbol}.gif`,
                product_category_id: item.product_category_id,
                product_category_name: item.product_category_name
            };
            res[pid] = p;
        }
        return res;
    }
    catch (err) {
        throw err;
    }
}
function parseUnitNameCity($html) {
    let x;
    // сюда либо прилетает ВСЯ страница либо только шапка. поэтому фильтры надо БЕЗ использования классов шапки
    // ниже нам придется ремувать элементы, поэтому надо клонировать див.
    let $div = oneOrError($html, ".content:has(.bg-image) div.title").clone(false, false); // новая универсальное поле имя/городв
    // name
    let name = oneOrError($div, "h1").text().trim();
    if (name == null || name.length < 1)
        throw new Error(`не найдено имя юнита`);
    // city
    // Нижний Новгород (Россия)	строка с городом но там еще куча мусора блять
    // Нижний Новгород (Россия, Южная россия) может и так быть то есть регион
    // привяжемся к ссылке на страну она идет последней
    //let s = $div.find("a:last").map((i, el) => el.previousSibling.nodeValue)[0] as any as string;
    let s = $div.children().detach().end().text().trim();
    let last = s.split(/\t/).pop();
    if (last == null)
        throw new Error(`не найден город юнита ${name}`);
    let m = last.match(/^(.*)\(/i);
    if (m == null || m[1] == null || m[1].length <= 1)
        throw new Error(`не найден город юнита ${name}`);
    let city = m[1].trim();
    return [name, city];
}
function oneOrError($item, selector) {
    let $one = $item.find(selector);
    if ($one.length != 1)
        throw new Error(`Найдено ${$one.length} элементов вместо 1 для селектора ${selector}`);
    return $one;
}
function extractIntPositive(str) {
    let m = cleanStr(str).match(/\d+/ig);
    if (m == null)
        return null;
    let n = m.map((val, i, arr) => numberfyOrError(val, -1));
    return n;
}
function cleanStr(str) {
    return str.replace(/[\s\$\%\©]/g, "");
}
function numberfyOrError(str, minVal = 0, infinity = false) {
    let n = numberfy(str);
    if (!infinity && (n === Number.POSITIVE_INFINITY || n === Number.NEGATIVE_INFINITY))
        throw new RangeError("Получили бесконечность, что запрещено.");
    if (n <= minVal) // TODO: как то блять неудобно что мин граница не разрешается. удобнее было бы если б она была разрешена
        throw new RangeError("Число должно быть > " + minVal);
    return n;
}
function numberfy(str) {
    // возвращает либо число полученно из строки, либо БЕСКОНЕЧНОСТЬ, либо -1 если не получилось преобразовать.
    if (String(str) === 'Не огр.' ||
        String(str) === 'Unlim.' ||
        String(str) === 'Не обм.' ||
        String(str) === 'N’est pas limité' ||
        String(str) === 'No limitado' ||
        String(str) === '无限' ||
        String(str) === 'Nicht beschr.') {
        return Number.POSITIVE_INFINITY;
    }
    else {
        // если str будет undef null или что то страшное, то String() превратит в строку после чего парсинг даст NaN
        // не будет эксепшнов
        let n = parseFloat(cleanStr(String(str)));
        return isNaN(n) ? -1 : n;
    }
}
function parseUnitType($html) {
    // классы откуда можно дернуть тип юнита грузятся скриптом уже после загрузки страницц
    // и добавляются в дивы. Поэтому берем скрипт который это делает и тащим из него информацию
    let lines = $html.find("div.title script").text().split(/\n/);
    let rx = /\bbody\b.*?\bbg-page-unit-(.*)\b/i;
    let typeStr = "";
    for (let line of lines) {
        let arr = rx.exec(line);
        if (arr != null && arr[1] != null) {
            typeStr = arr[1];
            break;
        }
    }
    if (typeStr.length <= 0)
        throw new Error("Невозможно спарсить тип юнита");
    // некоторый онанизм с конверсией но никак иначе
    let type = UnitTypes[typeStr] ? UnitTypes[typeStr] : UnitTypes.unknown;
    if (type == UnitTypes.unknown)
        throw new Error("Не описан тип юнита " + typeStr);
    return type;
}
function closestByTagName(items, tagname) {
    let tag = tagname.toUpperCase();
    let found = [];
    for (let i = 0; i < items.length; i++) {
        let node = items[i];
        while ((node = node.parentNode) && node.nodeName != tag) { }
        ;
        if (node)
            found.push(node);
    }
    return $(found);
}
function isWindow($html, url) {
    return url.indexOf("/window/") > 0;
}
function parseUnitTradeHall(html, url) {
    let $html = $(html);
    try {
        let $tbl = isWindow($html, url)
            ? $html.filter("table.list")
            : $html.find("table.list");
        let str = oneOrError($tbl, "div:first").text().trim();
        let filling = numberfyOrError(str, -1);
        let $rows = closestByTagName($html.find("a.popup"), "tr");
        let thItems = [];
        $rows.each((i, el) => {
            let $r = $(el);
            let $tds = $r.children("td");
            let cityRepUrl = oneOrError($tds.eq(2), "a").attr("href");
            let historyUrl = oneOrError($r, "a.popup").attr("href");
            // продукт
            // картинка может быть просто от /products/ так и ТМ /products/brand/ типа
            let img = oneOrError($tds.eq(2), "img").attr("src");
            let nums = extractIntPositive(cityRepUrl);
            if (nums == null)
                throw new Error("не получилось извлечь id продукта из ссылки " + cityRepUrl);
            let prodID = nums[0];
            let prodName = $tds.eq(2).attr("title").split("(")[0].trim();
            let product = { id: prodID, img: img, name: prodName };
            // склад. может быть -- вместо цены, кач, бренд так что -1 допускается
            let stock = {
                available: numberfyOrError($tds.eq(5).text(), -1),
                deliver: numberfyOrError($tds.eq(4).text().split("[")[1], -1),
                sold: numberfyOrError(oneOrError($tds.eq(3), "a.popup").text(), -1),
                ordered: numberfyOrError(oneOrError($tds.eq(4), "a").text(), -1),
                product: {
                    price: numberfy($tds.eq(8).text()),
                    quality: numberfy($tds.eq(6).text()),
                    brand: numberfy($tds.eq(7).text())
                }
            };
            // прочее "productData[price][{37181683}]" а не то что вы подумали
            let $input = oneOrError($tds.eq(9), "input");
            let name = $input.attr("name");
            let currentPrice = numberfyOrError($input.val(), -1);
            let dontSale = $tds.eq(9).find("span").text().indexOf("продавать") >= 0;
            // среднегородские цены
            let share = numberfyOrError($tds.eq(10).text(), -1);
            let city = {
                price: numberfyOrError($tds.eq(11).text()),
                quality: numberfyOrError($tds.eq(12).text()),
                brand: numberfyOrError($tds.eq(13).text(), -1)
            };
            thItems.push({
                product: product,
                stock: stock,
                price: currentPrice,
                city: city,
                share: share,
                historyUrl: historyUrl,
                reportUrl: cityRepUrl,
                name: name,
                dontSale: dontSale
            });
        });
        return [filling, thItems];
    }
    catch (err) {
        throw err;
    }
}
function parseWareSaleNew(html, url) {
    let $html = $(html);
    try {
        let $form = isWindow($html, url)
            ? $html.filter("form[name=storageForm]")
            : $html.find("form[name=storageForm]");
        if ($form.length <= 0)
            throw new Error("Не найдена форма.");
        let $tbl = oneOrError($html, "table.grid");
        let $rows = closestByTagName($tbl.find("select[name*='storageData']"), "tr");
        let dict = {};
        $rows.each((i, el) => {
            let $r = $(el);
            let $tds = $r.children("td");
            // товар
            let prod = parseProduct($tds.eq(2));
            let $price = oneOrError($r, "input.money[name*='[price]']");
            let $policy = oneOrError($r, "select[name*='[constraint]']");
            let $maxQty = oneOrError($r, "input.money[name*='[max_qty]']");
            let maxQty = numberfy($maxQty.val());
            maxQty = maxQty > 0 ? maxQty : null;
            dict[prod.img] = {
                product: prod,
                stock: parseStock($tds.eq(3)),
                outOrdered: numberfyOrError($tds.eq(4).text(), -1),
                price: numberfyOrError($price.val(), -1),
                salePolicy: $policy.prop("selectedIndex"),
                maxQty: maxQty,
                priceName: $price.attr("name"),
                policyName: $policy.attr("name"),
                maxName: $maxQty.attr("name"),
            };
        });
        return [$form, dict];
    }
    catch (err) {
        //throw new ParseError("sale", url, err);
        throw err;
    }
    function parseProduct($td) {
        // товар
        let $img = oneOrError($td, "img");
        let img = $img.attr("src");
        let name = $img.attr("alt");
        let $a = oneOrError($td, "a");
        let n = extractIntPositive($a.attr("href"));
        if (n == null || n.length > 1)
            throw new Error("не нашли id товара " + img);
        let id = n[0];
        return { name: name, img: img, id: id };
    }
    // если товара нет, то характеристики товара зануляет
    function parseStock($td) {
        let $rows = $td.find("tr");
        // могут быть прочерки для товаров которых нет вообще
        let available = numberfy(oneOrError($td, "td:contains(Количество)").next("td").text());
        if (available < 0)
            available = 0;
        return {
            available: available,
            product: {
                brand: 0,
                price: available > 0 ? numberfyOrError(oneOrError($td, "td:contains(Себестоимость)").next("td").text()) : 0,
                quality: available > 0 ? numberfyOrError(oneOrError($td, "td:contains(Качество)").next("td").text()) : 0
            }
        };
    }
}
function extractFile(fileUrl) {
    let items = fileUrl.split("/");
    if (items.length < 2)
        throw new Error(`Очевидно что ${fileUrl} не ссылка на файл`);
    let file = items[items.length - 1];
    let [symbol, ext] = file.split("."); // если нет расширения то будет undef во втором
    ext = ext == null ? "" : ext;
    if (symbol.length <= 0)
        throw new Error(`Нулевая длина имени файлв в ${fileUrl}`);
    return [symbol, ext];
}
function dateFromShort(str) {
    let items = str.split(".");
    let d = parseInt(items[0]);
    if (d <= 0)
        throw new Error("дата неправильная.");
    let m = parseInt(items[1]) - 1;
    if (m < 0)
        throw new Error("месяц неправильная.");
    let y = parseInt(items[2]);
    if (y < 0)
        throw new Error("год неправильная.");
    return new Date(y, m, d);
}
$(document).ready(() => run_async());