Backoffice stage 5: products, broadcasts, activity log, settings
- Products: services with full setup — content (name, description, extras names in 5 languages via text overrides), price tiers and extras, booking window, late-cancel fee, on/off, checklist; category discounts by membership level; membership plan prices; promo codes; relocation steps and documents. - Text overrides: TextOverride table on top of the i18n files; API, bots and the worker reload them every minute, mini apps fetch public.texts at start. - Promo codes applied to orders (admin new-order form and client API): not stacked with membership discount — the larger wins; once per client; validity, limit and categories. - Broadcasts to the client bot by any client filter, per-language texts with Russian fallback, test to yourself, ~25 msg/s, per-recipient status, resume after worker restart, history with progress and stop. - Activity log (who/when/what, filters, details) and Settings page with all owner settings grouped, plus payment methods. - Tests for promo codes and text overrides. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ac786cc5cc
commit
0edc6432b7
34 files changed
+2704
-26
No files matched your search
@@ -56,12 +56,43 @@ function interpolate(s: string, params?: TParams) {
|
||||
* выбираются по params.count. Отсутствующий ключ возвращается как есть — его ловит `pnpm i18n:check`.
|
||||
*/
|
||||
const merged = new Map<Locale, Messages>();
|
||||
/** Тексты, изменённые в админке (TextOverride): поверх переводов из locales/*.json */
|
||||
const overrides: Partial<Record<Locale, Messages>> = {};
|
||||
let overridesVersion = 0;
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
function withFallback(locale: Locale): Messages {
|
||||
let m = merged.get(locale);
|
||||
if (!m) merged.set(locale, (m = { ...resources.ru, ...(resources[locale] ?? {}) }));
|
||||
// свой перевод языка важнее русской правки; своя правка языка важнее всего
|
||||
if (!m) merged.set(locale, (m = { ...resources.ru, ...(overrides.ru ?? {}), ...(resources[locale] ?? {}), ...(overrides[locale] ?? {}) }));
|
||||
return m;
|
||||
}
|
||||
|
||||
/**
|
||||
* Подставить тексты из админки. Вызывают API, боты и воркер (из БД) и Mini App (из public.texts).
|
||||
* После вызова createT отдаёт новые тексты; подписчики (React-провайдер) перерисовываются.
|
||||
*/
|
||||
export function setTextOverrides(rows: { key: string; locale: string; value: string }[]) {
|
||||
for (const l of LOCALES) overrides[l] = {};
|
||||
for (const r of rows) if (isLocale(r.locale) && r.value) overrides[r.locale]![r.key] = r.value;
|
||||
merged.clear();
|
||||
overridesVersion++;
|
||||
for (const cb of listeners) cb();
|
||||
}
|
||||
|
||||
/** Номер версии правок — чтобы мемоизированный t пересоздавался */
|
||||
export const textOverridesVersion = () => overridesVersion;
|
||||
|
||||
export function onTextOverrides(cb: () => void) {
|
||||
listeners.add(cb);
|
||||
return () => listeners.delete(cb);
|
||||
}
|
||||
|
||||
/** Стандартный перевод (без правок из админки) — подсказка в форме редактирования */
|
||||
export function defaultText(locale: Locale, key: string): string | null {
|
||||
return resources[locale]?.[key] ?? null;
|
||||
}
|
||||
|
||||
export function createT(locale: Locale): TFunction {
|
||||
return tFromMessages(locale, withFallback(locale));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user