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}
-
- {/if}
- {#each tariffDraft.premiumTopupRubRows as row, index}
-
- {/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}
-
- {/if}
- {#each tariffDraft.premiumTopupStarsRows as row, index}
-
- {/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}
-
- {/if}
+
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", {}, "Удалить")}>
-
-
-
-
+ {/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}
-
- {/if}
- {#each tariffDraft.topupRubRows as row, index}
-
- {/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}
-
- {/if}
- {#each tariffDraft.topupStarsRows as row, index}
-
- {/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}
-
- {/if}
- {#each tariffDraft.hwidRubRows as row, index}
-
- {/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}
-
- {/if}
- {#each tariffDraft.hwidStarsRows as row, index}
-
- {/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,