您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Массовый вывоз товара из магазина на склады в 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());