Backoffice stage 4: partners (agencies, venues) and sales CRM
- CRM: configurable pipeline (default: New → Contact → Meeting → Negotiation → Connected / Lost), drag-and-drop board, lead card with call/meeting/note history, photos and files, tasks with Telegram reminders at the due time, map of venues (OpenStreetMap via Leaflet), lost reason. Sales reps see only their own leads. - "Connected" in one click: a venue becomes a club card partner with a one-time owner link to the work bot; an agency gets a code and a client link. The sales rep is recorded on the partner for the bonus. - Sales bonus formula in settings (per venue, per agency, % of revenue from the rep's agency clients for N months); defaults are 0 until the owner sets them. - Agencies (superadmin): clients, gift subscriptions, reward by month, payouts, client link, edit. Venues: offers by membership level (5 languages), staff, card checks, 8-week activity, owner link, map point. - Schema: SalesStage/SalesLead/SalesActivity/SalesTask; salesId on Agency and Partner; lat/lng/phone/note on Partner; agencyId on Payout. - Tests for stage rules, conversion, task reminders and bonus; dev:crm-demo. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
a848bb13e2
commit
ac786cc5cc
30 files changed
+4311
-11
No files matched your search
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
import { getDb } from '@barev/db';
|
||||
import { createT, formatAmd, formatDateTime, type Locale } from '@barev/i18n';
|
||||
import { autoAssign, chatRecipients, dueReminders, EVENTS_QUEUE, expireOffers, retryAutoAssign, type AutoAssignResult, type Ctx, type DomainEvent } from '@barev/services';
|
||||
import { autoAssign, chatRecipients, dueReminders, dueSalesTasks, EVENTS_QUEUE, expireOffers, retryAutoAssign, type AutoAssignResult, type Ctx, type DomainEvent } from '@barev/services';
|
||||
import { getFile } from '@barev/services/storage';
|
||||
import { loadRootEnv } from '@barev/services/env';
|
||||
import { createQueueNotifier, redisConnection } from '@barev/services/queue';
|
||||
@@ -286,6 +286,18 @@ async function handle(event: DomainEvent) {
|
||||
for (const a of recipients) await proBot.send({ chatId: a.telegramId!, text, buttons });
|
||||
return;
|
||||
}
|
||||
case 'sales.taskDue': {
|
||||
const task = await db.salesTask.findUnique({ where: { id: event.taskId }, include: { assignee: true, lead: true } });
|
||||
if (!task || task.doneAt || !task.assignee.telegramId || !task.assignee.active) return;
|
||||
const link =
|
||||
BACKOFFICE_URL.startsWith('https://') && task.leadId ? [{ text: adminT('bot.admin.open'), url: `${BACKOFFICE_URL}/#/crm/${task.leadId}` }] : [];
|
||||
await proBot.send({
|
||||
chatId: task.assignee.telegramId,
|
||||
text: `⏰ <b>${escapeHtml(task.title)}</b>${task.lead ? `\n${escapeHtml(task.lead.name)}${task.lead.phone ? ` · ${escapeHtml(task.lead.phone)}` : ''}` : ''}`,
|
||||
buttons: link.length ? [link] : [],
|
||||
});
|
||||
return;
|
||||
}
|
||||
case 'chat.assigned': {
|
||||
const [thread, assignee, by] = await Promise.all([
|
||||
db.chatThread.findUnique({ where: { id: event.threadId }, include: { client: { include: { user: true } }, provider: true } }),
|
||||
@@ -367,6 +379,8 @@ async function tick() {
|
||||
if (reminders || expired) console.log(`[worker] reminders=${reminders} expiredOffers=${expired}`);
|
||||
// заказы, которые ещё ждут исполнителя, — пробуем распределить снова
|
||||
for (const { orderId, res } of await retryAutoAssign(ctx)) await reportAuto(orderId, res);
|
||||
// задачи продажников, у которых наступил срок
|
||||
await dueSalesTasks(ctx);
|
||||
} catch (e) {
|
||||
console.error('[worker] tick failed', e);
|
||||
} finally {
|
||||
|
||||
Reference in new issue
Block a user