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
@@ -1,5 +1,5 @@
|
||||
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from 'react';
|
||||
import { createT, formatAmd, formatDateTime, isLocale, type Locale, type TFunction } from '@barev/i18n';
|
||||
import { createT, formatAmd, formatDateTime, isLocale, onTextOverrides, setTextOverrides, textOverridesVersion, type Locale, type TFunction } from '@barev/i18n';
|
||||
import { reportClientError } from './telegram.ts';
|
||||
|
||||
interface I18nValue {
|
||||
@@ -26,6 +26,12 @@ export function I18nProvider({ initial, children }: { initial: Locale; children:
|
||||
useEffect(() => {
|
||||
document.documentElement.lang = locale;
|
||||
}, [locale]);
|
||||
// тексты из админки подгрузились — перерисовать
|
||||
const [version, setVersion] = useState(textOverridesVersion());
|
||||
useEffect(() => {
|
||||
const off = onTextOverrides(() => setVersion(textOverridesVersion()));
|
||||
return () => void off();
|
||||
}, []);
|
||||
const value = useMemo<I18nValue>(
|
||||
() => ({
|
||||
locale,
|
||||
@@ -34,7 +40,8 @@ export function I18nProvider({ initial, children }: { initial: Locale; children:
|
||||
amd: (n) => formatAmd(locale, n),
|
||||
dt: (d, opts) => formatDateTime(locale, d, opts),
|
||||
}),
|
||||
[locale],
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[locale, version],
|
||||
);
|
||||
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
||||
}
|
||||
@@ -56,3 +63,16 @@ export function errorText(t: TFunction, err: unknown): string {
|
||||
}
|
||||
return t('errors.generic');
|
||||
}
|
||||
|
||||
/** Тексты, изменённые в админке (названия и описания услуг и т.п.) — подгрузить с API при старте Mini App */
|
||||
export async function loadTextOverrides(url = '/trpc/public.texts') {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) return;
|
||||
const body = (await res.json()) as { result?: { data?: { json?: { key: string; locale: string; value: string }[] } } };
|
||||
const rows = body.result?.data?.json;
|
||||
if (rows) setTextOverrides(rows);
|
||||
} catch {
|
||||
// нет сети — остаются стандартные тексты
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user