Relocations and requests in the new admin; star rating in the client bot

Orders section gets tabs: Orders / Relocations / Requests (concierge sees the
last two). Relocation list with statuses, plan progress and current step;
relocation card with per-client steps (status, client comment, rename, add,
remove), documents, quiz answers, team note, write/call. Requests: website
cleaning requests and app requests (membership, call back) with notes,
create order, turn on membership. Dashboard links point here instead of the
old admin.

Fix: leads with an unanswered 'already moved?' (NULL) were skipped by
'moved not true' filters — plan sync and linking to the app client.

Client bot: after an order is done, asks for 1-5 stars; low rating asks what
went wrong, the reply is saved as the rating comment and goes to the team chat.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
zabudko-ruandClaude Opus 5.5 committed 2026-10-21 04:17:21 +04:00
1 parent bbb25d3d0d
commit dcba7c200c
19 files changed
+1422 -22

No files matched your search

+30
View File
@@ -12,6 +12,7 @@ import {
DomainError,
getClientByTelegramId,
giveConsent,
rateOrder,
linkWebRequest,
setClientChannel,
setLocale,
@@ -40,6 +41,8 @@ const bot = new Bot(token || 'disabled:token');
/** Ожидаем ввод кода агентства от этих чатов */
const awaitingCode = new Set<number>();
/** Низкая оценка: следующий текст — комментарий к ней (chatId → заказ и оценка) */
const awaitingRatingComment = new Map<number, { orderId: string; score: number }>();
async function localeOf(from: Context['from']): Promise<Locale> {
if (!from) return 'ru';
@@ -193,11 +196,38 @@ bot.on('message:document', async (c) => {
await toTeam(c, c.message.caption ?? '', { tgFileId: d.file_id, tgBot: 'client', fileName: d.file_name ?? 'file', mime: d.mime_type ?? 'application/octet-stream', size: d.file_size ?? null });
});
/** Оценка заказа кнопкой со звёздами (сообщение приходит, когда заказ выполнен) */
bot.callbackQuery(/^rate:(\w+):([1-5])$/, async (c) => {
await c.answerCallbackQuery();
const client = await getClientByTelegramId(db, BigInt(c.from.id));
if (!client) return;
const t = createT(client.user.locale as Locale);
const score = Number(c.match[2]);
try {
await rateOrder(ctx, { orderId: c.match[1]!, clientId: client.id, score });
} catch (e) {
if (e instanceof DomainError) return void (await c.reply(t(`errors.${e.code}`)));
throw e;
}
await c.editMessageText(t('bot.client.rated', { stars: '★'.repeat(score) + '☆'.repeat(5 - score) }));
if (score <= 3) {
awaitingRatingComment.set(c.chat!.id, { orderId: c.match[1]!, score });
await c.reply(t('bot.client.ratedLow'));
}
});
/** ONB-6: ручной ввод кода агентства; любой другой текст — в чат команды */
bot.on('message:text', async (c) => {
const locale = await localeOf(c.from);
const t = createT(locale);
const client = await getClientByTelegramId(db, BigInt(c.from.id));
// комментарий к низкой оценке: сохраняем в оценку и передаём команде в чат
const pending = awaitingRatingComment.get(c.chat.id);
if (pending && client) {
awaitingRatingComment.delete(c.chat.id);
await rateOrder(ctx, { orderId: pending.orderId, clientId: client.id, score: pending.score, comment: c.message.text.slice(0, 1000) }).catch(() => undefined);
return toTeam(c, c.message.text);
}
if (!awaitingCode.has(c.chat.id)) return toTeam(c, c.message.text);
if (!client) return;
try {