Refine CRM chat UX and add DB-backed pin toggle

This commit is contained in:
Ruslan Bakiev
2026-02-19 13:51:18 +07:00
parent 626d4ddd76
commit 23a4deba37
10 changed files with 173 additions and 45 deletions

View File

@@ -388,6 +388,7 @@ async function getDashboard(auth: AuthContext | null) {
kind: m.kind === "CALL" ? "call" : "message",
direction: m.direction === "IN" ? "in" : "out",
text: m.content,
audioUrl: m.audioUrl ?? "",
duration: m.durationSec ? new Date(m.durationSec * 1000).toISOString().slice(14, 19) : "",
transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
}));
@@ -515,6 +516,7 @@ async function createCommunication(auth: AuthContext | null, input: {
kind?: "message" | "call";
direction?: "in" | "out";
text?: string;
audioUrl?: string;
at?: string;
durationSec?: number;
transcript?: string[];
@@ -540,6 +542,7 @@ async function createCommunication(auth: AuthContext | null, input: {
direction: input?.direction === "in" ? "IN" : "OUT",
channel: toDbChannel(input?.channel ?? "Phone") as any,
content: (input?.text ?? "").trim(),
audioUrl: (input?.audioUrl ?? "").trim() || null,
durationSec: typeof input?.durationSec === "number" ? input.durationSec : null,
transcriptJson: Array.isArray(input?.transcript) ? input.transcript : undefined,
occurredAt,
@@ -710,6 +713,41 @@ async function logPilotNote(auth: AuthContext | null, textInput: string) {
return { ok: true };
}
async function toggleContactPin(auth: AuthContext | null, contactInput: string, textInput: string) {
const ctx = requireAuth(auth);
const contactName = (contactInput ?? "").trim();
const text = (textInput ?? "").replace(/\s+/g, " ").trim();
if (!contactName) throw new Error("contact is required");
if (!text) throw new Error("text is required");
const contact = await prisma.contact.findFirst({
where: { teamId: ctx.teamId, name: contactName },
select: { id: true },
});
if (!contact) throw new Error("contact not found");
const existing = await prisma.contactPin.findFirst({
where: { teamId: ctx.teamId, contactId: contact.id, text },
select: { id: true },
});
if (existing) {
await prisma.contactPin.deleteMany({
where: { teamId: ctx.teamId, contactId: contact.id, text },
});
return { ok: true, pinned: false };
}
await prisma.contactPin.create({
data: {
teamId: ctx.teamId,
contactId: contact.id,
text,
},
});
return { ok: true, pinned: true };
}
export const crmGraphqlSchema = buildSchema(`
type Query {
me: MePayload!
@@ -728,6 +766,7 @@ export const crmGraphqlSchema = buildSchema(`
confirmLatestChangeSet: MutationResult!
rollbackLatestChangeSet: MutationResult!
logPilotNote(text: String!): MutationResult!
toggleContactPin(contact: String!, text: String!): PinToggleResult!
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
@@ -742,6 +781,11 @@ export const crmGraphqlSchema = buildSchema(`
id: ID!
}
type PinToggleResult {
ok: Boolean!
pinned: Boolean!
}
input CreateCalendarEventInput {
title: String!
start: String!
@@ -757,6 +801,7 @@ export const crmGraphqlSchema = buildSchema(`
kind: String
direction: String
text: String
audioUrl: String
at: String
durationSec: Int
transcript: [String!]
@@ -853,6 +898,7 @@ export const crmGraphqlSchema = buildSchema(`
kind: String!
direction: String!
text: String!
audioUrl: String!
duration: String!
transcript: [String!]!
}
@@ -958,6 +1004,9 @@ export const crmGraphqlRoot = {
logPilotNote: async (args: { text: string }, context: GraphQLContext) =>
logPilotNote(context.auth, args.text),
toggleContactPin: async (args: { contact: string; text: string }, context: GraphQLContext) =>
toggleContactPin(context.auth, args.contact, args.text),
createCalendarEvent: async (args: { input: { title: string; start: string; end?: string; contact?: string; note?: string; status?: string } }, context: GraphQLContext) =>
createCalendarEvent(context.auth, args.input),
@@ -969,6 +1018,7 @@ export const crmGraphqlRoot = {
kind?: "message" | "call";
direction?: "in" | "out";
text?: string;
audioUrl?: string;
at?: string;
durationSec?: number;
transcript?: string[];