/** * Локальная разработка: демо для CRM продажника и раздела «Партнёры» (этап 4). * pnpm --filter @barev/api dev:crm-demo * Создаёт продажника «Давид Саргсян», лиды по этапам с точками на карте Еревана, историю и задачи. * Повторный запуск пересоздаёт демо-лиды (по имени продажника). */ import { getDb } from '@barev/db'; import { env } from '../src/env.ts'; if (env.isProd) throw new Error('Только для разработки'); const db = getDb(); const H = 3600_000; const now = Date.now(); const ago = (h: number) => new Date(now - h * H); const sales = (await db.adminUser.findFirst({ where: { name: 'Давид Саргсян' } })) ?? (await db.adminUser.create({ data: { name: 'Давид Саргсян', roles: ['SALES'] } })); const owner = await db.adminUser.findFirst({ where: { roles: { has: 'SUPERADMIN' } } }); await db.salesLead.deleteMany({ where: { ownerId: sales.id, partnerId: null, agencyId: null } }); await db.salesTask.deleteMany({ where: { assigneeId: sales.id } }); const stages = Object.fromEntries((await db.salesStage.findMany()).map((s) => [s.id, s])); const S = { new: 'stg_new', contact: 'stg_contact', meeting: 'stg_meeting', talks: 'stg_talks', lost: 'stg_lost' } as const; if (!stages[S.new]) throw new Error('Нет этапов воронки — примените миграции'); type Act = { type: 'CALL' | 'MEETING' | 'NOTE' | 'STAGE'; text: string; h: number }; async function lead( data: { kind: 'VENUE' | 'AGENCY'; name: string; category?: string; contactName?: string; phone?: string; telegram?: string; address?: string; lat?: number; lng?: number; note?: string; lostReason?: string }, stageId: string, stageH: number, acts: Act[] = [], tasks: { title: string; inH: number }[] = [], ) { const l = await db.salesLead.create({ data: { ...data, stageId, stageChangedAt: ago(stageH), ownerId: sales.id, createdAt: ago(Math.max(stageH, ...acts.map((a) => a.h)) + 2) } }); for (const a of acts) await db.salesActivity.create({ data: { leadId: l.id, authorId: sales.id, type: a.type, text: a.text, createdAt: ago(a.h) } }); for (const t of tasks) await db.salesTask.create({ data: { leadId: l.id, assigneeId: sales.id, title: t.title, dueAt: new Date(now + t.inH * H), createdById: sales.id } }); return l; } await lead({ kind: 'VENUE', name: 'Gemini Coffee', category: 'cafe', address: 'Абовяна 12', lat: 40.1835, lng: 44.5181, phone: '+374 91 222 333' }, S.new, 20); await lead({ kind: 'VENUE', name: 'Tumanyan Shaurma', category: 'restaurant', address: 'Туманяна 40', lat: 40.1868, lng: 44.5108 }, S.new, 6, [], [{ title: 'Позвонить', inH: 3 }]); await lead( { kind: 'VENUE', name: 'Ankyun Wine Bar', category: 'restaurant', contactName: 'Ани, управляющая', phone: '+374 93 555 010', telegram: '@ankyun_bar', address: 'Сарьяна 8', lat: 40.1822, lng: 44.5064 }, S.contact, 50, [ { type: 'CALL', text: 'Интересно, просят условия письменно. Собственник решает по средам.', h: 50 }, { type: 'STAGE', text: 'Контакт', h: 50 }, ], [{ title: 'Отправить условия', inH: -5 }], ); await lead( { kind: 'VENUE', name: 'Lavash Restaurant', category: 'restaurant', contactName: 'Армен, собственник', phone: '+374 99 100 200', address: 'Туманяна 21', lat: 40.1859, lng: 44.5137 }, S.meeting, 30, [ { type: 'CALL', text: 'Договорились о встрече в четверг 15:00', h: 80 }, { type: 'MEETING', text: 'Готовы дать −10% по карте и десерт в подарок для Elite. Нужен макет наклейки на дверь.', h: 30 }, ], [{ title: 'Привезти наклейку и согласовать предложения', inH: 26 }], ); await lead( { kind: 'VENUE', name: 'Salon Nuar', category: 'beauty', contactName: 'Нуне', phone: '+374 77 300 400', address: 'Пушкина 33', lat: 40.1801, lng: 44.5089 }, S.talks, 12, [{ type: 'MEETING', text: 'Хотят −15% на первое посещение, обсуждают с партнёром.', h: 12 }], [{ title: 'Узнать решение', inH: 48 }], ); await lead({ kind: 'AGENCY', name: 'Yerevan Homes', contactName: 'Лусине, директор', phone: '+374 98 777 888', telegram: '@yerevanhomes' }, S.contact, 26, [{ type: 'CALL', text: 'Каждый месяц ~15 клиентов из РФ. Интересует подарочный месяц для клиентов.', h: 26 }], [{ title: 'Встреча в офисе', inH: 20 }]); await lead({ kind: 'AGENCY', name: 'Relocate.am', contactName: 'Гор', telegram: '@relocate_am' }, S.talks, 70, [{ type: 'MEETING', text: 'Готовы, ждут ставку 12% вместо 10%.', h: 70 }]); await lead({ kind: 'VENUE', name: 'Kond Bakery', category: 'cafe', address: 'Конд', lat: 40.1773, lng: 44.4991, lostReason: 'Не устроили условия' }, S.lost, 100, [{ type: 'STAGE', text: 'Отказ: Не устроили условия', h: 100 }]); // подключённые: точка на карте у существующих партнёров, продажник — у части const partners = await db.partner.findMany({ orderBy: { createdAt: 'asc' } }); const spots: [number, number][] = [ [40.1811, 44.5136], [40.1776, 44.5126], [40.1893, 44.5153], ]; for (const [i, p] of partners.entries()) { const s = spots[i % spots.length]!; await db.partner.update({ where: { id: p.id }, data: { lat: s[0] + i * 0.0012, lng: s[1] - i * 0.0009, salesId: i === 0 ? sales.id : (p.salesId ?? owner?.id ?? null) } }); } const agency = await db.agency.findFirst({ orderBy: { createdAt: 'asc' } }); if (agency) await db.agency.update({ where: { id: agency.id }, data: { salesId: sales.id } }); // задача без лида await db.salesTask.create({ data: { assigneeId: sales.id, title: 'Обойти Каскад: кафе без партнёрства', dueAt: new Date(now + 30 * H), createdById: sales.id } }); console.log(`CRM демо готово: продажник ${sales.name}, лидов 8, партнёров на карте ${partners.length}.`); process.exit(0);