Add follow-up status tracking slice
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
import { v } from "convex/values";
|
||||
|
||||
import {
|
||||
DO_NOT_CONTACT_RECHECK_MS,
|
||||
FOLLOW_UP_DUE_DELAY_MS,
|
||||
shouldCreateFollowUpDraftAfterSend,
|
||||
} from "../lib/outreach-follow-up";
|
||||
import { normalizeListLimit } from "./domain";
|
||||
import { internalMutation, mutation, query } from "./_generated/server";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
@@ -11,6 +16,19 @@ const strategy = v.union(
|
||||
v.literal("defer"),
|
||||
v.literal("do_not_contact"),
|
||||
);
|
||||
const manualSalesStatus = v.union(
|
||||
v.literal("follow_up_planned"),
|
||||
v.literal("follow_up_sent"),
|
||||
v.literal("reply_received"),
|
||||
v.literal("not_interested"),
|
||||
v.literal("later"),
|
||||
v.literal("meeting_scheduled"),
|
||||
v.literal("proposal_requested"),
|
||||
v.literal("proposal_sent"),
|
||||
v.literal("won"),
|
||||
v.literal("lost"),
|
||||
v.literal("do_not_pursue"),
|
||||
);
|
||||
|
||||
const REVIEW_JOIN_LIMIT = 4;
|
||||
|
||||
@@ -189,6 +207,9 @@ type OutreachRecordInsertArgs = {
|
||||
emailSubject?: string;
|
||||
emailBody?: string;
|
||||
followUpDraft?: string;
|
||||
followUpDueAt?: number;
|
||||
parentOutreachId?: Id<"outreachRecords">;
|
||||
salesStatus?: "follow_up_planned" | "follow_up_sent";
|
||||
now: number;
|
||||
};
|
||||
|
||||
@@ -201,10 +222,12 @@ const buildOutreachRecordsInsertPayload = (args: OutreachRecordInsertArgs) => {
|
||||
emailSubject?: string;
|
||||
emailBody?: string;
|
||||
followUpDraft?: string;
|
||||
followUpDueAt?: number;
|
||||
parentOutreachId?: Id<"outreachRecords">;
|
||||
approvalStatus: "draft";
|
||||
sendStatus: "not_sent";
|
||||
responseStatus: "none";
|
||||
salesStatus: "follow_up_planned";
|
||||
salesStatus: "follow_up_planned" | "follow_up_sent";
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
} = {
|
||||
@@ -213,7 +236,7 @@ const buildOutreachRecordsInsertPayload = (args: OutreachRecordInsertArgs) => {
|
||||
approvalStatus: "draft",
|
||||
sendStatus: "not_sent",
|
||||
responseStatus: "none",
|
||||
salesStatus: "follow_up_planned",
|
||||
salesStatus: args.salesStatus ?? "follow_up_planned",
|
||||
createdAt: args.now,
|
||||
updatedAt: args.now,
|
||||
};
|
||||
@@ -233,10 +256,55 @@ const buildOutreachRecordsInsertPayload = (args: OutreachRecordInsertArgs) => {
|
||||
if (args.followUpDraft !== undefined) {
|
||||
payload.followUpDraft = args.followUpDraft;
|
||||
}
|
||||
if (args.followUpDueAt !== undefined) {
|
||||
payload.followUpDueAt = args.followUpDueAt;
|
||||
}
|
||||
if (args.parentOutreachId !== undefined) {
|
||||
payload.parentOutreachId = args.parentOutreachId;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
async function createFollowUpDraftAfterInitialSend(
|
||||
ctx: MutationCtx,
|
||||
outreach: Doc<"outreachRecords">,
|
||||
sentAt: number,
|
||||
) {
|
||||
const existingFollowUps = await ctx.db
|
||||
.query("outreachRecords")
|
||||
.withIndex("by_parentOutreachId", (q) => q.eq("parentOutreachId", outreach._id))
|
||||
.take(1);
|
||||
|
||||
if (
|
||||
!shouldCreateFollowUpDraftAfterSend({
|
||||
existingFollowUpOutreachCount: existingFollowUps.length,
|
||||
followUpDraft: outreach.followUpDraft,
|
||||
salesStatus: outreach.salesStatus,
|
||||
sendStatus: "sent",
|
||||
})
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await ctx.db.insert(
|
||||
"outreachRecords",
|
||||
buildOutreachRecordsInsertPayload({
|
||||
leadId: outreach.leadId,
|
||||
auditId: outreach.auditId,
|
||||
strategy: "email_first",
|
||||
emailSubject: outreach.emailSubject
|
||||
? `Kurze Nachfrage: ${outreach.emailSubject}`
|
||||
: "Kurze Nachfrage zum Website-Audit",
|
||||
emailBody: outreach.followUpDraft,
|
||||
followUpDraft: outreach.followUpDraft,
|
||||
followUpDueAt: sentAt + FOLLOW_UP_DUE_DELAY_MS,
|
||||
parentOutreachId: outreach._id,
|
||||
now: Date.now(),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export const create = mutation({
|
||||
args: {
|
||||
leadId: v.id("leads"),
|
||||
@@ -671,6 +739,7 @@ export const recordEmailSendSuccess = internalMutation({
|
||||
await ctx.db.patch(args.id, {
|
||||
sendStatus: "sent",
|
||||
sentAt: args.sentAt,
|
||||
...(outreach.parentOutreachId ? { salesStatus: "follow_up_sent" as const } : {}),
|
||||
updatedAt: now,
|
||||
});
|
||||
|
||||
@@ -729,6 +798,64 @@ export const recordEmailSendSuccess = internalMutation({
|
||||
}
|
||||
|
||||
await ctx.db.insert("outreachSendAttempts", attempt);
|
||||
await createFollowUpDraftAfterInitialSend(ctx, outreach, args.sentAt);
|
||||
},
|
||||
});
|
||||
|
||||
export const updateManualSalesStatus = mutation({
|
||||
args: {
|
||||
id: v.id("outreachRecords"),
|
||||
salesStatus: manualSalesStatus,
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await requireOperator(ctx);
|
||||
|
||||
const outreach = await ctx.db.get(args.id);
|
||||
if (!outreach) {
|
||||
throw new Error("Outreach-Datensatz wurde nicht gefunden.");
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const outreachPatch: {
|
||||
salesStatus: typeof args.salesStatus;
|
||||
responseStatus?: "none" | "manual_reply_recorded" | "no_interest" | "follow_up_needed";
|
||||
doNotContactUntil?: number;
|
||||
updatedAt: number;
|
||||
} = {
|
||||
salesStatus: args.salesStatus,
|
||||
updatedAt: now,
|
||||
};
|
||||
const leadPatch: {
|
||||
contactStatus?: "contacted" | "replied" | "do_not_contact";
|
||||
updatedAt: number;
|
||||
} = {
|
||||
updatedAt: now,
|
||||
};
|
||||
|
||||
if (args.salesStatus === "reply_received") {
|
||||
outreachPatch.responseStatus = "manual_reply_recorded";
|
||||
leadPatch.contactStatus = "replied";
|
||||
}
|
||||
|
||||
if (args.salesStatus === "not_interested") {
|
||||
outreachPatch.responseStatus = "no_interest";
|
||||
leadPatch.contactStatus = "contacted";
|
||||
}
|
||||
|
||||
if (args.salesStatus === "do_not_pursue") {
|
||||
outreachPatch.responseStatus = "no_interest";
|
||||
outreachPatch.doNotContactUntil = now + DO_NOT_CONTACT_RECHECK_MS;
|
||||
leadPatch.contactStatus = "do_not_contact";
|
||||
}
|
||||
|
||||
await ctx.db.patch(args.id, outreachPatch);
|
||||
await ctx.db.patch(outreach.leadId, leadPatch);
|
||||
|
||||
return {
|
||||
id: args.id,
|
||||
salesStatus: args.salesStatus,
|
||||
doNotContactUntil: outreachPatch.doNotContactUntil ?? null,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -484,11 +484,14 @@ export default defineSchema({
|
||||
emailSubject: v.optional(v.string()),
|
||||
emailBody: v.optional(v.string()),
|
||||
followUpDraft: v.optional(v.string()),
|
||||
followUpDueAt: v.optional(v.number()),
|
||||
parentOutreachId: v.optional(v.id("outreachRecords")),
|
||||
approvalStatus: outreachApprovalStatus,
|
||||
sendStatus: outreachSendStatus,
|
||||
sentAt: v.optional(v.number()),
|
||||
responseStatus: outreachResponseStatus,
|
||||
salesStatus: outreachSalesStatus,
|
||||
doNotContactUntil: v.optional(v.number()),
|
||||
createdAt: v.number(),
|
||||
updatedAt: v.number(),
|
||||
})
|
||||
@@ -502,7 +505,8 @@ export default defineSchema({
|
||||
"updatedAt",
|
||||
])
|
||||
.index("by_sendStatus", ["sendStatus"])
|
||||
.index("by_sendStatus_and_updatedAt", ["sendStatus", "updatedAt"]),
|
||||
.index("by_sendStatus_and_updatedAt", ["sendStatus", "updatedAt"])
|
||||
.index("by_parentOutreachId", ["parentOutreachId"]),
|
||||
|
||||
outreachSendAttempts: defineTable({
|
||||
outreachId: v.id("outreachRecords"),
|
||||
|
||||
Reference in New Issue
Block a user