From e52c75538feeb0418669604f38436c1cf32e87b1 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Wed, 3 Jun 2026 11:34:08 +0300 Subject: [PATCH] feat: unify tariff package price rows --- .../admin/sections/TariffEditorModal.svelte | 515 +++++++----------- frontend/src/lib/admin/tariffDraft.js | 130 +++-- frontend/src/styles/admin.css | 10 +- 3 files changed, 269 insertions(+), 386 deletions(-) diff --git a/frontend/src/admin/sections/TariffEditorModal.svelte b/frontend/src/admin/sections/TariffEditorModal.svelte index 8f37859..53f9bbf 100644 --- a/frontend/src/admin/sections/TariffEditorModal.svelte +++ b/frontend/src/admin/sections/TariffEditorModal.svelte @@ -32,16 +32,6 @@ })); $: defaultCurrencyKey = normalizeCurrencyKey(tariffsCatalog?.default_currency || "rub"); $: defaultCurrencyCode = defaultCurrencyKey.toUpperCase(); - $: currencyPackageLabel = at( - "tariff_btn_package_currency", - { currency: defaultCurrencyCode }, - `Пакет ${defaultCurrencyCode}` - ); - $: currencyPaymentLabel = at( - "payment_default_currency", - { currency: defaultCurrencyCode }, - `Оплата ${defaultCurrencyCode}` - ); $: currencyPriceColumnLabel = at( "tariff_col_price_currency", { currency: defaultCurrencyCode }, @@ -413,103 +403,71 @@ >
- tariffsStore.addDraftRow("premiumTopupRubRows", { gb: 10, price: "" })} - > {currencyPackageLabel} - tariffsStore.addDraftRow("premiumTopupStarsRows", { gb: 10, price: "" })} - > {at("tariff_btn_package_stars", {}, "Пакет ⭐")} {at("tariff_btn_package", {}, "Пакет")}
-
+ {#if tariffDraft.premiumTopupRows.length}
- {currencyPaymentLabel} - {#if tariffDraft.premiumTopupRubRows.length} -
- {at("tariff_col_volume_gb", {}, "Объём, GB")} - {currencyPriceColumnLabel} - -
- {/if} - {#each tariffDraft.premiumTopupRubRows as row, index} -
- - - tariffsStore.removeDraftRow("premiumTopupRubRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} -
-
- {at("payment_stars", {}, "Оплата Telegram Stars")} + + {at("tariff_col_volume_gb", {}, "Объём, GB")} + {currencyPriceColumnLabel} + {at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")} + +
+ tariffsStore.moveDraftRow("premiumTopupRows", from, to)} + let:item={row} + let:index > - {#if tariffDraft.premiumTopupStarsRows.length} -
- {at("tariff_col_volume_gb", {}, "Объём, GB")} - {at("tariff_col_price_stars", {}, "Цена, ⭐")} - -
- {/if} - {#each tariffDraft.premiumTopupStarsRows as row, index} -
- - - tariffsStore.removeDraftRow("premiumTopupStarsRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} + + + + tariffsStore.removeDraftRow("premiumTopupRows", index)} + aria-label={at("btn_delete", {}, "Удалить")}> +
- + {/if} @@ -641,32 +599,26 @@
tariffsStore.addDraftRow("trafficRubRows", { gb: 10, price: "" })} - > {currencyPackageLabel} - tariffsStore.addDraftRow("trafficStarsRows", { gb: 10, price: "" })} - > {at("tariff_btn_package_stars", {}, "Пакет ⭐")} + tariffsStore.addDraftRow("trafficRows", { gb: 10, price: "", stars: "" })} + > {at("tariff_btn_package", {}, "Пакет")}
-
+ {#if tariffDraft.trafficRows.length}
- {currencyPaymentLabel} - {#if tariffDraft.trafficRubRows.length} -
- - {at("tariff_col_volume_gb", {}, "Объём, GB")} - {currencyPriceColumnLabel} - -
- {/if} +
+ + {at("tariff_col_volume_gb", {}, "Объём, GB")} + {currencyPriceColumnLabel} + {at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")} + +
tariffsStore.moveDraftRow("trafficRubRows", from, to)} + onReorder={(from, to) => tariffsStore.moveDraftRow("trafficRows", from, to)} let:item={row} let:index > @@ -688,61 +640,24 @@ bind:value={row.price} aria-label={currencyPriceAriaLabel} /> - tariffsStore.removeDraftRow("trafficRubRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> - -
-
- {at("payment_stars", {}, "Оплата Telegram Stars")} - {#if tariffDraft.trafficStarsRows.length} -
- - {at("tariff_col_volume_gb", {}, "Объём, GB")} - {at("tariff_col_price_stars", {}, "Цена, ⭐")} - -
- {/if} - tariffsStore.moveDraftRow("trafficStarsRows", from, to)} - let:item={row} - let:index - > - tariffsStore.removeDraftRow("trafficStarsRows", index)} + onclick={() => tariffsStore.removeDraftRow("trafficRows", index)} aria-label={at("btn_delete", {}, "Удалить")}>
-
+ {/if} {/if} @@ -766,96 +681,65 @@
tariffsStore.addDraftRow("topupRubRows", { gb: 10, price: "" })} - > {currencyPackageLabel} - tariffsStore.addDraftRow("topupStarsRows", { gb: 10, price: "" })} - > {at("tariff_btn_package_stars", {}, "Пакет ⭐")} + tariffsStore.addDraftRow("topupRows", { gb: 10, price: "", stars: "" })} + > {at("tariff_btn_package", {}, "Пакет")}
-
+ {#if tariffDraft.topupRows.length}
- {currencyPaymentLabel} - {#if tariffDraft.topupRubRows.length} -
- {at("tariff_col_volume_gb", {}, "Объём, GB")} - {currencyPriceColumnLabel} - -
- {/if} - {#each tariffDraft.topupRubRows as row, index} -
- - - tariffsStore.removeDraftRow("topupRubRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} -
-
- {at("payment_stars", {}, "Оплата Telegram Stars")} + + {at("tariff_col_volume_gb", {}, "Объём, GB")} + {currencyPriceColumnLabel} + {at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")} + +
+ tariffsStore.moveDraftRow("topupRows", from, to)} + let:item={row} + let:index > - {#if tariffDraft.topupStarsRows.length} -
- {at("tariff_col_volume_gb", {}, "Объём, GB")} - {at("tariff_col_price_stars", {}, "Цена, ⭐")} - -
- {/if} - {#each tariffDraft.topupStarsRows as row, index} -
- - - tariffsStore.removeDraftRow("topupStarsRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} + + + + tariffsStore.removeDraftRow("topupRows", index)} + aria-label={at("btn_delete", {}, "Удалить")}> +
- + {/if} {:else}

@@ -890,104 +774,69 @@

tariffsStore.addDraftRow("hwidRubRows", { count: 1, price: "" })} - > {currencyPackageLabel} - tariffsStore.addDraftRow("hwidStarsRows", { count: 1, price: "" })} - > {at("tariff_btn_package_stars", {}, "Пакет ⭐")} + tariffsStore.addDraftRow("hwidRows", { count: 1, price: "", stars: "" })} + > {at("tariff_btn_package", {}, "Пакет")}
-
+ {#if tariffDraft.hwidRows.length}
- {currencyPaymentLabel} - {#if tariffDraft.hwidRubRows.length} -
- {at("tariff_col_hwid_count", {}, "+ устройств")} - {currencyPriceColumnLabel} - -
- {/if} - {#each tariffDraft.hwidRubRows as row, index} -
- - - tariffsStore.removeDraftRow("hwidRubRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} -
-
- {at("payment_stars", {}, "Оплата Telegram Stars")} + + {at("tariff_col_hwid_count", {}, "+ устройств")} + {currencyPriceColumnLabel} + {at("tariff_col_price_stars_full", {}, "Цена, ⭐ Stars")} + +
+ tariffsStore.moveDraftRow("hwidRows", from, to)} + let:item={row} + let:index > - {#if tariffDraft.hwidStarsRows.length} -
- {at("tariff_col_hwid_count", {}, "+ устройств")} - {at("tariff_col_price_stars", {}, "Цена, ⭐")} - -
- {/if} - {#each tariffDraft.hwidStarsRows as row, index} -
- - - tariffsStore.removeDraftRow("hwidStarsRows", index)} - aria-label={at("btn_delete", {}, "Удалить")}> -
- {/each} + + + + tariffsStore.removeDraftRow("hwidRows", index)} + aria-label={at("btn_delete", {}, "Удалить")}> +
- + {/if} diff --git a/frontend/src/lib/admin/tariffDraft.js b/frontend/src/lib/admin/tariffDraft.js index d6fb290..0ab7542 100644 --- a/frontend/src/lib/admin/tariffDraft.js +++ b/frontend/src/lib/admin/tariffDraft.js @@ -24,17 +24,13 @@ export function emptyTariffDraft() { { months: 6, rub: 1200, stars: "", referral_inviter: 15, referral_referee: 7 }, { months: 12, rub: 2400, stars: "", referral_inviter: 30, referral_referee: 15 }, ], - topupRubRows: [], - topupStarsRows: [], - premiumTopupRubRows: [], - premiumTopupStarsRows: [], - trafficRubRows: [ - { gb: 10, price: 199 }, - { gb: 50, price: 799 }, + topupRows: [], + premiumTopupRows: [], + trafficRows: [ + { gb: 10, price: 199, stars: "" }, + { gb: 50, price: 799, stars: "" }, ], - trafficStarsRows: [], - hwidRubRows: [], - hwidStarsRows: [], + hwidRows: [], }; } @@ -66,6 +62,50 @@ export function rowsFromPackages(packageSet, currency, valueKey) { })); } +function packageValueSignature(value) { + const num = Number(value); + return Number.isFinite(num) ? String(num) : String(value || ""); +} + +export function packageRowsFromPackageSet(packageSet, currency, valueKey) { + const currencyRows = rowsFromPackages(packageSet, currency, valueKey); + const starsRows = rowsFromPackages(packageSet, "stars", valueKey); + const usedStars = new Set(); + + const rows = currencyRows.map((row) => { + const rowSignature = packageValueSignature(row[valueKey]); + const starsIndex = starsRows.findIndex( + (starsRow, index) => + !usedStars.has(index) && packageValueSignature(starsRow[valueKey]) === rowSignature + ); + const starsRow = starsIndex >= 0 ? starsRows[starsIndex] : null; + if (starsIndex >= 0) usedStars.add(starsIndex); + + return { + [valueKey]: row[valueKey], + price: row.price, + stars: starsRow?.price ?? "", + prices: row.prices, + min_price: row.min_price, + stars_prices: starsRow?.prices, + stars_min_price: starsRow?.min_price ?? "", + }; + }); + + starsRows.forEach((starsRow, index) => { + if (usedStars.has(index)) return; + rows.push({ + [valueKey]: starsRow[valueKey], + price: "", + stars: starsRow.price, + stars_prices: starsRow.prices, + stars_min_price: starsRow.min_price ?? "", + }); + }); + + return rows; +} + export function draftFromTariff(tariff, defaultCurrency = "rub") { const currency = normalizeCurrencyKey(defaultCurrency); const defaultPrices = tariff.prices?.[currency] || {}; @@ -109,14 +149,10 @@ export function draftFromTariff(tariff, defaultCurrency = "rub") { hwid_device_limit: tariff.hwid_device_limit ?? "", conversion_rate_rub_per_gb: tariff.conversion_rate_rub_per_gb ?? "", periodRows: periodRows.length ? periodRows : emptyTariffDraft().periodRows, - topupRubRows: rowsFromPackages(tariff.topup_packages, currency, "gb"), - topupStarsRows: rowsFromPackages(tariff.topup_packages, "stars", "gb"), - premiumTopupRubRows: rowsFromPackages(tariff.premium_topup_packages, currency, "gb"), - premiumTopupStarsRows: rowsFromPackages(tariff.premium_topup_packages, "stars", "gb"), - trafficRubRows: rowsFromPackages(tariff.traffic_packages, currency, "gb"), - trafficStarsRows: rowsFromPackages(tariff.traffic_packages, "stars", "gb"), - hwidRubRows: rowsFromPackages(tariff.hwid_device_packages, currency, "count"), - hwidStarsRows: rowsFromPackages(tariff.hwid_device_packages, "stars", "count"), + topupRows: packageRowsFromPackageSet(tariff.topup_packages, currency, "gb"), + premiumTopupRows: packageRowsFromPackageSet(tariff.premium_topup_packages, currency, "gb"), + trafficRows: packageRowsFromPackageSet(tariff.traffic_packages, currency, "gb"), + hwidRows: packageRowsFromPackageSet(tariff.hwid_device_packages, currency, "count"), }; } @@ -156,10 +192,34 @@ export function packagesFromRows(rows, valueKey) { .filter((row) => row[valueKey] > 0 && row.price !== null && row.price >= 0); } -export function packageSetFromRows(rubRows, starsRows, valueKey, defaultCurrency = "rub") { +export function packagesFromPackageRows(rows, valueKey, priceKey, options = {}) { + const pricesKey = options.pricesKey || "prices"; + const minPriceKey = options.minPriceKey || "min_price"; + return (rows || []) + .map((row) => { + const pkg = { + [valueKey]: parseNumber(row[valueKey]), + price: parseNumber(row[priceKey]), + }; + if (row[pricesKey] && typeof row[pricesKey] === "object") { + pkg.prices = structuredCloneSafe(row[pricesKey]); + } + const minPrice = parseNumber(row[minPriceKey]); + if (minPrice !== null) { + pkg.min_price = minPrice; + } + return pkg; + }) + .filter((row) => row[valueKey] > 0 && row.price !== null && row.price >= 0); +} + +export function packageSetFromRows(rows, valueKey, defaultCurrency = "rub") { const currency = normalizeCurrencyKey(defaultCurrency); - const defaultCurrencyPackages = packagesFromRows(rubRows, valueKey); - const stars = packagesFromRows(starsRows, valueKey); + const defaultCurrencyPackages = packagesFromPackageRows(rows, valueKey, "price"); + const stars = packagesFromPackageRows(rows, valueKey, "stars", { + pricesKey: "stars_prices", + minPriceKey: "stars_min_price", + }); if (!defaultCurrencyPackages.length && !stars.length) return null; return { ...(defaultCurrencyPackages.length ? { [currency]: defaultCurrencyPackages } : {}), @@ -200,21 +260,11 @@ export function tariffFromDraft(draft, fallbackCurrency = "rub") { const hwidLimit = parseIntNumber(draft.hwid_device_limit); if (hwidLimit !== null) tariff.hwid_device_limit = hwidLimit; - const hwidPackages = packageSetFromRows( - draft.hwidRubRows, - draft.hwidStarsRows, - "count", - defaultCurrency - ); + const hwidPackages = packageSetFromRows(draft.hwidRows, "count", defaultCurrency); if (hwidPackages) tariff.hwid_device_packages = hwidPackages; const premiumMonthlyGb = parseNumber(draft.premium_monthly_gb); if (premiumMonthlyGb !== null) tariff.premium_monthly_gb = premiumMonthlyGb; - const premiumTopupPackages = packageSetFromRows( - draft.premiumTopupRubRows, - draft.premiumTopupStarsRows, - "gb", - defaultCurrency - ); + const premiumTopupPackages = packageSetFromRows(draft.premiumTopupRows, "gb", defaultCurrency); if (premiumTopupPackages) tariff.premium_topup_packages = premiumTopupPackages; if (tariff.billing_model === "period") { @@ -254,20 +304,10 @@ export function tariffFromDraft(draft, fallbackCurrency = "rub") { .filter((row) => row.referral_referee !== null) .map((row) => [String(row.months), row.referral_referee]) ); - const topupPackages = packageSetFromRows( - draft.topupRubRows, - draft.topupStarsRows, - "gb", - defaultCurrency - ); + const topupPackages = packageSetFromRows(draft.topupRows, "gb", defaultCurrency); if (topupPackages) tariff.topup_packages = topupPackages; } else { - const trafficPackages = packageSetFromRows( - draft.trafficRubRows, - draft.trafficStarsRows, - "gb", - defaultCurrency - ); + const trafficPackages = packageSetFromRows(draft.trafficRows, "gb", defaultCurrency); if (trafficPackages) tariff.traffic_packages = trafficPackages; const conversion = parseNumber(draft.conversion_rate_rub_per_gb); if (conversion !== null) tariff.conversion_rate_rub_per_gb = conversion; diff --git a/frontend/src/styles/admin.css b/frontend/src/styles/admin.css index 346f67a..d2ae0ca 100644 --- a/frontend/src/styles/admin.css +++ b/frontend/src/styles/admin.css @@ -3581,13 +3581,8 @@ } .admin-row-editor-line.admin-row-editor-drag { - grid-template-columns: 24px minmax(90px, 1fr) minmax(110px, 1fr) 32px; -} - -.admin-package-columns { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; + grid-template-columns: 24px minmax(90px, 1fr) minmax(110px, 1fr) minmax(110px, 1fr) + 32px; } .admin-dialog-actions { @@ -3799,7 +3794,6 @@ } .admin-form-row.admin-form-row-3, - .admin-package-columns, .admin-row-editor-line, .admin-row-editor-line.admin-row-editor-4, .admin-row-editor-line.admin-row-editor-6,