SONR Music Multisport revenue - Only for Product Product + (-Discounts)
0
SONR Music Multisport q-ty
$0.00
Shipping
$0.00
Discount on Shipping (Free shipping promo)
$0.00
Tax (Not included in Gross Sales)
$0.00
Discounts on SONR Music Multisport
$0.00
Accessories revenue from SONR Music Multisport order
0
Accessories q-ty
0% fulfilled
0 fulfilled0 unfulfilled
4
SONR Music Mix
Orders containing items from two or more main product types (SONR Music · SONR Music 2 · SONR Music Multisport). These orders are excluded from Blocks 1–3 to avoid double-counting.
Gross Revenue
$0.00
Total customer paid · multi-type orders
0
Orders
$0.00
Products revenue
0
Products q-ty
$0.00
Shipping
$0.00
Discount on Shipping (Free shipping promo)
$0.00
Tax (Not included in Gross Sales)
$0.00
Discounts on Mix
$0.00
Accessories revenue from Mix order
0
Accessories q-ty
0% fulfilled
0 fulfilled0 unfulfilled
5
SONR Accessories
Order breakdown for orders containing Accessories SKUs. Data is based on the selected date range above.
Gross Revenue
$0.00
Total customer paid · orders containing Accessories
0
Orders
$0.00
Accessories revenue
0
Accessories q-ty
$0.00
Shipping
$0.00
Discount on Shipping (Free shipping promo)
$0.00
Tax (Not included in Gross Sales)
$0.00
Discounts on Accessories
0% fulfilled
0 fulfilled0 unfulfilled
6
B2B
SONR Music orders manually flagged Channel = B2B on the Orders Channels tab. Excluded from blocks 1–5. Metrics count Music products + accompanying Accessories (shipping/tax/discounts prorated).
Gross Revenue
$0.00
B2B orders · prorated shipping/tax/discount
0
Orders
$0.00
Product revenue Product + (-Discounts)
0
Product q-ty
$0.00
Shipping
$0.00
Discount on Shipping (Free shipping promo)
$0.00
Tax (Not included in Gross Sales)
$0.00
Discounts on B2B
$0.00
Accessories revenue from B2B order
0
Accessories q-ty
0% fulfilled
0 fulfilled0 unfulfilled
7
Other Orders
Orders that did not match any product type in blocks 1–5. May contain untagged SKUs.
8
Products Webflow SONR Coach
Coach-storefront orders flagged as Products In Order = SONR Music on the Coach → Orders Channels tab. Metrics & SKU breakdown count only Music items (and accessories that came along). PIO=Mix orders are excluded.
Gross Revenue
$0.00
Music-items only · prorated shipping/tax/discount
0
Orders
$0.00
SONR Music revenue
0
SONR Music q-ty
$0.00
Shipping
$0.00
Discount on Shipping (Free shipping promo)
$0.00
Tax (Not included in Gross Sales)
$0.00
Discounts on SONR Music
$0.00
Accessories revenue from SONR Music order
0
Accessories q-ty
0% fulfilled
0 fulfilled0 unfulfilled
9
SONR Orders export
Download a CSV report of Webflow SONR Music orders for a custom date range and selected product types.
Product types
1
B2B Orders ▾
SONR Music orders with Channel = B2B — 0 orders. They appear in the B2B block on Overview. Change the dropdown to move an order back to Auto. Click a row for SKU details.
2
Auto Orders ▾
SONR Music orders with Channel = Auto (default) — 0 orders. Split by product into blocks 1–5 on Overview. Set Channel = B2B to move an order into the B2B block. Click a row for SKU details.
3
From Webflow SONR Coach ▸
Coach-storefront orders flagged Products In Order = SONR Music on the Coach → Orders Channels tab — 0 orders. Shown here for visibility (marked “from Webflow SONR Coach”); the channel is set on the Coach page. They appear in the “Products Webflow SONR Coach” block on Overview. Click to expand.
0
Total Orders
0
Fulfilled
0%
0
Unfulfilled
0%
$0.00
Total Revenue
8
SONR Orders export
Download a CSV report of Coach orders for a custom date range and selected channels.
Channels
1
Unassigned Orders
Coach orders without a channel assigned. Select a channel in the dropdown — it will be saved to order_type and moved to the Assigned block below. Click a row to view SKU details.
0 orders
no pending changes
2
Assigned Orders Coach ▸
Coach orders that already have a channel — 0 orders. Click to expand.
3
Assigned Orders SONR Music ▸
Coach orders with Products In Order = SONR Music — 0 orders. Click to expand.
4
Assigned Orders SONR Mix ▸
Coach orders with Products In Order = Mix — 0 orders. Click to expand.
0
Total Orders
0
Fulfilled
0%
0
Unfulfilled
0%
$0.00
Total Revenue
7
SONR Orders export
Download a CSV report of Shopify orders for a custom date range and selected product types.
Product types
7
SONR Orders export
Download a CSV report of Amazon orders for a custom date range and selected product types.
Product types
SONR Orders
Sync
Manage data synchronization from Webflow, Shopify, and Amazon
0
Total Orders
0
Fulfilled
0%
0
Unfulfilled
0%
$0.00
Total Revenue
SONR Music
Revenue$0.00
Orders0
0% fulfilled
0 fulfilled0 unfulfilled
SONR Coach
Revenue$0.00
Orders0
0% fulfilled
0 fulfilled0 unfulfilled
SONR Shopify
Revenue$0.00
Orders0
0% fulfilled
0 fulfilled0 unfulfilled
SONR Amazon
Revenue$0.00
Orders0
0% fulfilled
0 fulfilled0 unfulfilled
Loading orders...
Order ID
Customer
Date
Status
Items
Total
📦
No orders found. Import from Webflow first.
SONR Music
Webflow Website
1. Quick Sync
Fetch latest orders from Webflow, transform and update orders.json
2. Auto Sync
Server-side background sync
3. Sync Products
Fetch products from Webflow and save to products.json
SONR Coach
Webflow Website
1. Quick Sync
Fetch latest orders from Webflow, transform and update orders.json
2. Auto Sync
Server-side background sync
3. Sync Products
Fetch products from Webflow and save to products.json
Fetch raw orders from Amazon SP-API into raw_amazon.json
2. Quick Sync
Fetch latest orders from Amazon, transform and update orders.json
3. Auto Sync
Server-side background sync
4. Sync Products
Fetch products from Amazon and save to raw_products (source: Amazon)
Product scope
Source
SONR Orders
Statistics
SONR Music · Webflow · Sales reports for the selected period
—
0
Orders in Range
$0
Total Revenue
$0
Average Order
0
Avg Items / Order
1
Reports
Analytics by product, color, status, country and more
Select order Status:
Product
Orders
Units
Revenue
Status
Orders
Items
%
Revenue
Country
Code
Orders
%
Items
Revenue
Shipping Method
Orders
%
Items
Total Shipping Cost
Avg Shipping
Payment
Orders
%
Items
Revenue
SONR Orders
OKR
Objectives & Key Results — coming soon
Страница в разработке.
SONR Orders
Notifications
Daily Telegram reports to group chats — every day at 9:00 AM GMT+2
SONR Orders
Settings
Manage users, countries & regions, and product catalog
Exchange Rates
Курс задаётся как 1 USD = курс × валюта (например, 1 USD = 17.5 MXN). Базовая валюта всегда USD.
Users
Username
Password
Name
Email
Phone
Role
Add New User
Create a new user account. Username and password are required. The user will be able to log in immediately after creation.
Unique login identifier
Login password
Display name (optional)
Contact email (optional)
Contact phone (optional)
Admin: full access. User: read-only orders
Countries & Regions
Manage shipping regions and their countries. Click a region name to rename it.
Sync from Webflow
Reads raw_products.json and imports products with source Webflow SONR Music into products.json.
New products are added, existing ones are updated. Your category assignments (skuid_product_type) are preserved.
Sync from Webflow — SONR Coach
Reads raw_products and imports products with source Webflow SONR Coach into products.
New products are added, existing ones are updated.
Sync from Shopify
Reads raw_products and imports products with source Shopify into products.
New products are added, existing ones are updated.
Sync from Amazon
Reads raw_products and imports products with source Amazon into products.
New products are added, existing ones are updated. Your skuid_product_type assignments are preserved.
Products
SONR Orders
Wiki
Документация по страницам и фильтрам
≡
Оглавление
Все статьи документации SONR Orders
1
SONR Database Structure
Структура MySQL базы данных — таблицы orders и app_data, поля, логика хранения, API endpoints и примеры данных.
2a
Dashboard · Webflow SONR Music
Аналитика продаж SONR Music Shop — источники данных, KPI-карточки, sub-tabs Overview / Orders Channels, секции по продуктам (Music, Music 2, Multisport, Accessories, B2B, Other, Coach-PIO=Music) + CSV-экспорт.
2b
Dashboard · Webflow SONR Coach
Сводная аналитика по заказам SONR Coach Shop — 4 KPI-карточки: Total Orders, Fulfilled, Unfulfilled, Total Revenue за выбранный период.
3
Sync
Управление синхронизацией данных из Webflow, Shopify и Amazon. Вкладки: Orders (таблица заказов), Webflow, Shopify, Amazon.
4
Notifications
Ежедневные Telegram-отчёты в групповые чаты — настройка бота, списка чатов и тест отправки.
5
Statistics
Аналитические отчёты за выбранный период — по продуктам, цвету, статусу, стране, методу доставки и оплате. KPI карточки с агрегатами.
6
OKR
Objectives & Key Results — страница в разработке. Пункт меню после Statistics.
5
Settings
Управление пользователями, регионами и каталогом SKU-продуктов. Три вкладки: Users, Countries & Regions, Products Settings.
1
SONR Database Structure
Структура MySQL базы данных — таблицы, поля, логика хранения и примеры данных.
Обзор
База данных MySQL 8.0 содержит две таблицы:
Таблица
Назначение
orders
Все заказы из всех источников (Webflow, Shopify, Amazon). Каждая строка — один заказ.
app_data
Key-value хранилище для конфигурации приложения: пользователи, каталог продуктов, регионы.
Подключение настраивается через env-переменные. Локально — Docker Compose (docker-compose.yml). На Railway — встроенный MySQL plugin с переменными MYSQLHOST, MYSQLPORT, MYSQLUSER, MYSQLPASSWORD, MYSQLDATABASE. Поддерживается также MYSQL_URL / DATABASE_URL как connection string.
Таблица orders
Хранит все заказы. Индексированные колонки позволяют быстро фильтровать по источнику, статусу и дате. Полный JSON объекта заказа хранится в колонке data.
Колонка
Тип
Описание
order_id
VARCHAR(255) PK
Уникальный идентификатор заказа. Совпадает с orderId в JSON. Primary key — защищает от дублей при upsert.
status
VARCHAR(50)
Статус заказа: fulfilled / unfulfilled. Индексируется для быстрой фильтрации.
Дата принятия заказа в формате ISO 8601, обрезанная до 19 символов (YYYY-MM-DDTHH:MM:SS). Индексируется для фильтрации по периоду.
order_products_in_type
VARCHAR(20)
Тип продуктового состава заказа. Допустимые значения: "SONR Coach", "SONR Music", "Mix". Заполняется приложением (не upstream-синком), сохраняется при ресинке через PRESERVED_ORDER_FIELDS. Индексируется.
data
LONGTEXT
Полный JSON объекта заказа. Все поля, используемые в интерфейсе, читаются отсюда.
Структура JSON объекта заказа (data)
Поле
Тип
Описание
orderId
string
Уникальный ID заказа. Совпадает с order_id в таблице.
status
string
"fulfilled" или "unfulfilled"
webflow
string
Источник заказа
shop
string
Название магазина
acceptedOn
ISO 8601
Дата и время заказа в UTC
customerPaid.value
integer (cents)
Сумма оплаченная клиентом в центах. 499 = $4.99
total
integer (cents)
Fallback для customerPaid.value (используется для Amazon/Shopify заказов)
subtotal
integer (cents)
Сумма позиций после скидок. Может быть меньше суммы item.price × count если скидка встроена напрямую
customer.name
string
Имя покупателя
customer.email
string
Email покупателя
items[]
array
Список позиций заказа (см. ниже)
extras[]
array
Доставка, скидки, налоги (см. ниже)
shipping
object
Адрес доставки, провайдер, трекинг
Структура items[]
Поле
Тип
Описание
variantId
string
ID варианта SKU. Используется как ключ связки с app_data['products']
product
string
Название продукта, напр. "SONR Music x2"
variant
string
Название варианта, напр. "Color: Coral"
price
integer (cents)
Цена за единицу
count
integer
Количество единиц позиции
variantImage.url
string
URL изображения варианта (показывается в SKU списке Dashboard)
Структура extras[]
type
Знак
Описание
"shipping"
+
Стоимость доставки
"discount-shipping"
−
Скидка на доставку (отрицательное значение)
"tax"
+
Налог (всегда положительный)
"discount"
−
Явная скидка на заказ (отрицательное значение)
Пример строки таблицы orders
SQL row example
order_id : "abc123xyz"
status : "fulfilled"
webflow : "SONR Music"
shop : "SONR Music Shop"
accepted_on: "2026-03-15T10:42:00"
data : {"orderId":"abc123xyz","status":"fulfilled","webflow":"SONR Music",
"acceptedOn":"2026-03-15T10:42:00Z","customerPaid":{"value":19900},
"items":[{"variantId":"abc","product":"SONR Music x2",
"variant":"Color: Coral","price":16800,"count":1}],
"extras":[{"type":"shipping","price":3100}],...}
Таблица app_data
Key-value хранилище. Ключ — строка, значение — JSON сериализованный в TEXT. Все операции через ON DUPLICATE KEY UPDATE — безопасный upsert.
Колонка
Тип
Описание
key_name
VARCHAR(100) PK
Имя ключа. Primary key — один ключ одна запись.
value
LONGTEXT
JSON значение. Читается и пишется через JSON.parse / JSON.stringify.
Ключи app_data
key_name
Структура значения
Назначение
users
[{ id, name, email, phone, username, password, role }]
Список пользователей. role: "admin" или "user". Пароли хранятся как есть (без хэширования). Управляется через Settings → User Management.
Каталог продуктов с пользовательскими настройками. skuid_product_type — тип SKU для классификации в Dashboard. skuid_product_color — цвет варианта (Blue/Yellow/Graphite/Coral/Black). skuid_main_product_qty — множитель количества (x2=2, x3=3). Настраивается в Settings → Products Settings.
raw_products
[{ id, name, skus: [{ skuId, ... }] }]
Сырые данные продуктов из Webflow API. Обновляется при "Sync Webflow SONR Music products". Не содержит пользовательских настроек — служит источником для обновления products.
regions
[{ name, countries: [] }]
Список регионов и стран. Используется на странице Statistics для группировки заказов по географии. Если ключ отсутствует при старте — инициализируется дефолтными значениями из server.js.
Пример строки таблицы app_data
SQL row example — ключ regions
key_name: "regions"
value : [{"name":"Europe","countries":["Austria","Belgium","Bulgaria",...]},
{"name":"North America","countries":["United States","Mexico",...]},
...]
Операции с базой данных
Все операции реализованы в db.js через пул соединений (mysql2/promise, лимит 10 соединений).
Функция
Описание
getPool()
Создаёт пул при первом вызове, инициализирует схему таблиц. Используется во всех остальных функциях.
getAllOrders()
Возвращает все заказы из таблицы orders, отсортированные по accepted_on DESC. Парсит data JSON каждой строки.
getOrderCount()
Возвращает количество заказов в таблице. Используется при старте сервера для логирования.
upsertOrders(orders[])
Вставляет или обновляет массив заказов в транзакции. affectedRows=1 → новый заказ, =2 → обновлён существующий. Возвращает { added, updated }.
getData(key)
Читает значение из app_data по ключу. Возвращает распарсенный объект или null.
setData(key, value)
Записывает значение в app_data (upsert). Сериализует через JSON.stringify.
migrateFromFiles(dataDir)
Первый запуск: если таблицы пустые — импортирует данные из JSON файлов в data/. Запускается однократно при старте сервера.
Endpoints API
Endpoint
Метод
Описание
/api/orders
GET
Возвращает все заказы из DB как JSON массив. Основной источник данных для всех страниц.
/data/orders.json
GET
Алиас для /api/orders — для обратной совместимости.
/products
GET
Возвращает app_data['products'].
/save-orders
POST
Принимает массив заказов, выполняет upsert через db.upsertOrders().
/users
GET
Возвращает app_data['users'].
/save-users
POST
Сохраняет массив пользователей в app_data['users'].
2
Dashboard
Страница аналитики продаж. Содержит две вкладки: Webflow SONR Music и Webflow SONR Coach.
Структура страницы
В шапке — hero-баннер с выбором периода (From / To + кнопка Apply). Под баннером — переключатель вкладок. Apply применяет выбранный период к активной вкладке.
Вкладка
Источник данных
Содержимое
Webflow SONR Music
webflow === "SONR Music" & shop === "SONR Music Shop" + одна доп. секция из Coach orders с order_products_in_type === "SONR Music"
KPI-карточки + 6 секций по продуктам + SKU-детализация + CSV-экспорт
Дата заказа — фильтрация по периоду (берётся slice 0–10, UTC)
webflow
string
Источник — фильтр "SONR Music"
shop
string
Магазин — фильтр "SONR Music Shop"
status
string
Статус заказа — fulfilled / unfulfilled
customerPaid.value
integer (cents)
Сумма оплаты клиентом — Total Revenue, Gross Revenue
items[].variantId
string
ID варианта SKU — связка с каталогом продуктов в DB
items[].price
integer (cents)
Цена позиции (line total) — SONR Music revenue, Accessories revenue
items[].count
integer
Количество единиц позиции
extras[].type
string
Тип позиции — "shipping", "discount-shipping", "tax", "discount"
extras[].price
integer (cents)
Стоимость позиции. Скидки отрицательные. Налоги всегда положительные
subtotal
integer (cents)
Сумма позиций после скидок
MySQL · app_data['products']
Каталог продуктов с пользовательскими настройками. Загружается через GET /products при старте страницы. Используется для классификации SKU в заказах.
Поле JSON
Тип
Где используется
skus[].skuId
string
Совпадает с items[].variantId в заказе — ключ связки
skus[].skuid_product_type
string
Тип SKU — фильтр секций и группировка
skus[].skuid_main_product_qty
integer
Количество устройств в одном SKU — множитель для подсчёта Units
Настраивается на странице Settings → Products Settings.
Выбор периода
Поля From и To в hero-баннере. По умолчанию — 2026-03-01 → 2026-03-31. После изменения нажмите Apply — все блоки пересчитываются.
Фильтрация по UTC-дате: из acceptedOn берётся slice(0, 10). Базовый фильтр: webflow === "SONR Music" и shop === "SONR Music Shop".
KPI-карточки
Четыре карточки — сводка по всем заказам SONR Music Shop за период.
Карточка
Логика расчёта
Total Orders
Количество заказов за период
Fulfilled
Заказы со status === "fulfilled" + % от Total Orders
Unfulfilled
Остальные заказы + % от Total Orders
Total Revenue
Сумма customerPaid.value — включает товары, доставку и налоги
Расчёт блоков 1–4 · item-by-item с пропорциональным распределением
Блоки SONR Music, SONR Music 2, SONR Music Multisport, SONR Accessories используют единую формулу, реализованную в computeMusicBlockMetrics(orders, mainType):
Quaiifying orders — заказы где есть хотя бы один SKU нужного типа (Music / Music 2 / Multisport / Accessories соответственно)
In-block items для блоков 1–3 = items типа mainType + items типа "Accessories". Для блока 4 (Mix) — items любого из 3 main-типов + "Accessories". Для блока 5 — только "Accessories"
Block share для каждого заказа = (Σ price in-block items) / (Σ price всех items). Если все items в заказе принадлежат блоку — share=1, иначе меньше
Product / Accessories revenue, q-ty — только in-block items
Shipping / Discount Shipping / Tax / Discounts — соответствующий extras[].price × share для каждого заказа
«{type} revenue» (нетто) = productRevenueNet = productRevenue + discount + discountShipping — выручка товара минус прораченные на блок скидка и скидка на доставку (обе отрицательные). Именно это значение показывает плитка «SONR Music revenue» и аналоги; отдельные плитки Discount / Discount Shipping остаются для справки
Заказы с двумя и более main-типами (например SONR Music + SONR Music Multisport) НЕ попадают в Блоки 1–3 — они идут только в Block 4 (Mix). Items без типа в каталоге попадут в Block 6 (Other Orders).
Секция 1 · Product SONR Music
Включает заказы с хотя бы одним SKU типа "SONR Music". Связь: items[].variantId = skus[].skuId в каталоге.
Метрика
Логика расчёта
Gross Revenue
Product + Accessories + Shipping + (−Discounts) + (−Discount Shipping) — см. формулу выше
Orders
Количество уникальных квалифицированных заказов
SONR Music revenue
Сумма item.price для позиций с типом "SONR Music"минус блочные Discount и Discount Shipping (нетто после скидок). Discount всё ещё показан отдельной плиткой.
SONR Music q-ty
Сумма skuid_main_product_qty × item.count
Shipping
Σ extras[].price × share где type === "shipping" (gross, без discount-shipping)
Discount Shipping
Σ extras[].price × share где type === "discount-shipping" (отрицательное, учтено в Gross Revenue)
Tax
Σ extras[].price × share где type === "tax"
Discounts
Σ extras[].price × share где type === "discount"
Accessories revenue
Сумма item.price для items типа "Accessories"
Accessories q-ty
Сумма item.count для accessories
Прогресс-бар
Процент fulfilled. Справа — абсолютные значения fulfilled и unfulfilled.
Подписи тайлов на странице (UI)
Подписи метрик на странице уточнены, чтобы было понятно, что входит в каждое число. Применяются ко всем 6 блокам Webflow SONR Music — продукт в подписи меняется по блоку (SONR Music / SONR Music 2 / Multisport / Mix / Accessories / Coach-Music).
Полная сумма, что заплатил клиент, без налога (Tax в Gross Revenue не входит)
{Product} revenue
«{Product} revenue - Only for Product» «Product + (-Discounts)»
Выручка только товара (без аксессуаров). Подпись упрощена — фактически значение также за вычетом прораченного Discount Shipping (productRevenueNet = Product + discount + discountShipping)
Discount on Shipping
Discount on Shipping (Free shipping promo)
extras[type="discount-shipping"] — скидка на доставку / промо бесплатной доставки, прораченная на блок
Tax
Tax (Not included in Gross Sales)
Налог; в Gross Revenue не включён
Discounts
Discounts on {Product}
Промо-скидка extras[type="discount"], прораченная на блок
Accessories revenue
Accessories revenue from {Product} order
Выручка аксессуаров, проданных в заказах с этим продуктом
Блок 5 (Accessories) — тайл «Accessories revenue» там показывает основную выручку блока (продукт = аксессуары), поэтому без «from … order».
Секция 2 · Product SONR Music 2
Заказы с хотя бы одним SKU типа "SONR Music 2". Логика метрик идентична Секции 1. SKU x2/x3 имеют skuid_main_product_qty = 2 или 3.
Метрика
Логика расчёта
Gross Revenue
Product + Accessories + Shipping + (−Discounts) + (−Discount Shipping) — см. формулу выше
Orders
Количество уникальных заказов
SONR Music 2 revenue
Сумма item.price для типа "SONR Music 2"минус блочные Discount и Discount Shipping (нетто после скидок).
Заказы с хотя бы одним SKU типа "SONR Music Multisport". Логика идентична Секциям 1 и 2 — в т.ч. tile SONR Music Multisport revenue показывает Σ item.price минус блочные Discount и Discount Shipping.
Mix-исключение: если заказ одновременно содержит items нескольких main-типов (например SONR Music + SONR Music Multisport), он исключается из Блоков 1–3 и попадает только в новый Block 4 "SONR Music Mix". Это применяется к Блокам 1, 2, 3 одинаково.
Секция 4 · SONR Music Mix
Заказы, в которых одновременно представлены SKU двух или более main-типов из множества {"SONR Music", "SONR Music 2", "SONR Music Multisport"}. Цель — иметь отдельный счёт для cross-product покупок, не "размазывая" их по блокам 1–3.
Фильтр:
distinct main-types in items[] ≥ 2
&& (acceptedOn в выбранном диапазоне)
Реализован в [app.js renderDashboard] через хелпер isMusicMixOrder(o). Эта же функция используется как negative-фильтр для блоков 1–3.
Метрики: вызов computeMusicBlockMetrics(mixOrders, ["SONR Music", "SONR Music 2", "SONR Music Multisport"]) — функция принимает массив типов и трактует все их как "main". productRevenue = Σ item.price для items любого из 3 main-типов. productQty учитывает skuid_main_product_qty как обычно. Tile "Products revenue" показывает productRevenue минус блочные Discount и Discount Shipping (нетто), плитка Discount остаётся.
Σ item.price для items любого main-типа минус блочные Discount и Discount Shipping
Products q-ty
Σ skuid_main_product_qty × item.count для main-items
Shipping / Discount Shipping / Tax / Discounts
Аналогично Секции 1, share = main+acc / total
Accessories revenue / q-ty
Σ item.price / item.count для Accessories items в mix-заказах
SKU breakdown: две группы — "SONR Music + Music 2 + Multisport (mix)" (предикат сматчит любой main-тип) и "SONR Accessories (from Music orders)" (если есть). Уникальные groupId = mix / accmix и offsets 11000/12000 чтобы не было коллизий с другими блоками.
Export-chip: в Block 8 (form) добавлен chip data-type="__mix__" с лейблом "Mix". musicOrderPrimaryType возвращает "__mix__" для заказов с distinct main-types ≥ 2 — этот код используется и в filtering, и в CSV-Type-колонке.
Секция 5 · SONR Accessories
Заказы с типом "Accessories" и без SONR Music / Music 2 / Multisport SKU — чисто аксессуарные заказы.
Сумма item.price для типа "Accessories"минус блочные Discount и Discount Shipping (нетто после скидок).
Accessories q-ty
Сумма item.count для accessories
Shipping / Discount Shipping / Tax / Discounts
Аналогично Секции 1
Секция 6 · B2B
Источник: Music-заказы, которым на под-вкладке Orders Channels вручную выставлен Channel = B2B (хранится в поле order_type === "B2B"). Такие заказы исключаются из блоков 1–5 (фильтры блоков работают по autoMusicOrders = musicShopOrders.filter(o => o.order_type !== "B2B")), чтобы не было двойного учёта, и показываются здесь отдельным блоком.
Метрики:computeMusicBlockMetrics(b2bOrders, ["SONR Music","SONR Music 2","SONR Music Multisport"]) — как у блока Mix: в Product revenue / q-ty идут все Music-товары, аксессуары — отдельным тайлом, order-level shipping/tax/discount/discount-shipping проратируются по доле in-block items. Тайлы и SKU-разбивка — те же, что у других блоков. KPI-карточки вверху страницы по-прежнему считают B2B-заказы (B2B — под-классификация, а не отдельный storefront).
Перевести заказ обратно в Auto — выбрать Channel = Auto в списке (поле order_type очищается).
Секция 7 · Other Orders
Все items из autoMusicOrders (Auto-заказы, без B2B), чей тип не входит в блоки 1–5 ("SONR Music", "SONR Music 2", "SONR Music Multisport", "Accessories"). Сюда попадают незатегованные SKU и SKU других линеек.
Зачем: это музыкальные продажи, которые физически прошли через Coach storefront. Видеть их хочется на Music-дашборде вместе с обычными Music-продажами.
Метрики: переиспользуют тот же computeMusicBlockMetrics(coachMusicOrders, "SONR Music") что и Секция 1 — то есть в Product Revenue / Product q-ty попадают только items с skuid_product_type === "SONR Music" (Coach- / Mix- / прочие SKU в этих заказах игнорируются). Tile Accessories revenue / q-ty учитывает аксессуары пришедшие в тех же заказах. Shipping / tax / discount / discount-shipping проратируются по доле Music-items в заказе.
PIO=Mix заказы намеренно исключены — попадают только заказы с PIO ровно равным "SONR Music".
SKU breakdown: две группы — "SONR Music (from Coach orders)" и "SONR Accessories (from Coach orders)". Уникальные groupId и offset чтобы не было коллизий с Секцией 1.
Секция 9 · SONR Orders export
CSV-экспорт по типу заказа. Диапазон дат берётся из top-of-page dashboard-хедера (dashDateFrom / dashDateTo) — отдельных date-input'ов в форме нет, чтобы пользоваться одним общим источником истины и не дублировать UI. Если в хедере не выбран период — кнопка возвращает status-ошибку.
Параметры формы:
Types — 6 chip-чекбоксов (SONR Music / Music 2 / Multisport / Accessories / Other / Coach (Music)) + мастер All
Источники заказов:
webflow="SONR Music" && shop="SONR Music Shop" — основная Music-storefront выборка (первые 5 типов)
webflow="SONR Coach" && shop="SONR Coach Shop" && order_products_in_type="SONR Music" — попадают в выборку только когда активен chip Coach (Music) (data-type=__coach_music__)
Тип заказа = синтетический __coach_music__ для Coach-PIO=Music, иначе тип первого counted item, иначе __other__. В CSV-колонке Type: "Coach (Music)" / "Other" / "SONR Music" и т.д. Колонки: Order ID, Date, Status, Type, Customer, Email, Items, Promo, Subtotal, Total, Shipping, Tax, Discount, Payment. Имя файла: sonr-music-orders-{from}-{to}.csv.
Блок SKU — структура
После каждой секции метрик — два сворачиваемых списка SKU (основной продукт + аксессуары из тех же заказов). Кликните на заголовок группы чтобы раскрыть, на строку SKU — чтобы развернуть таблицу заказов.
Колонка
Описание
Units
skuid_main_product_qty × item.count для основного продукта; item.count для аксессуаров
Tax
Сумма extras[type="tax"] из всех заказов SKU; — если нет
Revenue
Сумма item.price — line total, count не применяется
SKU summary row (7 tile'ов справа от названия): Units · Total paid · Price (product) · Discount · Disc. Shipping · Tax · Shipping. Каждое значение — сумма соответствующего поля по всем item-rows этого SKU (та же логика что у detail-rows). Discount/Disc. Shipping показывают "—" если сумма ≥ 0.
Таблица заказов (12 колонок): Order ID · Customer · Variant · Status · Date · Units · Total paid · Price (product) · Discount · Discount Shipping · Tax · Shipping. Семантика:
Units — item.count × (skuid_main_product_qty ?? 1)
Total paid — order-level customerPaid.value (одно значение на весь заказ — для всех item-rows одно)
Price (product) — item.price + prorated_discount (нетто после прорированной доли order-level discount по `item.price / Σ item.price`)
Tax / Shipping — order-level (Σ extras[type=tax] / Σ extras[type.includes(shipping)], последнее уже net — включает discount-shipping)
Tax / Shipping / Discount / Discount Shipping — — если 0 (или не отрицательные для скидок).
Под-вкладка: Orders Channels
Страница Music имеет два sub-tab'а (как Coach): Overview (KPI + блоки выше) и Orders Channels. Переключение — pill-tabs #music-sub-tabs (data-music-sub), функции рендера renderDashboard() (Overview) и renderMusicChannels() (Channels). Список фильтруется по тому же диапазону дат из dashboard-хедера.
Каждому Music-заказу назначается Channel одним dropdown'ом (одно поле, по аналогии с Coach):
Channel
Эффект на Overview
Auto (по умолчанию)
Заказ делится по продуктам в блоки 1–5, как раньше. Хранится как пустое order_type.
B2B
Заказ исключается из блоков 1–5 и попадает в блок B2B (Секция 6). Хранится как order_type === "B2B".
Сохранение — немедленно при выборе через POST /orders/set-order-type (то же поле и эндпоинт, что у каналов Coach; Music- и Coach-классификаторы не пересекаются, т.к. каждый фильтрует по webflow). Список разбит на сворачиваемые блоки: B2B Orders, Auto Orders (раскрыт по умолчанию — рабочий список), From Webflow SONR Coach.
From Webflow SONR Coach — Coach-storefront заказы с order_products_in_type === "SONR Music" (помеченные на Coach → Orders Channels). Показаны здесь для видимости с бейджем «from Webflow SONR Coach», только для просмотра — канал задаётся на Coach-странице (текущая логика блока «Products Webflow SONR Coach» на Overview сохранена). Клик по строке раскрывает SKU/детали заказа (переиспользуется renderCoachChDetails).
2b
Dashboard · Webflow SONR Coach
Аналитика продаж SONR Coach Shop — источники данных, KPI-сводка, 7 блоков по каналам, экспорт CSV, ручное назначение каналов.
Источники данных
MySQL · таблица orders
Основное хранилище заказов. Каждая строка — один заказ. Загружается через GET /api/orders. Колонка data содержит полный JSON объекта заказа.
Поля From и To в hero-баннере общие для обоих под-вкладок. По умолчанию — 2026-03-01 → 2026-03-31. После изменения нажмите Apply — все блоки и список Orders Channels пересчитываются.
Фильтрация по UTC-дате: acceptedOn.slice(0, 10). Базовый фильтр всех блоков: webflow === "SONR Coach" AND shop === "SONR Coach Shop".
Под-вкладки
Страница Coach разделена на 2 под-вкладки (pill-tabs) с общим date-range header'ом:
Sub-tab
Что показывает
Функция рендера
Overview
4 KPI-карточки + 8 блоков по каналам + CSV export
renderDashboardCoach()
Orders Channels
4 блока для ручного присвоения order_type и order_products_in_type
renderCoachChannels()
Как работает переключение: клик по pill-tab меняет CSS hidden у контейнеров #coach-sub-overview и #coach-sub-channels. При переключении на Overview вызывается renderDashboardCoach() — это перечитывает весь в-памяти массив orders и пересчитывает все метрики с текущим date-range. При переключении на Orders Channels вызывается renderCoachChannels() — та же дата-фильтрация, но список для ручного редактирования.
Общие данные: оба sub-tab работают над одним orders массивом (загруженным при старте страницы). Изменение канала через Orders Channels мутирует объект заказа в orders[] in-place (поле order_type) — при следующем переключении на Overview новая классификация отразится сразу без reload.
Sub-tab: Overview — KPI-карточки
Четыре карточки — сводка по всем Coach-заказам за период.
Карточка
Логика расчёта
Total Orders
Количество заказов с webflow === "SONR Coach" AND shop === "SONR Coach Shop" за период
Fulfilled
Заказы со status === "fulfilled" + % от Total Orders
Unfulfilled
Остальные заказы + % от Total Orders
Total Revenue
Σ customerPaid.value — включает товары, доставку и налоги
Sub-tab: Overview — Классификация в 8 блоков
Каждый Coach-заказ попадает ровно в один обычный блок (1–6 и 8). Блок 7 — агрегированный Combined view, заказы в нём не классифицируются отдельно. Алгоритм classifyCoachOrder(order):
Если всеitems в заказе имеют skuid_product_type === "Accessories" → блок 6 SONR Accessories (приоритет над order_type)
Иначе если order_type ∈ {"Direct", "Direct Customers", "B2B", "B2B Customers", "Dealers"} → соответствующий блок 1–5
Иначе → блок 8 Other Orders (нет order_type или значение вне 5 каналов)
Сумма заказов блоков 1, 2, 3, 4, 5, 6, 8 = Total Orders из KPI-карточек.
#
Блок
Условие попадания
Тип
1
Direct
order_type === "Direct", не все items — Accessories
канал
2
Direct Customers
order_type === "Direct Customers"
канал
3
B2B
order_type === "B2B"
канал
4
B2B Customers
order_type === "B2B Customers"
канал
5
Dealers
order_type === "Dealers"
канал
6
SONR Accessories
Все items типа "Accessories"
канал
7
Direct Customers + SONR Accessories
Read-only агрегация блоков 2 + 6
combined
8
Other Orders
Без order_type или значение вне 5 каналов
канал
Блок 7 — Combined view: read-only агрегация. def.combinedFrom = ["Direct Customers", "__accessories__"] — заказы из блоков 2 и 6 собираются через flatMap и считаются через тот же computeCoachBlockMetrics. Заказы остаются в исходных блоках 2 и 6. SKU-разбивка режим "all" — показывает все SKU (Coach + Accessories вместе). Tile "SONR Coach revenue only" во всех блоках (включая combined) показывает нетто coachOnlyRevenueNet.
Метрики каждого блока
В каждом блоке отображается единый набор тайлов (некоторые скрыты в блоках 6 и 7 — см. ниже). Расчёт реализован в computeCoachBlockMetrics(blockOrders).
Σ item.count × skuid_main_product_qty для всех items с типом ≠ "Accessories" (если skuid_main_product_qty не задан — берётся 1)
SONR Coach revenue only
Σ item.price где SKU имеет source === "Webflow SONR Coach" AND skuid_product_type === "SONR Coach", минус прораченные на блок Discount и Discount Shipping: coachOnlyRevenueNet = coachOnlyRevenue + discount + discountShipping (обе скидки отрицательные). Применяется во всех блоках. Discount и Discount Shipping показаны отдельными плитками.
SONR Receiver q-ty
Σ item.count × skuid_receivers_count для SKU с source === "Webflow SONR Coach"
SONR Radio q-ty
Σ item.count × skuid_radio_count для SKU с source === "Webflow SONR Coach"
Shipping
Σ extras[].price где type === "shipping" (gross, без discount-shipping)
Discount Shipping
Σ extras[].price где type === "discount-shipping" (отрицательное, учтено в Gross Revenue)
Tax
Σ extras[].price где type === "tax"
Discounts
Σ extras[].price где type === "discount"
Accessories revenue
Σ item.price для items с skuid_product_type === "Accessories"
SONR Coach revenue only, SONR Receiver q-ty, SONR Radio q-ty
Все остальные тайлы (Coach-only метрики бессмысленны для этого блока)
Прогресс-бар
Внизу каждого блока — процент fulfilled. Справа абсолютные значения fulfilled и unfulfilled.
SKU-разбивка
Под прогресс-баром — сворачиваемый список SKU из заказов блока. Для блока 6 SONR Accessories — только Accessory SKU. Для блоков 1–5 и 7 — non-Accessory SKU. Клик на строку SKU разворачивает список заказов с Order ID, Customer, Variant, Status, Date, Price.
SONR Orders export
Под 8 блоками — секция для выгрузки CSV-отчёта по Coach-заказам. Диапазон дат берётся из top-of-page dashboard-хедера (dashDateFrom / dashDateTo) — отдельных date-input'ов в форме нет, чтобы не дублировать UI и иметь один источник истины. Если в хедере не выбран период — кнопка возвращает status-ошибку.
Параметры формы
Channels — 7 chip-чекбоксов (Direct / Direct Customers / B2B / B2B Customers / Dealers / SONR Accessories / Other Orders) + мастер All
All — заполненный navy-чип, переключает все 7 chip'ов разом. Если отжать любой chip — All автоматически отжимается
Логика фильтра
Заказ попадает в выгрузку если: webflow === "SONR Coach" AND shop === "SONR Coach Shop" AND acceptedOn в диапазоне AND classifyCoachOrder(order) ∈ выбранные каналы.
CSV-колонки
Колонка
Источник
Order ID
orderId
Date
acceptedOn (форматированная)
Status
status
Channel
Результат classifyCoachOrder(order) (Direct / SONR Accessories / Other Orders / etc.)
Customer / Email
customer.name / customer.email
Items
Конкатенация "Product (variant) ×count" через "; "
Promo
Извлекается из extras[].name — формат "Discount (CODE)" → CODE
Subtotal / Total
В формате $X.XX
Shipping
Net shipping (Σ extras с type.includes("shipping"))
Tax / Discount
Σ соответствующих extras[]
Payment
order.payment
Имя файла: sonr-coach-orders-{from}-{to}.csv. Если фильтры не дали результатов — статус "No orders match the selected filters", файл не скачивается.
Sub-tab: Orders Channels
Позволяет вручную присвоить каждому Coach-заказу два ортогональных поля: Channel (order_type) и Products In Order (order_products_in_type). Функция рендера: renderCoachChannels(). Список фильтруется по дате из Dashboard-хедера (те же From/To что и в Overview).
Персистентность: оба поля входят в PRESERVED_ORDER_FIELDS — при Quick/Auto Sync с Webflow они автоматически re-attach'атся к incoming payload. Поля не перезаписываются синком.
Возможные значения order_type
Channel
Описание
Direct
Прямые продажи
Direct Customers
Прямые клиенты
B2B
B2B-продажи
B2B Customers
B2B-клиенты
Dealers
Дилеры
none
Канал явно не применим. В отличие от пустого значения — заказ помечается как "проверено и канала нет", уходит из Unassigned в Assigned Orders Coach. На Overview-вкладке такие заказы попадают в блок "Other Orders" (т.к. "none" не входит в COACH_KNOWN_CHANNELS).
Блок 1 · Unassigned Orders
Фильтр: !order_type && !order_products_in_type — заказы где не задано ни одно из двух полей. Колонки: Order ID · Date · Status · Customer · Promo · Amount · Channel · Products In Order. Promo извлекается из extras[].name (regex \(([^)]+)\)).
Batch-режим: правки накапливаются в pending state — строка подсвечивается тёплым жёлтым, в тулбаре появляется счётчик "N pending changes". Кнопка Submit changes сохраняет всё параллельно (Promise.all) через POST /orders/set-order-type и POST /orders/set-products-in-type. После submit вызывается renderCoachChannels() — заказы автоматически переезжают в нужные блоки 2/3/4.
Если задан Channel → заказ появится в Блоке 2. Если задан Products In Order (SONR Music или Mix) → в Блоке 3 или 4. Если задано оба — в обоих блоках одновременно (ортогональные классификации). Заказ исчезает из Блока 1 только когда заполнены оба поля или когда только первое поле становится отличным от пустого — по факту исчезает как только любое из двух полей установлено.
Блок 2 · Assigned Orders Coach
Фильтр: !!order_type — заказы с любым заданным каналом. Свёрнут по умолчанию. В dropdown можно изменить канал или сбросить ("— select —" → поле удаляется через db.setOrderField, заказ уходит обратно в Блок 1 если и PIO не задан). В отличие от Блока 1, изменение сохраняется немедленно при выборе значения в dropdown (нет Submit).
Блок 3 · Assigned Orders SONR Music
Фильтр: order_products_in_type === "SONR Music". Свёрнут по умолчанию. Заказ может одновременно быть в Блоке 2 (если задан order_type) и здесь — это независимые поля. Dropdown "Products In Order" тоже доступен здесь (немедленное сохранение).
Блок 4 · Assigned Orders SONR Mix
Фильтр: order_products_in_type === "Mix". Логика аналогична Блоку 3. Заказ с order_type + order_products_in_type = "Mix" присутствует в Блоках 2 и 4 одновременно.
Products In Order — допустимые значения
Value
Блок
Описание
SONR Coach
—
В заказе только Coach-продукты (отдельного блока пока нет)
SONR Music
Блок 3
В заказе только Music-продукты
Mix
Блок 4
В заказе и Coach, и Music продукты одновременно
Row expand · детали заказа
Клик по строке (вне dropdown) разворачивает панель с деталями:
Shipping — полный адрес (line1, city, state, postalCode, country), метод доставки
Promo code — извлекается из extras[].name (формат Discount (CODE))
Subtotal · Total — суммы по заказу
Extras — все записи extras[] (shipping / tax / discount / discount-shipping) с ценами; отрицательные — красным
Повторный клик на строку сворачивает панель. Доступно с клавиатуры: Enter / Space на сфокусированной строке.
API
Endpoint
Body
Эффект
POST /orders/set-order-type
{ orderId, order_type }
Surgical UPDATE поля order_type в JSON-data заказа. Пустое значение — JSON_REMOVE (поле удаляется). 404 если заказ не найден.
POST /orders/set-products-in-type
{ orderId, order_products_in_type }
Surgical UPDATE поля order_products_in_type + одноимённой физической колонки в одной транзакции. Допустимые значения: "SONR Coach", "SONR Music", "Mix" (валидация на сервере, иначе 400). Пустое значение — поле удаляется и колонка обнуляется. 404 если заказ не найден.
2c
Dashboard · Shopify
Аналитика продаж SONR Shopify Store — KPI, 5 блоков по типам продуктов + Other Orders + CSV-экспорт.
Источник данных
Заказы с webflow === "SONR Shopify" AND shop === "SONR Shopify Store". Дата заказа: acceptedOn.slice(0,10). По умолчанию открывается за текущий месяц.
Поле JSON
Тип
Заметка
orderId
string
Формат "shopify-{id}"
status
string
"fulfilled" / "pending"
acceptedOn
ISO 8601
Дата создания заказа
customerPaid.value / total
integer (cents)
Gross — Shopify заказы используют total (не customerPaid)
items[].variantId
string
Формат "shopify-{variant_id}" — связка со skus[].skuId в каталоге продуктов
items[].price
integer (cents)
Line total (per-unit × quantity) — единый формат с Webflow
items[].count
integer
Quantity
extras[]
array
Синтезируется из Shopify полей shipping_lines, total_tax, total_discounts в формат {type, name, price}
Важно: новые поля variantId и extras[] в Shopify заказах появились вместе с этой страницей. Существующие заказы получат их только после Quick Sync через Sync → Shopify → 2. Quick Sync.
KPI-карточки
Четыре карточки — сводка по всем Shopify-заказам за период.
Карточка
Логика
Total Orders
Количество заказов
Fulfilled
status === "fulfilled" + % от Total
Unfulfilled
Остальные + % от Total
Total Revenue
Σ customerPaid.value ?? total
Блоки 1–5 · по типу продукта
Каждый блок использует общий computeMusicBlockMetrics(orders, mainType) — тот же что для Music: per-item revenue + extras prorated by block share. Gross Revenue = Product + Accessories + Shipping + (−Discounts) + (−Discount Shipping) — Tax не включается. Плитка «{mainType} revenue» показывает нетто — productRevenueNet = productRevenue + discount + discountShipping (выручка товара минус прораченные на блок скидка и скидка на доставку); отдельные плитки Discount / Discount Shipping остаются для справки.
#
Блок
mainType
Условие попадания
1
SONR Music
"SONR Music"
Заказ содержит ≥1 item с этим типом
2
SONR Music 2
"SONR Music 2"
...
3
SONR Music Multisport
"SONR Music Multisport"
...
4
SONR Coach
"SONR Coach"
Заказ содержит ≥1 SONR Coach SKU
5
SONR Accessories Only
"Accessories"
Все items — Accessories AND нет SONR Music/Music 2/Multisport/Coach
Для блоков 1–4 in-block items = {mainType, "Accessories"} — accessories из тех же заказов учитываются в этих блоках. Для блока 5 — только Accessories. Items, не попавшие ни в один блок, идут в Block 6 Other Orders.
Items из Shopify-заказов, чей skuid_product_type не входит в {SONR Music, SONR Music 2, SONR Music Multisport, SONR Coach, Accessories} или вообще отсутствует в каталоге. Колонки: Order ID · Date · Status · Product · Variant · Type · Qty · Revenue. Клик по строке раскрывает полные детали заказа.
Чтобы убрать item из Block 6 — задай ему skuid_product_type в Settings → Products Settings → Shopify. Если SKU вообще нет в каталоге — сначала Sync → Shopify → 4. Sync Products чтобы подтянуть его в raw_products, потом Settings → Products Settings → Sync Shopify products.
Блок 7 · SONR Orders export
CSV-экспорт по типу продукта. Диапазон дат — из top-of-page dashboard-хедера (dashDateFrom / dashDateTo); отдельных date-input'ов в форме нет.
Types — 6 chip-чекбоксов (SONR Music / Music 2 / Multisport / Coach / Accessories / Other) + мастер All
Канал заказа определяется как тип первого counted item (или "Other" если такого нет). CSV-колонки: Order ID, Date, Status, Type, Customer, Email, Items, Subtotal, Total, Shipping, Tax, Discount, Payment. Имя файла: sonr-shopify-orders-{from}-{to}.csv.
2d
Dashboard · Amazon
Аналитика продаж SONR Amazon Store — 5 блоков по типам продуктов + Other Orders + CSV-экспорт.
Источники данных
Все 7 блоков работают над одним базовым набором: orders в MySQL, отфильтрованные по:
o.webflow === "SONR Amazon"
&& o.shop === "SONR Amazon Store"
&& o.acceptedOn в диапазоне дат из top-of-page header'а
Этот набор передаётся в renderAmazonBlocks(amzOrders), который строит все 6 блоков с метриками и SKU breakdown.
Items в Amazon-заказах
Amazon SP-API возвращает orders и orderItems отдельно. При Quick Sync для каждого заказа дополнительно вызывается /orders/v0/orders/{AmazonOrderId}/orderItems (rate-limit 0.5 req/sec, burst 30). Items маппятся в наш формат через transformAmazonOrder:
Поле
Источник Amazon
productId
"amazon-" + ASIN (например amazon-B0F9FRTLDY)
variantId
"amazon-" + SellerSKU — это ключ для матчинга с catalog
variantSKU
Сырой SellerSKU
asin
Сырой ASIN
product
Item.Title
count
QuantityOrdered
price
ItemPrice.Amount × 100 (центы)
Замены (Replacement Orders):transformAmazonOrder сохраняет поле isReplacement: raw.IsReplacementOrder === "true". Поле проставляется только если Amazon вернул его явно (список заказов его не включает — только single-order endpoint). Значение сохраняется в PRESERVED_ORDER_FIELDS в db.js — sync не перезаписывает его. Для исторических fulfilled-заказов с total=0 — endpoint POST /orders/amazon-backfill-replacements re-fetches /orders/v0/orders/{id} и заполняет поле. Replacement-заказы исключены из всех блоков: не входят в revenue, fulfilled-count и progress bar.
Скидки (Promotional Rebates): при маппинге items суммируется PromotionDiscount.Amount по всем items заказа. Если сумма > 0 — в order.extras[] добавляется запись {type:"discount", name:"Promotional Rebates", price: -total}. Именно этот extras-discount используется во всех блоках для тайла Discounts и для вычисления net revenue.
Backfill items для исторических заказов с пустым items[] — endpoint POST /orders/amazon-backfill-items. Идемпотентный, incremental persist каждые 25 заказов, retry с экспоненциальным backoff'ом.
Backfill discounts для заказов у которых уже есть items[] но пустой extras[] — endpoint POST /orders/amazon-backfill-discounts. Перезапрашивает /orderItems и заполняет PromotionDiscount для исторических заказов. Идемпотентный.
Каталог продуктов
Амазон-листинги хранятся в app_data['products'] с source: "Amazon". skuId в каталоге равен variantId в order items, что позволяет dashboard'у через skuMap[item.variantId]?.skuid_product_type определить тип продукта. Если у каталог-SKU не задан skuid_product_type — item уйдёт в Block 6 "Other Orders".
Блок 1 · SONR Music
Фильтр заказа: заказ попадает в блок если в его items[] есть хотя бы один item чей skuMap[item.variantId]?.skuid_product_type === "SONR Music".
Один заказ может одновременно попасть в несколько блоков (если у него items разных типов).
Метрики
Финансовые метрики считаются только по fulfilled заказам. Pending и Cancelled видны отдельными счётчиками, но в суммы не входят.
Tile
Что считается
Gross Revenue (hero)
Product Revenue − Promotional Rebates (m.productRevenue + m.discount) — определение Amazon Seller Central. Accessories в gross НЕ входят (остаются отдельной плиткой). Item-by-item через computeMusicBlockMetrics, только fulfilled (без replacements). Promotional Rebates = extras[{type:"discount"}].price (отрицательное; сейчас в данных = 0).
Σ item.price для SONR Music items минус блочные Discount и Discount Shipping (productRevenueNet) — только fulfilled
SONR Music q-ty
Σ item.count × (skuid_main_product_qty ?? 1) — только fulfilled
Accessories revenue
Σ item.price для Accessories items в fulfilled заказах блока
Accessories q-ty
Σ count для Accessories items — только fulfilled
Discounts
Σ PromotionDiscount.Amount по всем items заказа, хранится в extras[{type:"discount"}] — отрицательная, только fulfilled
Pending
Кол-во заказов со статусом pending — всегда виден (0 если нет)
Cancelled
Кол-во заказов со статусом canceled — всегда виден (0 если нет)
Replacements
Кол-во warranty replacement заказов (isReplacement=true) — всегда виден; исключены из всех финансовых метрик
Progress bar
Под метриками — % fulfilled по всем заказам блока с цветными счётчиками N fulfilled / N unfulfilled.
SKU breakdown
Под progress-bar — два сворачиваемых списка SKU по всем заказам блока (включая unfulfilled):
SONR Music — все items типа "SONR Music" из заказов блока
Accessories — все Accessories items из заказов блока
Блок 2 · SONR Music 2
Идентичная Блоку 1 логика для type = "SONR Music 2". Те же тайлы: Gross Revenue (Product Revenue − Promotional Rebates, accessories не входят), Orders, Orders Fulfilled, revenue (net), q-ty, Accessories, Discounts, Pending, Cancelled, Replacements. Финансовые метрики — только fulfilled (без replacements).
Блок 3 · SONR Music Multisport
То же что Блоки 1/2 для type = "SONR Music Multisport". Те же тайлы: Orders, Orders Fulfilled, revenue (net), q-ty, Accessories, Discounts, Pending, Cancelled, Replacements.
Блок 4 · SONR Accessories
Этот блок ведёт себя иначе — он показывает заказы где аксессуары пришли сами по себе, без основного продукта.
Где AMAZON_PRODUCT_MAIN_TYPES = {"SONR Music", "SONR Music 2", "SONR Music Multisport", "SONR Coach"}.
Зачем такой фильтр
Аксессуары которые приехали с основным продуктом (Music/Coach/etc) уже учитываются в блоке этого продукта (tile "Accessories revenue/q-ty"). Block 4 показывает только чистые accessory-only заказы — кто-то купил исключительно аксессуар.
Метрики
Финансовые метрики — только fulfilled. computeMusicBlockMetrics(fulfilledOrders, "Accessories") с isAccBlock = true:
Product revenue / q-ty и Accessories revenue / q-ty tiles скрыты — для accessory-only блока они бессмысленны
Gross Revenue — Product Revenue − Promotional Rebates (для Acc-only блока Product = Accessories, поэтому значение не меняется), только fulfilled (без replacements)
Σ item.price × count для Coach items минус блочные Discount и Discount Shipping (coachRevenueNet) — fulfilled
SONR Receiver q-ty
Σ count × skuid_receivers_count для Coach items — fulfilled
SONR Radio q-ty
Σ count × skuid_radio_count для Coach items — fulfilled
Discounts
Σ PromotionDiscount из extras[{type:"discount"}] × block share — fulfilled (отрицательная)
Accessories revenue / q-ty
Accessories items в fulfilled заказах
Pending / Cancelled
Счётчики по всем заказам (без replacements), всегда видны
Replacements
Warranty replacements (isReplacement=true), всегда виден; исключены из revenue и fulfilled
Блок 6 · Other Orders
Это item-level dump — не агрегированные заказы, а конкретные позиции которые "потерялись".
Что туда попадает
Items из всех Amazon-заказов где:
const type = skuMap[item.variantId]?.skuid_product_type;
if (!type || !AMAZON_COUNTED_TYPES.has(type)) {
uncountedItems.push({ order, item, type: type || "—" });
}
То есть item попадает если:
SKU нет в каталоге (skuMap[item.variantId] = undefined) — typically значит что нужно сделать Sync → Amazon → Sync Products чтобы подтянуть свежие листинги, потом Settings → Products → Sync Amazon products
SKU есть, но без типа (skuid_product_type = пусто) — нужно зайти в Settings → Products → Amazon и проставить тип
SKU с типом вне 5 counted-types (например "Music Streaming" или "Others") — будет в Block 6 пока не переклассифицирован
AMAZON_COUNTED_TYPES = {"SONR Music", "SONR Music 2", "SONR Music Multisport", "SONR Coach", "Accessories"}.
Отображение
Используется тот же helper renderDashOtherSkus что у Music Block 5. Это таблица с колонками: Order ID · Date · Status · Product · Variant · Type · Qty · Revenue. Клик по строке раскрывает полные детали заказа (items, customer, shipping, extras).
Settings → Products Settings → Sync Amazon products — перенести в рабочий products
Settings → Products → Amazon — выбрать skuid_product_type для каждого ASIN. После save — заказы автоматически переедут в нужный блок 1–5 при следующем рендере dashboard'а
Блок 7 · Mixed Orders
Заказ попадает если в его items[] есть товары 2+ различных основных типов (не считая Accessories). Например SONR Music + SONR Coach в одном заказе. Такие заказы также остаются в своих блоках 1–5.
const types = new Set(items.map(it => skuMap[it.variantId]?.skuid_product_type).filter(t => t && t !== "Accessories"));
types.size >= 2
Tile
Что считается
Gross Revenue (hero)
Σ order.total — фактическая сумма заказа, только fulfilled (без replacements)
Σ item.price × count для Coach items минус блочные Discount и Discount Shipping (coachRevenueNet) — fulfilled
SONR Receiver q-ty
Σ count × skuid_receivers_count для Coach items — fulfilled
SONR Radio q-ty
Σ count × skuid_radio_count для Coach items — fulfilled
SONR Music q-ty
Σ count × skuid_main_product_qty для Music items — fulfilled
Discounts
Σ PromotionDiscount из extras[{type:"discount"}] × block share — fulfilled (отрицательная)
Accessories revenue / q-ty
Accessories items в fulfilled заказах
Pending / Cancelled
Счётчики по всем заказам (без replacements), всегда видны
Replacements
Warranty replacements (isReplacement=true), всегда виден; исключены из revenue и fulfilled
Блок 8 · SONR Orders export
CSV-экспорт по типу продукта. Диапазон дат — из top-of-page dashboard-хедера (dashDateFrom / dashDateTo); отдельных date-input'ов в форме нет.
Types — 6 chip-чекбоксов (SONR Music / SONR Music 2 / SONR Music Multisport / SONR Accessories / SONR Coach / Other) + мастер All (заполненный navy при активности)
Логика фильтра экспорта
Заказ попадает в выгрузку если выполняется ОБА условия:
И один из двух матчей: (а) хотя бы один item имеет skuid_product_type ∈ выбранные chip'ы, ИЛИ (б) primary type заказа (тип первого counted item, иначе __other__) ∈ выбранные chip'ы
CSV-колонки:Order ID, Date, Status, Type, Customer, Email, Items, Subtotal, Total, Shipping, Tax, Discount, Payment. Type = primary type заказа (или "Other" для __other__). Items = склейка "Product (variant) ×count" через "; ". Filename:sonr-amazon-orders-{from}-{to}.csv.
Per-block Export-кнопки
В шапке каждого блока 1–6 — outlined-navy Export кнопка с download-иконкой (рядом с Wiki-кнопкой). Клик скачивает CSV только этого блока за период из dashboard-header'а. Реализована через delegated event handler с уникальными data-amazon-export-type / data-amazon-export-slug атрибутами (не конфликтует с Music dashboard'ом который использует data-export-type).
Settings → Products → Amazon tab — для каждого ASIN dropdown'ом проставить skuid_product_type: SONR Music / SONR Music 2 / Multisport / Accessories / SONR Coach
Save changes
Открыть Dashboard → Amazon — все заказы автоматически распределены по блокам
Если в БД уже есть Amazon-заказы с пустым items[] (импорт делался до фикса import flow), нужно дополнительно дёрнуть POST /orders/amazon-backfill-items — иначе заказы попадут в Block 6 (нет items для матчинга). Backfill идемпотентный, безопасно повторно запускать.
3
Sync
Управление синхронизацией данных из всех источников.
Обзор
Страница Sync управляет импортом и синхронизацией заказов из всех подключённых источников данных. Состоит из четырёх вкладок:
Вкладка
Назначение
Orders
Сводная таблица всех заказов из всех источников. Статистика, фильтры, экспорт CSV.
Webflow
Синхронизация SONR Music и SONR Coach из Webflow E-commerce API.
Shopify
Подключение и синхронизация магазина Shopify через OAuth 2.0.
Amazon
Синхронизация заказов Amazon через SP-API.
Подробное описание каждой вкладки — в отдельных статьях (см. боковое меню).
Sync
Sync · Orders
Сводная таблица всех заказов — просмотр, фильтры, экспорт.
Статистика (Stats Bar)
В верхней части — 4 KPI-карточки по всем источникам (Coach excluded не применяется — учитываются все):
Карточка
Что показывает
Total Orders
Общее число заказов во всей базе
Fulfilled
Количество + % выполненных заказов
Unfulfilled
Количество + % невыполненных заказов
Total Revenue
Суммарная выручка по всем заказам (customerPaid.value)
Ниже — карточки по каждому источнику: SONR Music, SONR Coach, SONR Shopify, SONR Amazon. Каждая карточка показывает Revenue, Orders, прогресс-бар fulfilled % и счётчики fulfilled/unfulfilled.
Таблица заказов
Основной контент вкладки — таблица всех заказов с колонками:
Колонка
Источник данных
Order ID
order.orderId
Customer
order.customer.name
Date
order.acceptedOn — отображается в формате DD.MM.YYYY
Status
order.status — цветная метка (fulfilled, unfulfilled и др.)
Items
Список товаров в заказе (order.items[])
Total
order.customerPaid.value в центах → форматируется через formatCurrency()
↗
Кнопка открытия модального окна с деталями заказа
Фильтры и поиск
Элемент
Назначение
All Sites
Фильтр по источнику: All / SONR Music / SONR Coach / SONR Shopify / SONR Amazon
Search
Текстовый поиск по Order ID и имени покупателя
All Statuses
Фильтр по статусу: All / Fulfilled / Unfulfilled
Page size
50 / 100 / 200 / All orders — количество строк на странице
From / To + Apply
Фильтр по дате принятия заказа (acceptedOn). UTC строковое сравнение.
Экспорт CSV
Кнопка Export CSV доступна только администраторам (role === "admin"). Экспортирует все заказы с применёнными фильтрами. Имя файла включает диапазон дат. Валюта в CSV записывается без пробелов (formatCurrencyRaw()).
Sync
Sync · Webflow
Синхронизация заказов SONR Music и SONR Coach из Webflow E-commerce API.
Источники
Вкладка содержит два блока — по одному на каждый Webflow сайт:
Сайт
Site ID
Поле webflow
SONR Music
635937ab62b5fe11f4a2284a
SONR Music
SONR Coach
5dae68cb7b316e4ed4f61201
SONR Coach
1. Quick Sync
Забирает заказы из Webflow API за выбранный период, трансформирует в унифицированный формат и сохраняет в MySQL через upsert.
Параметр
Описание
From (дата)
Нижняя граница выборки — acceptedOn >= date
Count
All (все за период) / Last 50 / 100 / 200
Sync (кнопка)
Запускает POST /sync-webflow → сервер делает запросы к Webflow API
Статус операции отображается под toolbar. При успехе показывается количество добавленных/обновлённых заказов.
2. Auto Sync
Серверный фоновый синк — работает без участия пользователя по расписанию. Управляется через toggle-переключатель.
Параметр
Значения
Count
50 / 100 / 200 последних заказов на каждом цикле
Interval
Every 1 hour / Every 2 hours
Toggle
Вкл/выкл фоновый синк. Настройки сохраняются в MySQL (app_data['auto_sync']) и восстанавливаются автоматически при перезапуске сервера или деплое на Railway.
3. Sync Products
Забирает каталог продуктов из Webflow API и сохраняет в app_data['raw_products']. Доступно для обоих сайтов (SONR Music и SONR Coach). Это сырые данные без пользовательских настроек.
После выполнения — перейдите в Settings → Products Settings и нажмите соответствующую кнопку Sync (SONR Music или SONR Coach), чтобы обновить рабочий каталог app_data['products']. Пользовательские поля (skuid_product_type, skuid_product_color, skuid_main_product_qty, skuid_receivers_count, skuid_radio_count) при этом сохраняются.
Sync
Sync · Shopify
Подключение и синхронизация заказов из Shopify через OAuth 2.0.
Подключение (OAuth)
При первом входе отображается блок Connect Shopify с кнопкой «Connect Shopify Account». Поток авторизации:
Кнопка → GET /shopify/auth → редирект на Shopify OAuth.
После подтверждения Shopify редиректит на /shopify/callback.
Сервер обменивает code на access token, сохраняет в памяти.
Страница-подтверждение показывает токен с кнопкой Copy.
На Railway токен нужно вставить вручную в Railway Variables → SHOPIFY_ACCESS_TOKEN.
После подключения блок Connect скрывается и появляется блок синхронизации.
1. Import from Shopify
Первичный импорт — загружает сырые заказы из Shopify Admin API (2025-07) в app_data['raw_shopify']. Использует cursor-based пагинацию через Link header. Параметр — дата From.
2. Quick Sync
Загружает свежие заказы из Shopify Admin API напрямую, трансформирует в унифицированный формат и делает upsert в MySQL. Параметры — From (дата, передаётся как created_at_min) и Count. Использует cursor-based пагинацию через Link header.
3. Auto Sync
Фоновый серверный синк по расписанию (1 или 2 часа). Управляется toggle-переключателем. Настройки сохраняются в MySQL (app_data['auto_sync']) и восстанавливаются автоматически при перезапуске сервера или деплое на Railway.
4. Sync Products
Пулит все продукты из Shopify Admin API (GET /admin/api/2025-07/products.json?limit=250) с cursor-based пагинацией (Link header) и сохраняет в raw_products с source: "Shopify".
Трансформация transformShopifyProduct:
Наше поле
Источник в Shopify
productId
"shopify-" + raw.id
source
"Shopify"
name
raw.title
slug
raw.handle
description
raw.body_html (HTML-теги удаляются)
shippable
variants[0].requires_shipping
skus[].skuId
"shopify-" + variant.id
skus[].price
round(variant.price × 100) (в центах)
skus[].image
variant image по image_id, иначе fallback на product.image.src
skus[].variant
{option1, option2, option3} — для отображения различий между вариантами
Импорт — только в raw_products. Чтобы применить в каталог — используйте Settings → Products Settings → Sync from Shopify.
Конфигурация
Параметр
Значение
Shop
sonr-music.myshopify.com
API version
2025-07 (переопределяется через SHOPIFY_API_VERSION env var)
Синхронизация заказов Amazon US через Amazon Selling Partner API (SP-API).
Подключение
Amazon SP-API не использует OAuth flow из браузера. Credentials задаются вручную через ENV переменные (локально в .env, на Railway — в Variables).
Переменная
Описание
AMAZON_LWA_CLIENT_ID
LWA Application Client ID
AMAZON_LWA_CLIENT_SECRET
LWA Application Client Secret
AMAZON_REFRESH_TOKEN
Refresh Token (содержит символ | — при вставке в Railway убедитесь что значение не обрезается)
AMAZON_MARKETPLACE_ID
ATVPDKIKX0DER (Amazon US)
AMAZON_SELLER_ID
Selling Partner ID (e.g. A5K41I1GYC008) — нужен только для 4. Sync Products
Если credentials не заданы — отображается блок-заглушка «Add your Amazon credentials to .env». После добавления и перезапуска появляется блок синхронизации.
Access token кешируется в памяти сервера и автоматически обновляется за 5 минут до истечения.
1. Import from Amazon
Загружает сырые заказы из Amazon Orders API (/orders/v0/orders) в app_data['raw_amazon']. Items каждого заказа загружаются отдельно через /orders/v0/orders/{orderId}/orderItems с rate limiting. Параметр — дата From (обязательная для SP-API, CreatedAfter).
2. Quick Sync
Забирает заказы напрямую из Amazon SP-API (GET /orders/v0/orders), трансформирует и делает upsert в MySQL. Параметры — From (дата) и фильтр статуса.
После сохранения заказов автоматически запускает backfill items (POST /orders/amazon-backfill-items) — новые заказы сразу получают информацию о продуктах и отображаются в Dashboard блоках без ожидания следующего Auto Sync.
3. Auto Sync
Фоновый серверный синк по расписанию (1 или 2 часа). Управляется toggle-переключателем. Настройки сохраняются в MySQL (app_data['auto_sync']) и восстанавливаются автоматически при перезапуске или деплое на Railway.
4. Sync Products
Тянет все listings продавца через SP-API GET /listings/2021-08-01/items/{sellerId} и сохраняет в app_data['raw_products'] с source: "Amazon". Постранично через pagination.nextToken (pageSize=20). Каждый listing маппится в product-record c одним SKU внутри (Amazon не моделирует варианты как Webflow/Shopify — каждый ASIN/SKU отдельный листинг). Поля product: productId: "amazon-{sku}", name (из summaries[].itemName), один SKU с sku, asin, image (summaries[].mainImage.link), price (если в offers[] есть запись для текущего marketplace).
Merge в raw_products идёт по ключу productId:source — повторный sync обновляет записи in-place, не дублирует. Endpoint: POST /sync-products-amazon.
Конвертация валюты в USD
Amazon-маркетплейсы могут присылать заказы в разных валютах. Amazon.com — USD, Amazon.com.mx — MXN. Валюта берётся из OrderTotal.CurrencyCode (fallback по маркетплейсу). Чтобы вся аналитика была в одной валюте, не-USD заказы конвертируются в USD при синке.
Курс — из Settings → Exchange (1 USD = курс × валюта); конвертация: USD = сумма / курс
Что конвертируется: total, subtotal, items[].price, extras[].price (скидка), shipping.cost
Когда: при Auto Sync / Quick Sync / Import и в бэкфилле позиций. Если курса для валюты нет — заказ остаётся в исходной валюте (тег currency ставится, конвертация не делается)
Существующие заказы: разовый POST /orders/amazon-convert-currency конвертирует уже загруженные не-USD заказы (идемпотентно — пропускает уже сконвертированные по originalCurrency)
Курс «замораживается» на момент синка (exchangeRate в заказе). Если поменять курс в Settings — пересчёт существующих заказов делается повторным запуском конвертации/синка.
N
Notifications
Ежедневные Telegram-отчёты в групповые чаты — автоматически каждый день в 9:00 GMT+2.
Что это
Страница Notifications позволяет настроить автоматическую отправку ежедневного отчёта о заказах в Telegram-группы. Сервер каждый день в настроенное время (по умолчанию 9:00 GMT+2) собирает статистику за вчера и рассылает сообщение во все настроенные чаты.
Содержимое daily-отчёта
Сообщение содержит заказы за вчерашний день (UTC), каждый канал отдельным блоком с разбивкой по продуктам. Для Amazon дополнительно показываются статусы:
Все каналы всегда присутствуют в сообщении (даже с "нет заказов"). Названия продуктов обрезаются до 35–40 символов. Выручка из поля data.total (центы → доллары).
Настройка
Telegram Bot Token
Создайте бота через @BotFather → /newbot → токен вида 123456:ABC-DEF...
Добавьте бота в группу
Задайте токен через переменную окружения TELEGRAM_BOT_TOKEN (локально в .env, на Railway — в Variables)
Токен не хранится в БД и не отображается в UI — только через env var.
Telegram Chats
Каждый чат отображается карточкой с именем группы и Chat ID. Как узнать Chat ID:
Добавьте бота в группу и напишите /start
Откройте https://api.telegram.org/bot{TOKEN}/getUpdates — найдите message.chat.id (для групп — отрицательное число)
Кнопка + Add Chat открывает inline-форму (Name + Chat ID). Кнопка Send Report у каждой карточки — немедленно отправляет вчерашний отчёт в этот конкретный чат.
Send Time
Выберите час (0–23) и минуту (00/15/30/45) в GMT+2. После нажатия Save Settings cron-задача перепланируется автоматически — без перезапуска сервера.
Кнопка Send to All немедленно отправляет вчерашний отчёт во все настроенные чаты — независимо от статуса Enable.
Backend
Endpoint
Назначение
GET /api/notifications-config
Загрузить конфиг (токен не возвращается)
POST /api/notifications-config
Сохранить конфиг + перепланировать cron
POST /api/notifications-test
Отправить отчёт во все чаты сейчас
POST /api/notifications-send-chat
Отправить отчёт в один чат ({ chat_id })
Cron динамический: при старте сервера загружается из app_data['notifications_config'] (send_hour, send_minute в GMT+2). Конвертируется в UTC автоматически.
4
Statistics
Аналитические отчёты по продуктам SONR Music / SONR Coach со всех источников за выбранный период.
Product scope (переключатель)
В шапке страницы — 3 pill-кнопки над hero-плашкой, переключающие набор данных для всех 7 отчётов. По умолчанию выбран All.
Кнопка
Что попадает
All (по умолчанию)
Все заказы за период, без фильтра по SKU-типу
SONR Music
Заказы, у которых хотя бы один item имеет skuid_product_type ∈ {"SONR Music", "SONR Music 2", "SONR Music Multisport"}
SONR Coach
Заказы, у которых хотя бы один item имеет skuid_product_type === "SONR Coach"
Source (переключатель)
Второй ряд под Product scope — 3 pill-кнопки для фильтра по источнику заказа. Применяется AND с Product scope: оба условия должны выполняться одновременно. По умолчанию — Webflow.
Кнопка
Какие заказы
Webflow (по умолчанию)
webflow ∈ {"SONR Music", "SONR Coach"} — оба Webflow-storefront вместе
Shopify
webflow === "SONR Shopify"
Amazon
webflow === "SONR Amazon"
Кнопки General (все источники вместе) и OKR убраны. OKR вынесен в отдельную страницу (пункт меню после Statistics).
Hero-sublabel динамически отражает оба фильтра: {scope} · {source} · Sales reports for the selected period.
Источники данных
Страница работает с заказами из MySQL (endpoint /api/orders) + продуктовым каталогом /products для определения skuid_product_type. Источник заказа (webflow, shop) не используется в фильтре — выборка идёт по типу SKU из каталога.
Фильтр по дате
В верхней части страницы — поля From и To + кнопка Apply. По умолчанию выставляется первый день текущего месяца → сегодня (UTC). Все отчёты пересчитываются при нажатии Apply.
KPI карточки
Карточка
Формула
Orders in Range
Количество заказов за выбранный период
Total Revenue
Сумма customerPaid.value по всем заказам периода
Average Order
Total Revenue / Orders in Range
Avg Items / Order
Среднее количество позиций на заказ
Фильтр по статусу
Перед таблицами — строка chip-фильтров: Unfulfilled, Fulfilled, Refunded, Disputed, Dispute-lost. Можно выбрать несколько. Все активные чипы применяются к отчётам одновременно. Если ни один не выбран — показываются все статусы.
Как считается Revenue (нетто)
На вкладках Orders и Products выручка (Revenue) считается нетто — за вычетом скидок, распределённых по позициям заказа. Для каждой позиции:
itemShare = item.price / Σ(цены всех позиций заказа) — доля позиции в товарной сумме заказа
netRevenue позиции = item.price + (orderDiscount + orderDiscountShipping) × itemShare
Здесь orderDiscount (extras[type="discount"]) и orderDiscountShipping (extras[type="discount-shipping"]) — суммы уровня заказа, обе отрицательные, поэтому уменьшают выручку. Доставка (положительная) и налог в Revenue не входят.
Revenue на всех уровнях (группа типа → продукт → SKU) — это сумма netRevenue входящих позиций, поэтому суммы строк точно сходятся с итогом блока. В раскрытом списке заказов колонка Revenue показывает item.price позиции (gross), а скидки вынесены в отдельные колонки Discount / Disc. Ship.
Примечание: на дашбордах (Dashboard) проращивание идёт по доле блока в заказе (blockItemValue / totalItemValue), а в Statistics — по доле каждой позиции. Результат на уровне типа продукта совпадает.
Вкладки отчётов
Вкладка
Колонки
Описание
Orders
Карточки по типу продукта
Продукты сгруппированы по типу (skuid_product_type): SONR Music, SONR Music 2, SONR Music Multisport, SONR Coach, Accessories. Каждая группа показывает заголовок с суммарными units + нетто-Revenue (см. секцию «Как считается Revenue» ниже), затем строки по названию продукта. Уровень 2: клик на продукт раскрывает список SKU-вариантов (фото, кол-во заказов, units, нетто-Revenue) — раскрываются все проданные SKU, имя/фото подтягиваются из каталога по коду SKU (работает и для Amazon). Уровень 3: клик на SKU раскрывает заказы с 4 финансовыми колонками — Total Paid (полная сумма заказа), Revenue (выручка позиции, gross), Discount и Disc. Ship (скидки уровня заказа, «—» если нет).
Products
Dashboard-карточки по типу продукта
Разбивка по 5 типам: SONR Music, SONR Music 2, SONR Music Multisport, SONR Coach, Accessories. Каждая карточка показывает: нетто-Revenue (hero), Orders, Units Sold, Avg Order Revenue (= Revenue / Orders), прогресс fulfilled/unfulfilled. Ниже карточки — список SKU с кол-вом заказов, юнитов и нетто-Revenue. Раскрытие SKU: те же 4 колонки заказа, что и на вкладке Orders (Total Paid · Revenue · Discount · Disc. Ship). Классификация по skuid_product_type из skuMap.
Color
Product · Orders · Units · Revenue
Группировка по типу продукта (skuid_product_type): SONR Music / Music 2 / Multisport / Coach / Accessories — работает для всех источников (Webflow/Shopify/Amazon), реагирует на фильтр Source. Внутри каждого типа — разбивка по цвету из каталога (skuid_product_color, задаётся в Settings → Products Settings); SKU без цвета → группа Unknown. Под таблицей — SVG-пироги долей цвета по каждому типу.
Status
Status · Orders · Items · % · Revenue
Распределение заказов по статусу исполнения.
Country
Country · Code · Orders · % · Items · Revenue
География заказов. Вверху — два SVG-пирога бок о бок: Revenue by Region (группировка по регионам из Settings → Regions) и Revenue by Country (топ-15 стран). Дольки и проценты считаются по доходу (revenue, нетто-сумма заказов в USD), а не по количеству заказов — поэтому топ-15 стран в пироге отбираются и сортируются именно по доходу. В легенде у каждого сегмента — сумма дохода и его доля (%). Оба пирога интерактивны: hover-тултип показывает Orders · Items · Revenue (процент привязан к Revenue), а в Region-тултипе — топ стран региона по доходу. Клик на строку страны в таблице раскрывает список продуктов этой страны (Product · Orders · Items · Revenue), отсортированный по количеству заказов. Ниже — кнопки ↓ Export Region CSV и ↓ Export Country CSV для скачивания данных за выбранный период.
Статистика по методам доставки. Вверху — SVG-пирог Orders by Shipping с % на секторах; справа — Avg Shipping (paid) (среднее только по платным методам) + Total Cost + цветовая легенда. Hover-тултип: Orders / Items / Avg Shipping для каждого метода. Ниже пирога — кнопка ↓ Export Shipping CSV. Источники методов: Webflow → extras[type=shipping].name; Shopify → shipping.method; Amazon → "Amazon FBA (Expedited/NextDay/Standard)".
Payment
Payment · Orders · % · Items · Revenue
Группировка по методу оплаты. Вверху — SVG-пирог Orders by Payment с % на секторах; справа — Total Revenue + Total Orders + цветовая легенда. Hover-тултип: Orders / Items / Revenue. Ниже — кнопка ↓ Export Payment CSV. Нормализация: Amazon Other → "Amazon Pay"; Shopify null → "Shopify Payments"; Shopify новые заказы используют payment_gateway_names[0].
6
OKR
Objectives & Key Results — отдельная страница (пункт меню после Statistics). Сейчас заглушка, наполнение появится позже.
Статус
Страница в разработке — пока показывает только hero-баннер и заглушку «Страница в разработке». Контейнер page-okr зарегистрирован в switchPage() (app.js).
Контекст
Ранее OKR был placeholder-кнопкой в фильтре Source на странице Statistics (всегда пустая выборка). Теперь вынесен в отдельную страницу — кнопка из Source убрана. Назначение страницы — цели и ключевые результаты (Objectives & Key Results); метрики и формулы будут добавлены позже.
5
Settings
Управление пользователями, регионами и каталогом SKU-продуктов.
Обзор
Страница Settings доступна только администраторам. По умолчанию открывается вкладка Overview с карточками-навигаторами к четырём разделам:
Вкладка
Назначение
Overview
Стартовая вкладка. Четыре карточки-ссылки на остальные вкладки с кратким описанием.
Users
Список пользователей и кнопка «+ Add User» для раскрытия формы создания.
Countries & Regions
Управление регионами доставки и их странами (сетка из двух колонок).
Products Settings
Каталог SKU-продуктов: категории, цвет и количество для Dashboard.
Exchange
Курсы валют относительно USD (1 USD = курс × валюта), хранятся в БД.
Settings
Settings · Users
Управление учётными записями пользователей.
Таблица пользователей
По умолчанию отображается список всех пользователей. Нажмите + Add User в правом углу заголовка — форма создания раскроется ниже. Нажмите Cancel или снова «+ Add User» чтобы свернуть.
Все поля в таблице редактируемые — кнопка Edit справа. Правила безопасности:
Нельзя удалить себя
Нельзя удалить последнего администратора
Нельзя понизить роль последнего администратора
Форма Add New User
Раскрывается по кнопке + Add User. Обязательные поля — Username и Password. После создания форма автоматически сворачивается.
Поле
Обязательное
Описание
Username
Да
Уникальный идентификатор для входа
Password
Да
Пароль для входа
Full Name
Нет
Отображаемое имя
Email
Нет
Контактный email
Phone
Нет
Контактный телефон
Role
Да
user (только просмотр заказов) или admin (полный доступ)
Хранение
Пользователи хранятся в app_data['users'] (таблица app_data, ключ users). Пароли хранятся в открытом виде в JSON — не используйте критичные пароли.
Settings
Settings · Countries & Regions
Управление регионами доставки и их составом по странам.
Назначение
Регионы используются на странице Statistics в отчёте Country — страны группируются по регионам для отображения на SVG-чарте.
Управление регионами
Кнопка + Add Region создаёт новый регион. Клик по названию региона — переименование. Под каждым регионом список стран с возможностью добавления (+ Add Country) и удаления (×).
Данные хранятся в app_data['regions'].
Settings
Settings · Products Settings
Настройка SKU-каталога — категории, цвет и количество продуктов для Dashboard.
Sync from Webflow / Shopify / Amazon
Четыре кнопки для импорта продуктов из app_data['raw_products'] в app_data['products']:
Кнопка
Источник
Endpoint
Sync Webflow SONR Music products
source === "Webflow SONR Music"
POST /products/sync-from-raw
Sync Webflow SONR Coach products
source === "Webflow SONR Coach"
POST /products/sync-from-raw-coach
Sync Shopify products
source === "Shopify"
POST /products/sync-from-raw-shopify
Sync Amazon products
source === "Amazon"
POST /products/sync-from-raw-amazon
Все четыре сохраняют пользовательские настройки SKU (skuid_product_type, skuid_product_color, skuid_main_product_qty и пр.) при обновлении.
Перед sync нужно сначала получить сырые данные — через Sync → Webflow → Sync Products для Music/Coach, Sync → Shopify → Sync Products для Shopify, или Sync → Amazon → Sync Products для Amazon.
Переключение источника
Над списком продуктов расположены четыре переключателя — SONR Music, SONR Coach, Shopify, Amazon. Они фильтруют список по полю product.source на клиенте. Все продукты хранятся в одной таблице app_data['products'], переключение не меняет данные.
Список продуктов
Список всех SKU выбранного источника, сгруппированных по skuid_product_type. Набор полей зависит от активного источника.
SONR Music:
Поле
Значения
Как используется
skuid_product_type
SONR Music / SONR Music 2 / SONR Music Multisport / Accessories / и др.
Определяет в какой блок Dashboard попадает SKU. Если не задан — SKU попадает в Block 5 (Other).
skuid_product_color
Blue / Yellow / Graphite / Coral / Black
Цвет варианта. Доступен в skuMap, используется в Statistics → Color.
skuid_main_product_qty
1 / 2 / 3 (число)
Множитель количества. Например SONR Music x2 → 2. Применяется при подсчёте Q-ty в Dashboard.
SONR Coach:
Поле
Значения
Как используется
skuid_product_type
SONR Coach / Accessories / и др.
Категория SKU для классификации в отчётах.
skuid_product_color
Blue / Yellow / Graphite / Coral / Black
Цвет варианта.
skuid_main_product_qty
1 / 2 / … (число)
Количество основных устройств в наборе.
skuid_receivers_count
число
Количество приёмников (receivers) в наборе.
skuid_radio_count
число
Количество радиомодулей (radios) в наборе.
Изменения не сохраняются автоматически. После редактирования одного или нескольких SKU появляется кнопка Save changes — нажмите её чтобы сохранить все изменения разом. Данные отправляются одним запросом POST /products/save-skus, который обновляет только явно изменённые поля, не трогая остальные.
Sync from Webflow — сохранение настроек
При повторной синхронизации SONR Music продуктов из Webflow все три поля (skuid_product_type, skuid_product_color, skuid_main_product_qty) сохраняются — они не затираются сырыми данными из Webflow.
Связь с Dashboard
При загрузке страницы Dashboard строится skuMap — словарь variantId → { skuid_product_type, skuid_product_color, skuid_main_product_qty }. Все блоки Dashboard используют этот словарь для классификации товаров в заказах. После изменения настроек SKU нужно перезагрузить страницу Dashboard для применения изменений.
Settings
Settings · Exchange
Курсы валют относительно USD — задаются вручную, хранятся в БД.
Назначение
Вкладка Settings → Exchange позволяет задать курсы обмена валют. Базовая валюта всегда USD. Курс задаётся в формате 1 USD = курс × валюта (например, 1 USD = 17.5 MXN, 1 USD = 0.92 EUR).
Форма
Список валют, каждая строка — одна валюта:
Поле
Описание
Rate (rate)
Сколько единиц валюты за 1 USD (число, до 4 знаков)
Code (code)
Код валюты — свободный ввод, авто-uppercase (MXN, EUR, MDL…). Обязателен
Name (name)
Название валюты (опционально, напр. «Mexican Peso»)
+ Add Currency — добавить строку; ✕ — удалить; Save changes — сохранить весь список. Строки без кода при сохранении отбрасываются.
Хранение
Данные лежат в MySQL app_data['exchange_rates'] как массив [{ code, name, rate }]. API: GET /exchange-rates (чтение), POST /save-exchange-rates (запись всего массива). Синхронизируется между local и Railway через db-sync.
Использование — конвертация Amazon
Эти курсы используются для приведения заказов Amazon с не-USD валютой к USD. Например, Amazon Mexico присылает заказы в MXN — при синке они конвертируются в USD по курсу MXN (USD = сумма / курс), чтобы вся аналитика была в одной валюте. Подробнее — в статье Sync · Amazon.