docs: refresh docs demo data
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
||||
import { DEV_MOCK } from "./previewMock.js";
|
||||
import { DEMO_DATASET } from "./demoDataset.js";
|
||||
|
||||
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
|
||||
|
||||
function defaultClone(value) {
|
||||
try {
|
||||
@@ -8,6 +11,616 @@ function defaultClone(value) {
|
||||
}
|
||||
}
|
||||
|
||||
let demoPromosState = null;
|
||||
let demoAdsState = null;
|
||||
let demoSupportTicketsState = null;
|
||||
let demoSupportMessagesState = null;
|
||||
let demoTariffsState = null;
|
||||
const demoSettingsChanges = new Map();
|
||||
|
||||
function demoPromos() {
|
||||
if (!demoPromosState) demoPromosState = defaultClone(DEMO_DATASET.promos || []);
|
||||
return demoPromosState;
|
||||
}
|
||||
|
||||
function demoAds() {
|
||||
if (!demoAdsState) demoAdsState = defaultClone(DEMO_DATASET.ads || []);
|
||||
return demoAdsState;
|
||||
}
|
||||
|
||||
function demoSupportTickets() {
|
||||
if (!demoSupportTicketsState) {
|
||||
demoSupportTicketsState = defaultClone(DEMO_DATASET.supportTickets || []);
|
||||
}
|
||||
return demoSupportTicketsState;
|
||||
}
|
||||
|
||||
function demoSupportMessages() {
|
||||
if (!demoSupportMessagesState) {
|
||||
demoSupportMessagesState = defaultClone(DEMO_DATASET.supportMessages || {});
|
||||
}
|
||||
return demoSupportMessagesState;
|
||||
}
|
||||
|
||||
function demoTariffs() {
|
||||
if (!demoTariffsState) {
|
||||
demoTariffsState = defaultClone(
|
||||
DEMO_DATASET.tariffsCatalog || {
|
||||
default_tariff: "",
|
||||
topup_packages_default: { rub: [], stars: [] },
|
||||
tariffs: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
return demoTariffsState;
|
||||
}
|
||||
|
||||
function queryParams(path) {
|
||||
return new URLSearchParams(String(path || "").split("?")[1] || "");
|
||||
}
|
||||
|
||||
function jsonBody(options) {
|
||||
try {
|
||||
return options?.body ? JSON.parse(String(options.body)) : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function writeDemoLanguage(language) {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.localStorage?.setItem(DEMO_LANGUAGE_STORAGE_KEY, language);
|
||||
} catch {
|
||||
// Demo storage can be unavailable in private contexts; the in-memory mock still updates.
|
||||
}
|
||||
}
|
||||
|
||||
function paged(items, params, fallbackSize = 25) {
|
||||
const total = items.length;
|
||||
if (params.has("limit") || params.has("offset")) {
|
||||
const limit = Math.max(1, Number(params.get("limit") || fallbackSize));
|
||||
const offset = Math.max(0, Number(params.get("offset") || 0));
|
||||
return {
|
||||
items: items.slice(offset, offset + limit),
|
||||
total,
|
||||
page: Math.floor(offset / limit),
|
||||
pageSize: limit,
|
||||
};
|
||||
}
|
||||
const page = Math.max(0, Number(params.get("page") || 0));
|
||||
const pageSize = Math.max(1, Number(params.get("page_size") || fallbackSize));
|
||||
const start = page * pageSize;
|
||||
return { items: items.slice(start, start + pageSize), total, page, pageSize };
|
||||
}
|
||||
|
||||
function stringDate(value) {
|
||||
const time = Date.parse(value || "");
|
||||
return Number.isFinite(time) ? time : 0;
|
||||
}
|
||||
|
||||
function userName(user) {
|
||||
return (
|
||||
[user?.first_name, user?.last_name].filter(Boolean).join(" ").trim() ||
|
||||
user?.username ||
|
||||
user?.email ||
|
||||
String(user?.user_id || "")
|
||||
);
|
||||
}
|
||||
|
||||
function filterDemoUsers(params) {
|
||||
let out = [...(DEMO_DATASET.adminUsers || [])];
|
||||
const q = (params.get("q") || params.get("search") || "").trim().toLowerCase();
|
||||
if (q) {
|
||||
out = out.filter((user) =>
|
||||
[
|
||||
user.user_id,
|
||||
user.telegram_id,
|
||||
user.username,
|
||||
user.first_name,
|
||||
user.last_name,
|
||||
user.email,
|
||||
user.panel_user_uuid,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(q))
|
||||
);
|
||||
}
|
||||
|
||||
const filter = params.get("filter") || "all";
|
||||
if (filter === "active") out = out.filter((user) => !user.is_banned);
|
||||
else if (filter === "banned") out = out.filter((user) => user.is_banned);
|
||||
else if (filter === "tg_linked") out = out.filter((user) => user.telegram_linked);
|
||||
else if (filter === "no_tg") out = out.filter((user) => !user.telegram_linked);
|
||||
else if (filter === "email_linked") out = out.filter((user) => Boolean(user.email));
|
||||
else if (filter === "no_email") out = out.filter((user) => !user.email);
|
||||
else if (filter === "panel_linked") out = out.filter((user) => Boolean(user.panel_user_uuid));
|
||||
|
||||
const panelStatus = params.get("panel_status") || "all";
|
||||
if (panelStatus !== "all") out = out.filter((user) => user.panel_status === panelStatus);
|
||||
|
||||
const premiumTraffic = params.get("premium_traffic") || "all";
|
||||
if (premiumTraffic !== "all") {
|
||||
out = out.filter((user) => (user.premium_traffic?.state || "none") === premiumTraffic);
|
||||
}
|
||||
|
||||
const sort = params.get("sort") || "registered_desc";
|
||||
out.sort((a, b) => {
|
||||
if (sort === "registered_asc")
|
||||
return stringDate(a.registration_date) - stringDate(b.registration_date);
|
||||
if (sort === "name_asc") return userName(a).localeCompare(userName(b));
|
||||
if (sort === "name_desc") return userName(b).localeCompare(userName(a));
|
||||
if (sort === "id_asc") return Number(a.user_id || 0) - Number(b.user_id || 0);
|
||||
if (sort === "id_desc") return Number(b.user_id || 0) - Number(a.user_id || 0);
|
||||
if (sort === "premium_ratio_asc")
|
||||
return Number(a.premium_traffic?.percent ?? -1) - Number(b.premium_traffic?.percent ?? -1);
|
||||
if (sort === "premium_ratio_desc")
|
||||
return Number(b.premium_traffic?.percent ?? -1) - Number(a.premium_traffic?.percent ?? -1);
|
||||
return stringDate(b.registration_date) - stringDate(a.registration_date);
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
function demoSupportCounts(items = demoSupportTickets()) {
|
||||
const byStatus = { open: 0, awaiting_admin: 0, awaiting_user: 0, resolved: 0, closed: 0 };
|
||||
for (const item of items) byStatus[item.status] = (byStatus[item.status] || 0) + 1;
|
||||
const closed = (byStatus.closed || 0) + (byStatus.resolved || 0);
|
||||
return {
|
||||
...byStatus,
|
||||
active: items.length - closed,
|
||||
closed,
|
||||
total: items.length,
|
||||
total_unread_admin: items.reduce((sum, item) => sum + Number(item.unread_admin_count || 0), 0),
|
||||
};
|
||||
}
|
||||
|
||||
function filterDemoSupportTickets(items, params) {
|
||||
let out = [...items];
|
||||
const status = params.get("status");
|
||||
if (status === "active")
|
||||
out = out.filter((item) => !["closed", "resolved"].includes(item.status));
|
||||
else if (status === "closed")
|
||||
out = out.filter((item) => ["closed", "resolved"].includes(item.status));
|
||||
else if (status) out = out.filter((item) => item.status === status);
|
||||
|
||||
const priority = params.get("priority");
|
||||
if (priority) out = out.filter((item) => item.priority === priority);
|
||||
const category = params.get("category");
|
||||
if (category) out = out.filter((item) => item.category === category);
|
||||
const search = (params.get("search") || "").trim().toLowerCase();
|
||||
if (search) {
|
||||
out = out.filter((item) =>
|
||||
[item.subject, item.user?.username, item.user?.email, item.ticket_id]
|
||||
.filter(Boolean)
|
||||
.some((value) => String(value).toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
const priorityRank = { urgent: 4, high: 3, normal: 2, low: 1 };
|
||||
const sort = params.get("sort") || "updated_desc";
|
||||
out.sort((a, b) => {
|
||||
if (sort === "importance_desc") {
|
||||
return (
|
||||
(priorityRank[b.priority] || 0) - (priorityRank[a.priority] || 0) ||
|
||||
stringDate(b.last_message_at || b.created_at) -
|
||||
stringDate(a.last_message_at || a.created_at)
|
||||
);
|
||||
}
|
||||
if (sort === "updated_asc") {
|
||||
return (
|
||||
stringDate(a.last_message_at || a.created_at) -
|
||||
stringDate(b.last_message_at || b.created_at)
|
||||
);
|
||||
}
|
||||
if (sort === "created_desc") return stringDate(b.created_at) - stringDate(a.created_at);
|
||||
if (sort === "created_asc") return stringDate(a.created_at) - stringDate(b.created_at);
|
||||
return (
|
||||
stringDate(b.last_message_at || b.created_at) - stringDate(a.last_message_at || a.created_at)
|
||||
);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
function demoSettingsSections(clone) {
|
||||
const sections = clone(DEMO_DATASET.settingsSections || []);
|
||||
for (const section of sections) {
|
||||
for (const field of section.fields || []) {
|
||||
if (!demoSettingsChanges.has(field.key)) continue;
|
||||
const change = demoSettingsChanges.get(field.key);
|
||||
if (change.deleted) {
|
||||
field.value = field.default ?? "";
|
||||
field.overridden = false;
|
||||
} else {
|
||||
field.value = change.value;
|
||||
field.overridden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function applyDemoSettingToMock(key, value) {
|
||||
if (key === "WEBAPP_TITLE") DEV_MOCK.config.title = value || "";
|
||||
if (key === "WEBAPP_LOGO_URL") DEV_MOCK.config.logoUrl = value || "";
|
||||
if (key === "WEBAPP_LOGO_USE_EMOJI") DEV_MOCK.config.logoUseEmoji = Boolean(value);
|
||||
if (key === "WEBAPP_LOGO_EMOJI") DEV_MOCK.config.logoEmoji = value || "";
|
||||
if (key === "WEBAPP_LOGO_EMOJI_FONT") DEV_MOCK.config.logoEmojiFont = value || "system";
|
||||
if (key === "WEBAPP_FAVICON_URL" || key === "WEBAPP_LOGO_FAVICON_URL") {
|
||||
DEV_MOCK.config.faviconUrl = value || DEV_MOCK.config.faviconUrl || "";
|
||||
}
|
||||
if (key === "WEBAPP_FAVICON_USE_CUSTOM") DEV_MOCK.config.faviconUseCustom = Boolean(value);
|
||||
}
|
||||
|
||||
function userSnapshotForTicket(ticket) {
|
||||
const detail = DEMO_DATASET.adminUserDetails?.[String(ticket?.user_id)] || {};
|
||||
const user = detail.user || ticket?.user || {};
|
||||
const sub = detail.active_subscription || {};
|
||||
return {
|
||||
user_id: user.user_id,
|
||||
name: userName(user) || user.username || user.email || String(user.user_id || ""),
|
||||
username: user.username || "",
|
||||
email: user.email || "",
|
||||
tariff: sub.tariff_name || sub.tariff_key || "Demo",
|
||||
panel_status: user.panel_status || sub.status_from_panel || "",
|
||||
remaining: sub.end_date || "",
|
||||
regular_traffic: `${sub.traffic_used_bytes || 0} / ${sub.traffic_limit_bytes || 0}`,
|
||||
premium_traffic: `${sub.premium_used_bytes || 0} / ${sub.premium_limit_bytes || 0}`,
|
||||
};
|
||||
}
|
||||
|
||||
function demoApiResponse(path, cleanPath, options, context) {
|
||||
const { clone, currentLang = "ru", normalizeLangCode = (value) => value || "ru" } = context;
|
||||
const method = String(options.method || "GET").toUpperCase();
|
||||
const params = queryParams(path);
|
||||
|
||||
if (cleanPath === "/admin/stats") return clone(DEMO_DATASET.stats);
|
||||
if (cleanPath === "/admin/sync") return { ok: true, status: "queued" };
|
||||
|
||||
if (cleanPath === "/admin/payments") {
|
||||
const page = paged(DEMO_DATASET.adminPayments || [], params, 25);
|
||||
return {
|
||||
ok: true,
|
||||
payments: clone(page.items),
|
||||
total: page.total,
|
||||
page: page.page,
|
||||
page_size: page.pageSize,
|
||||
};
|
||||
}
|
||||
if (/^\/admin\/payments\/\d+$/.test(cleanPath)) {
|
||||
const id = Number(cleanPath.split("/").pop());
|
||||
const payment = (DEMO_DATASET.adminPayments || []).find((item) => item.payment_id === id);
|
||||
return payment ? { ok: true, payment: clone(payment) } : { ok: false, error: "not_found" };
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/users") {
|
||||
const filtered = filterDemoUsers(params);
|
||||
const page = paged(filtered, params, 25);
|
||||
return {
|
||||
ok: true,
|
||||
users: clone(page.items),
|
||||
total: page.total,
|
||||
page: page.page,
|
||||
page_size: page.pageSize,
|
||||
};
|
||||
}
|
||||
if (cleanPath.startsWith("/admin/users/")) {
|
||||
const parts = cleanPath.split("/");
|
||||
const id = Number(parts[3]);
|
||||
const detail = DEMO_DATASET.adminUserDetails?.[String(id)];
|
||||
if (!detail) return { ok: false, error: "not_found" };
|
||||
if (parts[4]) {
|
||||
if (parts[4] === "telegram-profile-link") {
|
||||
return { ok: true, url: `https://t.me/${detail.user?.username || "demo_user"}` };
|
||||
}
|
||||
if (parts[4] === "message" && parts[5] === "preview") {
|
||||
return { ok: true, text: "Demo broadcast preview for the selected account." };
|
||||
}
|
||||
return { ok: true, user: clone(detail.user), detail: clone(detail) };
|
||||
}
|
||||
return clone(detail);
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/logs") {
|
||||
let logs = [...(DEMO_DATASET.adminLogs || [])];
|
||||
const userId = params.get("user_id");
|
||||
if (userId) {
|
||||
logs = logs.filter(
|
||||
(item) =>
|
||||
String(item.user_id || "") === userId || String(item.target_user_id || "") === userId
|
||||
);
|
||||
}
|
||||
const page = paged(logs, params, 50);
|
||||
return {
|
||||
ok: true,
|
||||
logs: clone(page.items),
|
||||
total: page.total,
|
||||
page: page.page,
|
||||
page_size: page.pageSize,
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/promos") {
|
||||
if (method === "POST") {
|
||||
const body = jsonBody(options);
|
||||
demoPromos().unshift({
|
||||
id: 3900 + demoPromos().length + 1,
|
||||
code: body.code || "DEMO",
|
||||
bonus_days: Number(body.bonus_days || 7),
|
||||
max_activations: Number(body.max_activations || 1),
|
||||
current_activations: 0,
|
||||
is_active: true,
|
||||
valid_until: new Date(Date.now() + Number(body.valid_days || 30) * 86400000).toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
created_by_admin_id: DEV_MOCK.data.user?.id || DEV_MOCK.data.user?.user_id,
|
||||
});
|
||||
return { ok: true, promo: clone(demoPromos()[0]) };
|
||||
}
|
||||
const page = paged(demoPromos(), params, 25);
|
||||
return {
|
||||
ok: true,
|
||||
promos: clone(page.items),
|
||||
total: page.total,
|
||||
page: page.page,
|
||||
page_size: page.pageSize,
|
||||
};
|
||||
}
|
||||
if (cleanPath.startsWith("/admin/promos/")) {
|
||||
const id = Number(cleanPath.split("/").pop());
|
||||
const promo = demoPromos().find((item) => item.id === id);
|
||||
if (!promo) return { ok: false, error: "not_found" };
|
||||
if (method === "DELETE") {
|
||||
demoPromosState = demoPromos().filter((item) => item.id !== id);
|
||||
return { ok: true };
|
||||
}
|
||||
Object.assign(promo, jsonBody(options));
|
||||
return { ok: true, promo: clone(promo) };
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/ads") {
|
||||
if (method === "POST") {
|
||||
const body = jsonBody(options);
|
||||
demoAds().unshift({
|
||||
id: 900 + demoAds().length + 1,
|
||||
source: body.source || "demo",
|
||||
start_param: body.start_param || "demo_campaign",
|
||||
cost: Number(body.cost || 0),
|
||||
is_active: true,
|
||||
created_at: new Date().toISOString(),
|
||||
stats: { users: 0, trial_activations: 0, payments: 0, revenue: 0 },
|
||||
});
|
||||
return { ok: true, campaign: clone(demoAds()[0]) };
|
||||
}
|
||||
return { ok: true, campaigns: clone(demoAds()), totals: clone(DEMO_DATASET.adsTotals || {}) };
|
||||
}
|
||||
if (cleanPath.startsWith("/admin/ads/")) {
|
||||
const parts = cleanPath.split("/");
|
||||
const id = Number(parts[3]);
|
||||
const campaign = demoAds().find((item) => item.id === id);
|
||||
if (!campaign) return { ok: false, error: "not_found" };
|
||||
if (parts[4] === "toggle") {
|
||||
campaign.is_active = !campaign.is_active;
|
||||
return { ok: true, campaign: clone(campaign) };
|
||||
}
|
||||
if (method === "DELETE") {
|
||||
demoAdsState = demoAds().filter((item) => item.id !== id);
|
||||
return { ok: true };
|
||||
}
|
||||
return { ok: true, campaign: clone(campaign) };
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/backups") return clone(DEMO_DATASET.backups);
|
||||
if (cleanPath === "/admin/backups/create") {
|
||||
const archive = clone(DEMO_DATASET.backups?.archives?.[0] || {});
|
||||
archive.name = `minishop-demo-${Date.now()}.zip`;
|
||||
archive.created_at = new Date().toISOString();
|
||||
return {
|
||||
ok: true,
|
||||
archive,
|
||||
result: { archive_name: archive.name, completed_at: archive.created_at, warnings: [] },
|
||||
};
|
||||
}
|
||||
if (cleanPath === "/admin/backups/upload") {
|
||||
return { ok: true, archive: clone(DEMO_DATASET.backups?.archives?.[0] || {}) };
|
||||
}
|
||||
if (cleanPath === "/admin/backups/restore") {
|
||||
return {
|
||||
ok: true,
|
||||
result: {
|
||||
archive_name: DEMO_DATASET.backups?.archives?.[0]?.name || "demo.zip",
|
||||
database_restored: true,
|
||||
warnings: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/settings" && method === "PATCH") {
|
||||
const body = jsonBody(options);
|
||||
for (const key of body.deletes || []) demoSettingsChanges.set(key, { deleted: true });
|
||||
for (const [key, value] of Object.entries(body.updates || {})) {
|
||||
demoSettingsChanges.set(key, { value, deleted: false });
|
||||
applyDemoSettingToMock(key, value);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
applied: Object.keys(body.updates || {}).length,
|
||||
reverted: (body.deletes || []).length,
|
||||
};
|
||||
}
|
||||
if (cleanPath === "/admin/settings") return { ok: true, sections: demoSettingsSections(clone) };
|
||||
|
||||
if (cleanPath === "/admin/tariffs") {
|
||||
if (method === "PUT") {
|
||||
const body = jsonBody(options);
|
||||
const catalog = body.catalog || body;
|
||||
demoTariffsState = defaultClone(catalog);
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
path: "data/tariffs.json",
|
||||
catalog: clone(demoTariffs()),
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/panel/internal-squads") {
|
||||
return {
|
||||
ok: true,
|
||||
squads: clone(
|
||||
DEMO_DATASET.panelSquads || [
|
||||
{ uuid: "db786ee8-816b-4760-80aa-1fc7a3669ff2", name: "Base RU" },
|
||||
{ uuid: "5f29045a-5e8b-4b06-a7b1-29abf0ad3a54", name: "Base EU" },
|
||||
{ uuid: "2f2f6e0a-1f2d-4e80-a33b-0ebf3a409012", name: "Premium EU" },
|
||||
]
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/translations" && method === "PATCH") {
|
||||
return { ok: true, applied: 1, reverted: 0, file_written: false };
|
||||
}
|
||||
if (cleanPath === "/admin/translations") {
|
||||
return { ok: true, ...clone(DEMO_DATASET.translations) };
|
||||
}
|
||||
|
||||
if (cleanPath === "/admin/support/stats") return { ok: true, stats: demoSupportCounts() };
|
||||
if (cleanPath === "/admin/support/tickets") {
|
||||
const tickets = filterDemoSupportTickets(demoSupportTickets(), params);
|
||||
const page = paged(tickets, params, 50);
|
||||
return { ok: true, tickets: clone(page.items), total: page.total };
|
||||
}
|
||||
if (cleanPath.startsWith("/admin/support/tickets/")) {
|
||||
const parts = cleanPath.split("/");
|
||||
const ticketId = Number(parts[4]);
|
||||
const ticket = demoSupportTickets().find((item) => item.ticket_id === ticketId);
|
||||
if (!ticket) return { ok: false, error: "not_found" };
|
||||
const messages = demoSupportMessages()[String(ticketId)] || [];
|
||||
if (parts[5] === "read") {
|
||||
ticket.unread_admin_count = 0;
|
||||
return { ok: true };
|
||||
}
|
||||
if (parts[5] === "messages") {
|
||||
const body = jsonBody(options);
|
||||
const message = {
|
||||
message_id: Date.now(),
|
||||
ticket_id: ticketId,
|
||||
author_role: "admin",
|
||||
author_user_id: DEV_MOCK.data.user?.id || DEV_MOCK.data.user?.user_id,
|
||||
author_name: "Поддержка",
|
||||
body: body.body || "",
|
||||
is_internal_note: Boolean(body.is_internal_note),
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages.push(message);
|
||||
demoSupportMessages()[String(ticketId)] = messages;
|
||||
ticket.last_message_at = message.created_at;
|
||||
ticket.last_message_role = "admin";
|
||||
ticket.status = "awaiting_user";
|
||||
return { ok: true, ticket: clone(ticket), message: clone(message) };
|
||||
}
|
||||
if (method === "PATCH") {
|
||||
Object.assign(ticket, jsonBody(options));
|
||||
return { ok: true, ticket: clone(ticket) };
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
ticket: clone(ticket),
|
||||
messages: clone(messages),
|
||||
user_snapshot: userSnapshotForTicket(ticket),
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/support/tickets" && method === "POST") {
|
||||
const body = jsonBody(options);
|
||||
const user = DEV_MOCK.data.user || {};
|
||||
const ticket = {
|
||||
ticket_id: 4900 + demoSupportTickets().length + 1,
|
||||
user_id: user.user_id || user.id,
|
||||
subject: body.subject || "Новое обращение в поддержку",
|
||||
category: body.category || "other",
|
||||
priority: body.priority || "normal",
|
||||
status: "awaiting_admin",
|
||||
unread_user_count: 0,
|
||||
unread_admin_count: 1,
|
||||
last_message_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString(),
|
||||
user,
|
||||
};
|
||||
demoSupportTickets().unshift(ticket);
|
||||
demoSupportMessages()[String(ticket.ticket_id)] = [
|
||||
{
|
||||
message_id: Date.now(),
|
||||
ticket_id: ticket.ticket_id,
|
||||
author_role: "user",
|
||||
author_user_id: ticket.user_id,
|
||||
author_name: userName(user) || user.username || "Демо-пользователь",
|
||||
body: body.body || "",
|
||||
created_at: ticket.created_at,
|
||||
},
|
||||
];
|
||||
return { ok: true, ticket: clone(ticket) };
|
||||
}
|
||||
if (cleanPath === "/support/tickets") {
|
||||
const tickets = filterDemoSupportTickets(demoSupportTickets(), params);
|
||||
const page = paged(tickets, params, 50);
|
||||
return {
|
||||
ok: true,
|
||||
tickets: clone(page.items),
|
||||
total: page.total,
|
||||
counts: demoSupportCounts(demoSupportTickets()),
|
||||
};
|
||||
}
|
||||
if (cleanPath.startsWith("/support/tickets/")) {
|
||||
const parts = cleanPath.split("/");
|
||||
const ticketId = Number(parts[3]);
|
||||
const ticket = demoSupportTickets().find((item) => item.ticket_id === ticketId);
|
||||
if (!ticket) return { ok: false, error: "not_found" };
|
||||
const messages = demoSupportMessages()[String(ticketId)] || [];
|
||||
if (parts[4] === "read") {
|
||||
ticket.unread_user_count = 0;
|
||||
return { ok: true };
|
||||
}
|
||||
if (parts[4] === "messages") {
|
||||
const body = jsonBody(options);
|
||||
const user = DEV_MOCK.data.user || {};
|
||||
const message = {
|
||||
message_id: Date.now(),
|
||||
ticket_id: ticketId,
|
||||
author_role: "user",
|
||||
author_user_id: user.user_id || user.id,
|
||||
author_name: userName(user) || user.username || "Демо-пользователь",
|
||||
body: body.body || "",
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
messages.push(message);
|
||||
demoSupportMessages()[String(ticketId)] = messages;
|
||||
ticket.last_message_at = message.created_at;
|
||||
ticket.last_message_role = "user";
|
||||
ticket.status = "awaiting_admin";
|
||||
return { ok: true, ticket: clone(ticket), message: clone(message) };
|
||||
}
|
||||
return { ok: true, ticket: clone(ticket), messages: clone(messages) };
|
||||
}
|
||||
if (cleanPath === "/support/unread") {
|
||||
return {
|
||||
ok: true,
|
||||
unread: demoSupportTickets().reduce(
|
||||
(sum, item) => sum + Number(item.unread_user_count || 0),
|
||||
0
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
if (cleanPath === "/account/language" && method === "POST") {
|
||||
const language = normalizeLangCode(jsonBody(options).language || currentLang);
|
||||
DEV_MOCK.data.user.language_code = language;
|
||||
DEV_MOCK.config.language = language;
|
||||
writeDemoLanguage(language);
|
||||
return { ok: true, language };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function mockApi(path, options = {}, context = {}) {
|
||||
const {
|
||||
currentLang = "ru",
|
||||
@@ -16,6 +629,12 @@ export async function mockApi(path, options = {}, context = {}) {
|
||||
} = context;
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 120));
|
||||
const cleanPath = String(path || "").split("?")[0];
|
||||
const demoResponse = demoApiResponse(path, cleanPath, options, {
|
||||
clone,
|
||||
currentLang,
|
||||
normalizeLangCode,
|
||||
});
|
||||
if (demoResponse !== undefined) return demoResponse;
|
||||
const adminUsers = [
|
||||
{
|
||||
user_id: 100200300,
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
import { DEMO_DATASET } from "./demoDataset.js";
|
||||
|
||||
const DEMO_LANGUAGE_STORAGE_KEY = "rw_minishop_demo_language";
|
||||
|
||||
function readStoredDemoLanguage() {
|
||||
if (typeof window === "undefined") return "";
|
||||
try {
|
||||
return window.localStorage?.getItem(DEMO_LANGUAGE_STORAGE_KEY) || "";
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
const WINDOWS_95_THEME = {
|
||||
key: "windows95",
|
||||
names: { ru: "Windows 95", en: "Windows 95" },
|
||||
@@ -426,6 +439,55 @@ export const DEV_MOCK = {
|
||||
},
|
||||
};
|
||||
|
||||
function applyDemoDataset() {
|
||||
const storedLanguage = readStoredDemoLanguage();
|
||||
const demoUser = {
|
||||
...(DEMO_DATASET.currentUser || {}),
|
||||
id: DEMO_DATASET.currentUser?.id ?? DEMO_DATASET.currentUser?.user_id,
|
||||
language_code: storedLanguage || DEMO_DATASET.currentUser?.language_code || "ru",
|
||||
};
|
||||
|
||||
Object.assign(DEV_MOCK.config, DEMO_DATASET.config || {});
|
||||
DEV_MOCK.config.language = demoUser.language_code || "ru";
|
||||
Object.assign(DEV_MOCK.data, {
|
||||
user: demoUser,
|
||||
subscription: DEMO_DATASET.currentSubscription || DEV_MOCK.data.subscription,
|
||||
devices: DEMO_DATASET.devices || DEV_MOCK.data.devices,
|
||||
plans: DEMO_DATASET.plans || DEV_MOCK.data.plans,
|
||||
payment_methods: DEMO_DATASET.paymentMethods || DEV_MOCK.data.payment_methods,
|
||||
referral: DEMO_DATASET.referral || DEV_MOCK.data.referral,
|
||||
tariff_change_options:
|
||||
DEMO_DATASET.tariff_change_options || DEV_MOCK.data.tariff_change_options,
|
||||
topup_options: DEMO_DATASET.topup_options || DEV_MOCK.data.topup_options,
|
||||
device_topup_options: DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options,
|
||||
settings: {
|
||||
...DEV_MOCK.data.settings,
|
||||
...(DEMO_DATASET.webappSettings || {}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
applyDemoDataset();
|
||||
|
||||
function applyDemoTariffScenario(subscriptionPatch = {}) {
|
||||
DEV_MOCK.data.subscription = {
|
||||
...DEV_MOCK.data.subscription,
|
||||
...(DEMO_DATASET.currentSubscription || {}),
|
||||
...subscriptionPatch,
|
||||
traffic_limit_strategy:
|
||||
subscriptionPatch.traffic_limit_strategy ||
|
||||
DEMO_DATASET.currentSubscription?.traffic_limit_strategy ||
|
||||
DEV_MOCK.data.subscription.traffic_limit_strategy ||
|
||||
"MONTH",
|
||||
};
|
||||
DEV_MOCK.data.plans = DEMO_DATASET.plans;
|
||||
DEV_MOCK.data.tariff_change_options =
|
||||
DEMO_DATASET.tariff_change_options || DEV_MOCK.data.tariff_change_options;
|
||||
DEV_MOCK.data.topup_options = DEMO_DATASET.topup_options || DEV_MOCK.data.topup_options;
|
||||
DEV_MOCK.data.device_topup_options =
|
||||
DEMO_DATASET.device_topup_options || DEV_MOCK.data.device_topup_options;
|
||||
}
|
||||
|
||||
export function applyPreviewMock(kind) {
|
||||
const mode = String(kind || "")
|
||||
.trim()
|
||||
@@ -506,6 +568,10 @@ export function applyPreviewMock(kind) {
|
||||
];
|
||||
} else if (mode === "tariffs") {
|
||||
DEV_MOCK.data.settings.traffic_mode = false;
|
||||
if (DEMO_DATASET.plans?.length) {
|
||||
applyDemoTariffScenario();
|
||||
return;
|
||||
}
|
||||
DEV_MOCK.data.subscription = {
|
||||
...DEV_MOCK.data.subscription,
|
||||
tariff_key: "standard",
|
||||
@@ -519,57 +585,40 @@ export function applyPreviewMock(kind) {
|
||||
id: "standard:period:1",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
tariff_description: "100 GB каждый месяц",
|
||||
billing_model: "period",
|
||||
sale_mode: "subscription",
|
||||
months: 1,
|
||||
price: 150,
|
||||
currency: "RUB",
|
||||
title: "Стандарт",
|
||||
subtitle: "1 месяц",
|
||||
description: "100 GB каждый месяц",
|
||||
monthly_gb: 100,
|
||||
sale_mode: "subscription",
|
||||
},
|
||||
{
|
||||
id: "standard:period:3",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
tariff_description: "100 GB каждый месяц",
|
||||
billing_model: "period",
|
||||
sale_mode: "subscription",
|
||||
months: 3,
|
||||
price: 400,
|
||||
currency: "RUB",
|
||||
title: "Стандарт",
|
||||
subtitle: "3 месяца",
|
||||
description: "100 GB каждый месяц",
|
||||
monthly_gb: 100,
|
||||
sale_mode: "subscription",
|
||||
},
|
||||
{
|
||||
id: "business:period:1",
|
||||
tariff_key: "business",
|
||||
tariff_name: "Бизнес",
|
||||
tariff_name: "Business",
|
||||
tariff_description: "500 GB и premium-серверы",
|
||||
billing_model: "period",
|
||||
sale_mode: "subscription",
|
||||
months: 1,
|
||||
price: 350,
|
||||
price: 690,
|
||||
currency: "RUB",
|
||||
title: "Бизнес",
|
||||
title: "Business",
|
||||
subtitle: "1 месяц",
|
||||
description: "300 GB и приоритетные серверы",
|
||||
monthly_gb: 300,
|
||||
},
|
||||
{
|
||||
id: "traffic:traffic:50",
|
||||
tariff_key: "traffic",
|
||||
tariff_name: "Трафик",
|
||||
billing_model: "traffic",
|
||||
sale_mode: "traffic_package",
|
||||
months: 50,
|
||||
traffic_gb: 50,
|
||||
price: 799,
|
||||
currency: "RUB",
|
||||
title: "Трафик",
|
||||
subtitle: "50 GB",
|
||||
description: "Пакет без срока действия",
|
||||
sale_mode: "subscription",
|
||||
},
|
||||
];
|
||||
DEV_MOCK.data.tariff_change_options = {
|
||||
@@ -579,44 +628,31 @@ export function applyPreviewMock(kind) {
|
||||
title: "Стандарт",
|
||||
description: "100 GB каждый месяц",
|
||||
billing_model: "period",
|
||||
monthly_gb: 100,
|
||||
expires_at: "31.05.2026",
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
tariff_key: "business",
|
||||
title: "Бизнес",
|
||||
description: "300 GB и приоритетные серверы",
|
||||
title: "Business",
|
||||
description: "500 GB и premium-серверы",
|
||||
billing_model: "period",
|
||||
monthly_gb: 300,
|
||||
monthly_gb: 500,
|
||||
price: 690,
|
||||
currency: "RUB",
|
||||
actions: [
|
||||
{
|
||||
mode: "recalc_days",
|
||||
kind: "free",
|
||||
title: "recalc_days",
|
||||
days_after: 10,
|
||||
remaining_days: 25,
|
||||
},
|
||||
{ mode: "paid_diff", kind: "payment", title: "paid_diff", price: 190, currency: "RUB" },
|
||||
],
|
||||
},
|
||||
{
|
||||
tariff_key: "traffic",
|
||||
title: "Трафик",
|
||||
description: "Пакеты без срока действия",
|
||||
billing_model: "traffic",
|
||||
actions: [
|
||||
{
|
||||
mode: "convert_days_to_gb",
|
||||
kind: "free",
|
||||
title: "convert_days_to_gb",
|
||||
converted_gb: 18,
|
||||
remaining_days: 25,
|
||||
title: "Пересчитать дни",
|
||||
days_after: 12,
|
||||
remaining_days: 28,
|
||||
},
|
||||
{
|
||||
mode: "buy_package",
|
||||
mode: "paid_diff",
|
||||
kind: "payment",
|
||||
title: "+50 GB",
|
||||
traffic_gb: 50,
|
||||
price: 799,
|
||||
title: "Доплатить разницу",
|
||||
price: 240,
|
||||
currency: "RUB",
|
||||
},
|
||||
],
|
||||
@@ -625,10 +661,29 @@ export function applyPreviewMock(kind) {
|
||||
};
|
||||
DEV_MOCK.data.topup_options = {
|
||||
ok: true,
|
||||
topup_kind: "regular",
|
||||
traffic_mode: false,
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
traffic_percent: 86,
|
||||
warning_levels: [85, 90, 95],
|
||||
regular: {
|
||||
can_topup: true,
|
||||
monthly_limit_gb: 100,
|
||||
used_gb: 86,
|
||||
available_gb: 14,
|
||||
packages: [
|
||||
{ gb: 10, price: 99, currency: "RUB" },
|
||||
{ gb: 50, price: 399, currency: "RUB" },
|
||||
],
|
||||
},
|
||||
premium: {
|
||||
can_topup: true,
|
||||
monthly_limit_gb: 25,
|
||||
used_gb: 24,
|
||||
available_gb: 1,
|
||||
packages: [
|
||||
{ gb: 10, price: 190, currency: "RUB" },
|
||||
{ gb: 25, price: 390, currency: "RUB" },
|
||||
],
|
||||
},
|
||||
plans: [
|
||||
{
|
||||
id: "standard:topup:10",
|
||||
@@ -643,67 +698,53 @@ export function applyPreviewMock(kind) {
|
||||
subtitle: "Стандарт",
|
||||
},
|
||||
{
|
||||
id: "standard:topup:50",
|
||||
id: "standard:premium_topup:10",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
sale_mode: "topup",
|
||||
traffic_gb: 50,
|
||||
months: 50,
|
||||
price: 399,
|
||||
sale_mode: "premium_topup",
|
||||
traffic_gb: 10,
|
||||
months: 10,
|
||||
price: 190,
|
||||
currency: "RUB",
|
||||
title: "50 GB",
|
||||
subtitle: "Стандарт",
|
||||
},
|
||||
{
|
||||
id: "standard:topup:200",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
sale_mode: "topup",
|
||||
traffic_gb: 200,
|
||||
months: 200,
|
||||
price: 1299,
|
||||
currency: "RUB",
|
||||
title: "200 GB",
|
||||
title: "Premium 10 GB",
|
||||
subtitle: "Стандарт",
|
||||
},
|
||||
],
|
||||
};
|
||||
DEV_MOCK.data.device_topup_options = {
|
||||
ok: true,
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
current_limit: 5,
|
||||
extra_hwid_devices: 2,
|
||||
extra_hwid_devices_valid_until_text: "01.06.2026 12:00",
|
||||
renewal_available: false,
|
||||
renewal_recommended_count: 0,
|
||||
current_devices: 1,
|
||||
max_devices: 2,
|
||||
available_extra_devices: 3,
|
||||
packages: [
|
||||
{ count: 1, price: 120, currency: "RUB" },
|
||||
{ count: 3, price: 290, currency: "RUB" },
|
||||
],
|
||||
plans: [
|
||||
{
|
||||
id: "standard:hwid:1",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
sale_mode: "hwid_devices",
|
||||
device_count: 1,
|
||||
months: 1,
|
||||
price: 99,
|
||||
sale_mode: "hwid_device",
|
||||
purchased_hwid_devices: 1,
|
||||
price: 120,
|
||||
currency: "RUB",
|
||||
title: "+1",
|
||||
subtitle: "Стандарт",
|
||||
},
|
||||
{
|
||||
id: "standard:hwid:3",
|
||||
tariff_key: "standard",
|
||||
tariff_name: "Стандарт",
|
||||
sale_mode: "hwid_devices",
|
||||
device_count: 3,
|
||||
months: 3,
|
||||
price: 249,
|
||||
currency: "RUB",
|
||||
title: "+3",
|
||||
subtitle: "Стандарт",
|
||||
title: "+1 устройство",
|
||||
},
|
||||
],
|
||||
};
|
||||
} else if (mode === "depleted") {
|
||||
DEV_MOCK.data.settings.traffic_mode = false;
|
||||
DEV_MOCK.data.settings.trial_available = false;
|
||||
if (DEMO_DATASET.plans?.length) {
|
||||
const limitBytes = Number(DEMO_DATASET.currentSubscription?.traffic_limit_bytes || 0);
|
||||
applyDemoTariffScenario({
|
||||
traffic_used: DEMO_DATASET.currentSubscription?.traffic_limit || "150 GB",
|
||||
traffic_used_bytes: limitBytes,
|
||||
});
|
||||
return;
|
||||
}
|
||||
return;
|
||||
} else if (mode === "devices") {
|
||||
DEV_MOCK.data.settings.my_devices_enabled = true;
|
||||
DEV_MOCK.data.subscription = {
|
||||
|
||||
Reference in New Issue
Block a user