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
@@ -0,0 +1,140 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SalesLeadKind" AS ENUM ('VENUE', 'AGENCY');
|
||||
|
||||
-- CreateEnum
|
||||
CREATE TYPE "SalesStageKind" AS ENUM ('OPEN', 'WON', 'LOST');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Agency" ADD COLUMN "note" TEXT,
|
||||
ADD COLUMN "salesId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Partner" ADD COLUMN "lat" DOUBLE PRECISION,
|
||||
ADD COLUMN "lng" DOUBLE PRECISION,
|
||||
ADD COLUMN "note" TEXT,
|
||||
ADD COLUMN "phone" TEXT,
|
||||
ADD COLUMN "salesId" TEXT;
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "Payout" ADD COLUMN "agencyId" TEXT;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SalesStage" (
|
||||
"id" TEXT NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"kind" "SalesStageKind" NOT NULL DEFAULT 'OPEN',
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"color" TEXT NOT NULL DEFAULT 'blue',
|
||||
"active" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SalesStage_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SalesLead" (
|
||||
"id" TEXT NOT NULL,
|
||||
"kind" "SalesLeadKind" NOT NULL,
|
||||
"name" TEXT NOT NULL,
|
||||
"category" TEXT,
|
||||
"contactName" TEXT,
|
||||
"phone" TEXT,
|
||||
"telegram" TEXT,
|
||||
"address" TEXT,
|
||||
"lat" DOUBLE PRECISION,
|
||||
"lng" DOUBLE PRECISION,
|
||||
"stageId" TEXT NOT NULL,
|
||||
"stageChangedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"ownerId" TEXT,
|
||||
"note" TEXT,
|
||||
"lostReason" TEXT,
|
||||
"partnerId" TEXT,
|
||||
"agencyId" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "SalesLead_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SalesActivity" (
|
||||
"id" TEXT NOT NULL,
|
||||
"leadId" TEXT NOT NULL,
|
||||
"authorId" TEXT,
|
||||
"type" TEXT NOT NULL,
|
||||
"text" TEXT,
|
||||
"fileKey" TEXT,
|
||||
"fileName" TEXT,
|
||||
"mime" TEXT,
|
||||
"size" INTEGER,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SalesActivity_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "SalesTask" (
|
||||
"id" TEXT NOT NULL,
|
||||
"leadId" TEXT,
|
||||
"assigneeId" TEXT NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"dueAt" TIMESTAMP(3) NOT NULL,
|
||||
"doneAt" TIMESTAMP(3),
|
||||
"remindedAt" TIMESTAMP(3),
|
||||
"createdById" TEXT,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "SalesTask_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SalesLead_ownerId_stageId_idx" ON "SalesLead"("ownerId", "stageId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SalesActivity_leadId_createdAt_idx" ON "SalesActivity"("leadId", "createdAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "SalesTask_assigneeId_doneAt_dueAt_idx" ON "SalesTask"("assigneeId", "doneAt", "dueAt");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "Payout_agencyId_createdAt_idx" ON "Payout"("agencyId", "createdAt");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Agency" ADD CONSTRAINT "Agency_salesId_fkey" FOREIGN KEY ("salesId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Partner" ADD CONSTRAINT "Partner_salesId_fkey" FOREIGN KEY ("salesId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "Payout" ADD CONSTRAINT "Payout_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesLead" ADD CONSTRAINT "SalesLead_stageId_fkey" FOREIGN KEY ("stageId") REFERENCES "SalesStage"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesLead" ADD CONSTRAINT "SalesLead_ownerId_fkey" FOREIGN KEY ("ownerId") REFERENCES "AdminUser"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesLead" ADD CONSTRAINT "SalesLead_partnerId_fkey" FOREIGN KEY ("partnerId") REFERENCES "Partner"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesLead" ADD CONSTRAINT "SalesLead_agencyId_fkey" FOREIGN KEY ("agencyId") REFERENCES "Agency"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesActivity" ADD CONSTRAINT "SalesActivity_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "SalesLead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesTask" ADD CONSTRAINT "SalesTask_leadId_fkey" FOREIGN KEY ("leadId") REFERENCES "SalesLead"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "SalesTask" ADD CONSTRAINT "SalesTask_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "AdminUser"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
|
||||
-- Воронка по умолчанию (по ТЗ): Новый → Контакт → Встреча → Переговоры → Подключён / Отказ; суперадмин меняет в CRM
|
||||
INSERT INTO "SalesStage" ("id", "name", "kind", "sortOrder", "color") VALUES
|
||||
('stg_new', 'Новый', 'OPEN', 0, 'blue'),
|
||||
('stg_contact', 'Контакт', 'OPEN', 1, 'learn'),
|
||||
('stg_meeting', 'Встреча', 'OPEN', 2, 'docs'),
|
||||
('stg_talks', 'Переговоры', 'OPEN', 3, 'perks'),
|
||||
('stg_won', 'Подключён', 'WON', 4, 'home'),
|
||||
('stg_lost', 'Отказ', 'LOST', 5, 'pink');
|
||||
@@ -664,12 +664,19 @@ model Agency {
|
||||
commissionPct Int @default(10)
|
||||
contact String?
|
||||
active Boolean @default(true)
|
||||
/// продажник, который подключил агентство (бонус в CRM)
|
||||
salesId String?
|
||||
sales AdminUser? @relation("AgencySales", fields: [salesId], references: [id])
|
||||
/// заметка команды
|
||||
note String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
clients Client[]
|
||||
commissions AgencyCommission[]
|
||||
promoCodes PromoCode[]
|
||||
payouts Payout[]
|
||||
leads SalesLead[]
|
||||
}
|
||||
|
||||
model AgencyCommission {
|
||||
@@ -755,6 +762,14 @@ model Partner {
|
||||
offer Json
|
||||
terms Json?
|
||||
address String?
|
||||
/// точка на карте заведений
|
||||
lat Float?
|
||||
lng Float?
|
||||
phone String?
|
||||
/// продажник, который подключил заведение (бонус в CRM)
|
||||
salesId String?
|
||||
sales AdminUser? @relation("PartnerSales", fields: [salesId], references: [id])
|
||||
note String?
|
||||
active Boolean @default(true)
|
||||
/// одноразовая ссылка для собственника: t.me/<рабочий бот>?start=po-<код> (выдаёт админка)
|
||||
ownerInviteCode String? @unique
|
||||
@@ -764,6 +779,7 @@ model Partner {
|
||||
offers PartnerOffer[]
|
||||
members PartnerMember[]
|
||||
invites PartnerInvite[]
|
||||
leads SalesLead[]
|
||||
}
|
||||
|
||||
enum OfferLimit {
|
||||
@@ -850,6 +866,9 @@ model Payout {
|
||||
provider Provider? @relation(fields: [providerId], references: [id])
|
||||
companyId String?
|
||||
company ProviderCompany? @relation(fields: [companyId], references: [id])
|
||||
/// вознаграждение агентству
|
||||
agencyId String?
|
||||
agency Agency? @relation(fields: [agencyId], references: [id])
|
||||
periodStart DateTime
|
||||
periodEnd DateTime
|
||||
amount Int
|
||||
@@ -861,6 +880,7 @@ model Payout {
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([providerId, createdAt])
|
||||
@@index([agencyId, createdAt])
|
||||
}
|
||||
|
||||
// ───────────── Админка ─────────────
|
||||
@@ -891,6 +911,10 @@ model AdminUser {
|
||||
segments SavedSegment[]
|
||||
chatThreads ChatThread[]
|
||||
chatTemplates ChatTemplate[]
|
||||
salesAgencies Agency[] @relation("AgencySales")
|
||||
salesPartners Partner[] @relation("PartnerSales")
|
||||
salesLeads SalesLead[] @relation("LeadOwner")
|
||||
salesTasks SalesTask[] @relation("TaskAssignee")
|
||||
}
|
||||
|
||||
/// Вход в новую админку через Telegram: страница показывает ссылку t.me/<рабочий бот>?start=lg-<code>,
|
||||
@@ -1246,3 +1270,102 @@ model ChatTemplate {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ───────────── CRM продажника (этап 4) ─────────────
|
||||
|
||||
enum SalesLeadKind {
|
||||
/// кафе, ресторан, магазин… — станет партнёром клубной карты
|
||||
VENUE
|
||||
/// агентство недвижимости / релокации — будет приводить клиентов
|
||||
AGENCY
|
||||
}
|
||||
|
||||
enum SalesStageKind {
|
||||
OPEN
|
||||
/// подключён — лид становится партнёром или агентством
|
||||
WON
|
||||
LOST
|
||||
}
|
||||
|
||||
/// Этап воронки; набор и порядок настраивает суперадмин
|
||||
model SalesStage {
|
||||
id String @id @default(cuid())
|
||||
name String
|
||||
kind SalesStageKind @default(OPEN)
|
||||
sortOrder Int @default(0)
|
||||
/// пастельный цвет колонки: pink | blue | home | learn | docs | perks
|
||||
color String @default("blue")
|
||||
active Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
leads SalesLead[]
|
||||
}
|
||||
|
||||
model SalesLead {
|
||||
id String @id @default(cuid())
|
||||
kind SalesLeadKind
|
||||
name String
|
||||
/// для заведений: cafe | restaurant | shop | beauty | other
|
||||
category String?
|
||||
contactName String?
|
||||
phone String?
|
||||
telegram String?
|
||||
address String?
|
||||
lat Float?
|
||||
lng Float?
|
||||
stageId String
|
||||
stage SalesStage @relation(fields: [stageId], references: [id])
|
||||
stageChangedAt DateTime @default(now())
|
||||
/// продажник, который ведёт лид
|
||||
ownerId String?
|
||||
owner AdminUser? @relation("LeadOwner", fields: [ownerId], references: [id])
|
||||
note String?
|
||||
lostReason String?
|
||||
/// во что превратился лид после «Подключён»
|
||||
partnerId String?
|
||||
partner Partner? @relation(fields: [partnerId], references: [id])
|
||||
agencyId String?
|
||||
agency Agency? @relation(fields: [agencyId], references: [id])
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
activities SalesActivity[]
|
||||
tasks SalesTask[]
|
||||
|
||||
@@index([ownerId, stageId])
|
||||
}
|
||||
|
||||
/// История контактов: звонок, встреча, заметка, фото/файл, смена этапа
|
||||
model SalesActivity {
|
||||
id String @id @default(cuid())
|
||||
leadId String
|
||||
lead SalesLead @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
authorId String?
|
||||
/// CALL | MEETING | NOTE | FILE | STAGE | WON
|
||||
type String
|
||||
text String?
|
||||
fileKey String?
|
||||
fileName String?
|
||||
mime String?
|
||||
size Int?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([leadId, createdAt])
|
||||
}
|
||||
|
||||
/// Задача продажника с напоминанием в Telegram (рабочий бот) в срок
|
||||
model SalesTask {
|
||||
id String @id @default(cuid())
|
||||
leadId String?
|
||||
lead SalesLead? @relation(fields: [leadId], references: [id], onDelete: Cascade)
|
||||
assigneeId String
|
||||
assignee AdminUser @relation("TaskAssignee", fields: [assigneeId], references: [id])
|
||||
title String
|
||||
dueAt DateTime
|
||||
doneAt DateTime?
|
||||
remindedAt DateTime?
|
||||
createdById String?
|
||||
createdAt DateTime @default(now())
|
||||
|
||||
@@index([assigneeId, doneAt, dueAt])
|
||||
}
|
||||
Reference in new issue
Block a user