feat(chat): threads UI + graphql flow + qwen/gigachat integration
This commit is contained in:
781
Frontend/server/graphql/schema.ts
Normal file
781
Frontend/server/graphql/schema.ts
Normal file
@@ -0,0 +1,781 @@
|
||||
import { buildSchema } from "graphql";
|
||||
import type { H3Event } from "h3";
|
||||
import type { AuthContext } from "../utils/auth";
|
||||
import { clearAuthSession, setSession } from "../utils/auth";
|
||||
import { prisma } from "../utils/prisma";
|
||||
import { normalizePhone, verifyPassword } from "../utils/password";
|
||||
import { persistChatMessage, runCrmAgentFor } from "../agent/crmAgent";
|
||||
|
||||
type GraphQLContext = {
|
||||
auth: AuthContext | null;
|
||||
event: H3Event;
|
||||
};
|
||||
|
||||
function requireAuth(auth: AuthContext | null) {
|
||||
if (!auth) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
return auth;
|
||||
}
|
||||
|
||||
function mapChannel(channel: string) {
|
||||
if (channel === "TELEGRAM") return "Telegram";
|
||||
if (channel === "WHATSAPP") return "WhatsApp";
|
||||
if (channel === "INSTAGRAM") return "Instagram";
|
||||
if (channel === "EMAIL") return "Email";
|
||||
return "Phone";
|
||||
}
|
||||
|
||||
function toDbChannel(channel: string) {
|
||||
const c = channel.toLowerCase();
|
||||
if (c === "telegram") return "TELEGRAM";
|
||||
if (c === "whatsapp") return "WHATSAPP";
|
||||
if (c === "instagram") return "INSTAGRAM";
|
||||
if (c === "email") return "EMAIL";
|
||||
return "PHONE";
|
||||
}
|
||||
|
||||
async function loginWithPassword(event: H3Event, phoneInput: string, passwordInput: string) {
|
||||
const phone = normalizePhone(phoneInput);
|
||||
const password = (passwordInput ?? "").trim();
|
||||
|
||||
if (!phone) {
|
||||
throw new Error("phone is required");
|
||||
}
|
||||
if (!password) {
|
||||
throw new Error("password is required");
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { phone },
|
||||
include: {
|
||||
memberships: {
|
||||
orderBy: { createdAt: "asc" },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!user || !verifyPassword(password, user.passwordHash)) {
|
||||
throw new Error("invalid credentials");
|
||||
}
|
||||
|
||||
const membership = user.memberships[0];
|
||||
if (!membership) {
|
||||
throw new Error("user has no team access");
|
||||
}
|
||||
|
||||
const conversation =
|
||||
(await prisma.chatConversation.findFirst({
|
||||
where: { teamId: membership.teamId, createdByUserId: user.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
})) ||
|
||||
(await prisma.chatConversation.create({
|
||||
data: { teamId: membership.teamId, createdByUserId: user.id, title: "Pilot" },
|
||||
}));
|
||||
|
||||
setSession(event, {
|
||||
teamId: membership.teamId,
|
||||
userId: user.id,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function getAuthPayload(auth: AuthContext | null) {
|
||||
const ctx = requireAuth(auth);
|
||||
const [user, team, conv] = await Promise.all([
|
||||
prisma.user.findUnique({ where: { id: ctx.userId } }),
|
||||
prisma.team.findUnique({ where: { id: ctx.teamId } }),
|
||||
prisma.chatConversation.findUnique({
|
||||
where: { id: ctx.conversationId },
|
||||
include: { messages: { orderBy: { createdAt: "desc" }, take: 1, select: { text: true, createdAt: true } } },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!user || !team || !conv) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
return {
|
||||
user: { id: user.id, phone: user.phone, name: user.name },
|
||||
team: { id: team.id, name: team.name },
|
||||
conversation: {
|
||||
id: conv.id,
|
||||
title: conv.title ?? "New chat",
|
||||
createdAt: conv.createdAt.toISOString(),
|
||||
updatedAt: conv.updatedAt.toISOString(),
|
||||
lastMessageAt: conv.messages[0]?.createdAt?.toISOString?.() ?? null,
|
||||
lastMessageText: conv.messages[0]?.text ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function defaultConversationTitle(input?: string | null) {
|
||||
const value = (input ?? "").trim();
|
||||
if (value) return value;
|
||||
const stamp = new Intl.DateTimeFormat("en-GB", {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
}).format(new Date());
|
||||
return `Chat ${stamp}`;
|
||||
}
|
||||
|
||||
async function getChatConversations(auth: AuthContext | null) {
|
||||
const ctx = requireAuth(auth);
|
||||
|
||||
const items = await prisma.chatConversation.findMany({
|
||||
where: { teamId: ctx.teamId, createdByUserId: ctx.userId },
|
||||
include: {
|
||||
messages: {
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 1,
|
||||
select: { text: true, createdAt: true },
|
||||
},
|
||||
},
|
||||
take: 100,
|
||||
});
|
||||
|
||||
return items
|
||||
.map((c) => ({
|
||||
id: c.id,
|
||||
title: defaultConversationTitle(c.title),
|
||||
createdAt: c.createdAt.toISOString(),
|
||||
updatedAt: c.updatedAt.toISOString(),
|
||||
lastMessageAt: c.messages[0]?.createdAt?.toISOString?.() ?? null,
|
||||
lastMessageText: c.messages[0]?.text ?? null,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
const aTime = a.lastMessageAt ?? a.updatedAt;
|
||||
const bTime = b.lastMessageAt ?? b.updatedAt;
|
||||
return bTime.localeCompare(aTime);
|
||||
});
|
||||
}
|
||||
|
||||
async function createChatConversation(auth: AuthContext | null, event: H3Event, titleInput?: string | null) {
|
||||
const ctx = requireAuth(auth);
|
||||
|
||||
const conversation = await prisma.chatConversation.create({
|
||||
data: {
|
||||
teamId: ctx.teamId,
|
||||
createdByUserId: ctx.userId,
|
||||
title: defaultConversationTitle(titleInput),
|
||||
},
|
||||
});
|
||||
|
||||
setSession(event, {
|
||||
teamId: ctx.teamId,
|
||||
userId: ctx.userId,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: conversation.id,
|
||||
title: defaultConversationTitle(conversation.title),
|
||||
createdAt: conversation.createdAt.toISOString(),
|
||||
updatedAt: conversation.updatedAt.toISOString(),
|
||||
lastMessageAt: null,
|
||||
lastMessageText: null,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectChatConversation(auth: AuthContext | null, event: H3Event, id: string) {
|
||||
const ctx = requireAuth(auth);
|
||||
const convId = (id ?? "").trim();
|
||||
if (!convId) throw new Error("id is required");
|
||||
|
||||
const conversation = await prisma.chatConversation.findFirst({
|
||||
where: {
|
||||
id: convId,
|
||||
teamId: ctx.teamId,
|
||||
createdByUserId: ctx.userId,
|
||||
},
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!conversation) throw new Error("conversation not found");
|
||||
|
||||
setSession(event, {
|
||||
teamId: ctx.teamId,
|
||||
userId: ctx.userId,
|
||||
conversationId: conversation.id,
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function getChatMessages(auth: AuthContext | null) {
|
||||
const ctx = requireAuth(auth);
|
||||
const items = await prisma.chatMessage.findMany({
|
||||
where: { teamId: ctx.teamId, conversationId: ctx.conversationId },
|
||||
orderBy: { createdAt: "asc" },
|
||||
take: 200,
|
||||
});
|
||||
|
||||
return items.map((m) => {
|
||||
const debug = (m.planJson as any) ?? {};
|
||||
return {
|
||||
id: m.id,
|
||||
role: m.role === "USER" ? "user" : m.role === "ASSISTANT" ? "assistant" : "system",
|
||||
text: m.text,
|
||||
plan: Array.isArray(debug.steps) ? (debug.steps as string[]) : [],
|
||||
thinking: Array.isArray(debug.thinking) ? (debug.thinking as string[]) : Array.isArray(debug.steps) ? (debug.steps as string[]) : [],
|
||||
tools: Array.isArray(debug.tools) ? (debug.tools as string[]) : [],
|
||||
toolRuns: Array.isArray(debug.toolRuns)
|
||||
? (debug.toolRuns as any[])
|
||||
.filter((t) => t && typeof t === "object")
|
||||
.map((t: any) => ({
|
||||
name: String(t.name ?? "crm:unknown"),
|
||||
status: t.status === "error" ? "error" : "ok",
|
||||
input: String(t.input ?? ""),
|
||||
output: String(t.output ?? ""),
|
||||
at: t.at ? String(t.at) : m.createdAt.toISOString(),
|
||||
}))
|
||||
: [],
|
||||
createdAt: m.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function getDashboard(auth: AuthContext | null) {
|
||||
const ctx = requireAuth(auth);
|
||||
const from = new Date(Date.now() - 1000 * 60 * 60 * 24 * 30);
|
||||
const to = new Date(Date.now() + 1000 * 60 * 60 * 24 * 60);
|
||||
|
||||
const [
|
||||
contactsRaw,
|
||||
communicationsRaw,
|
||||
calendarRaw,
|
||||
dealsRaw,
|
||||
feedRaw,
|
||||
pinsRaw,
|
||||
documentsRaw,
|
||||
] = await Promise.all([
|
||||
prisma.contact.findMany({
|
||||
where: { teamId: ctx.teamId },
|
||||
include: {
|
||||
note: { select: { content: true } },
|
||||
messages: { select: { occurredAt: true }, orderBy: { occurredAt: "desc" }, take: 1 },
|
||||
},
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 500,
|
||||
}),
|
||||
prisma.contactMessage.findMany({
|
||||
where: { contact: { teamId: ctx.teamId } },
|
||||
orderBy: { occurredAt: "asc" },
|
||||
take: 2000,
|
||||
include: { contact: { select: { id: true, name: true } } },
|
||||
}),
|
||||
prisma.calendarEvent.findMany({
|
||||
where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
|
||||
include: { contact: { select: { name: true } } },
|
||||
orderBy: { startsAt: "asc" },
|
||||
take: 500,
|
||||
}),
|
||||
prisma.deal.findMany({
|
||||
where: { teamId: ctx.teamId },
|
||||
include: { contact: { select: { name: true, company: true } } },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 500,
|
||||
}),
|
||||
prisma.feedCard.findMany({
|
||||
where: { teamId: ctx.teamId },
|
||||
include: { contact: { select: { name: true } } },
|
||||
orderBy: { happenedAt: "desc" },
|
||||
take: 200,
|
||||
}),
|
||||
prisma.contactPin.findMany({
|
||||
where: { teamId: ctx.teamId },
|
||||
include: { contact: { select: { name: true } } },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 500,
|
||||
}),
|
||||
prisma.workspaceDocument.findMany({
|
||||
where: { teamId: ctx.teamId },
|
||||
orderBy: { updatedAt: "desc" },
|
||||
take: 200,
|
||||
}),
|
||||
]);
|
||||
|
||||
const channelsByContactId = new Map<string, Set<string>>();
|
||||
for (const item of communicationsRaw) {
|
||||
if (!channelsByContactId.has(item.contactId)) {
|
||||
channelsByContactId.set(item.contactId, new Set());
|
||||
}
|
||||
channelsByContactId.get(item.contactId)?.add(mapChannel(item.channel));
|
||||
}
|
||||
|
||||
const contacts = contactsRaw.map((c) => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
avatar: c.avatarUrl ?? "",
|
||||
company: c.company ?? "",
|
||||
country: c.country ?? "",
|
||||
location: c.location ?? "",
|
||||
channels: Array.from(channelsByContactId.get(c.id) ?? []),
|
||||
lastContactAt: c.messages[0]?.occurredAt?.toISOString?.() ?? c.updatedAt.toISOString(),
|
||||
description: c.note?.content ?? "",
|
||||
}));
|
||||
|
||||
const communications = communicationsRaw.map((m) => ({
|
||||
id: m.id,
|
||||
at: m.occurredAt.toISOString(),
|
||||
contactId: m.contactId,
|
||||
contact: m.contact.name,
|
||||
channel: mapChannel(m.channel),
|
||||
kind: m.kind === "CALL" ? "call" : "message",
|
||||
direction: m.direction === "IN" ? "in" : "out",
|
||||
text: m.content,
|
||||
duration: m.durationSec ? new Date(m.durationSec * 1000).toISOString().slice(14, 19) : "",
|
||||
transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
|
||||
}));
|
||||
|
||||
const calendar = calendarRaw.map((e) => ({
|
||||
id: e.id,
|
||||
title: e.title,
|
||||
start: e.startsAt.toISOString(),
|
||||
end: (e.endsAt ?? e.startsAt).toISOString(),
|
||||
contact: e.contact?.name ?? "",
|
||||
note: e.note ?? "",
|
||||
}));
|
||||
|
||||
const deals = dealsRaw.map((d) => ({
|
||||
id: d.id,
|
||||
contact: d.contact.name,
|
||||
title: d.title,
|
||||
company: d.contact.company ?? "",
|
||||
stage: d.stage,
|
||||
amount: d.amount ? String(d.amount) : "",
|
||||
nextStep: d.nextStep ?? "",
|
||||
summary: d.summary ?? "",
|
||||
}));
|
||||
|
||||
const feed = feedRaw.map((c) => ({
|
||||
id: c.id,
|
||||
at: c.happenedAt.toISOString(),
|
||||
contact: c.contact?.name ?? "",
|
||||
text: c.text,
|
||||
proposal: {
|
||||
title: ((c.proposalJson as any)?.title ?? "") as string,
|
||||
details: (Array.isArray((c.proposalJson as any)?.details) ? (c.proposalJson as any).details : []) as string[],
|
||||
key: ((c.proposalJson as any)?.key ?? "") as string,
|
||||
},
|
||||
decision: c.decision === "ACCEPTED" ? "accepted" : c.decision === "REJECTED" ? "rejected" : "pending",
|
||||
decisionNote: c.decisionNote ?? "",
|
||||
}));
|
||||
|
||||
const pins = pinsRaw.map((p) => ({
|
||||
id: p.id,
|
||||
contact: p.contact.name,
|
||||
text: p.text,
|
||||
}));
|
||||
|
||||
const documents = documentsRaw.map((d) => ({
|
||||
id: d.id,
|
||||
title: d.title,
|
||||
type: d.type,
|
||||
owner: d.owner,
|
||||
scope: d.scope,
|
||||
updatedAt: d.updatedAt.toISOString(),
|
||||
summary: d.summary,
|
||||
body: d.body,
|
||||
}));
|
||||
|
||||
return {
|
||||
contacts,
|
||||
communications,
|
||||
calendar,
|
||||
deals,
|
||||
feed,
|
||||
pins,
|
||||
documents,
|
||||
};
|
||||
}
|
||||
|
||||
async function createCalendarEvent(auth: AuthContext | null, input: {
|
||||
title: string;
|
||||
start: string;
|
||||
end?: string;
|
||||
contact?: string;
|
||||
note?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
const ctx = requireAuth(auth);
|
||||
|
||||
const title = (input?.title ?? "").trim();
|
||||
const start = input?.start ? new Date(input.start) : null;
|
||||
const end = input?.end ? new Date(input.end) : null;
|
||||
|
||||
if (!title) throw new Error("title is required");
|
||||
if (!start || Number.isNaN(start.getTime())) throw new Error("start is invalid");
|
||||
|
||||
const contactName = (input?.contact ?? "").trim();
|
||||
const contact = contactName
|
||||
? await prisma.contact.findFirst({ where: { teamId: ctx.teamId, name: contactName }, select: { id: true, name: true } })
|
||||
: null;
|
||||
|
||||
const created = await prisma.calendarEvent.create({
|
||||
data: {
|
||||
teamId: ctx.teamId,
|
||||
contactId: contact?.id ?? null,
|
||||
title,
|
||||
startsAt: start,
|
||||
endsAt: end && !Number.isNaN(end.getTime()) ? end : null,
|
||||
note: (input?.note ?? "").trim() || null,
|
||||
status: (input?.status ?? "").trim() || null,
|
||||
},
|
||||
include: { contact: { select: { name: true } } },
|
||||
});
|
||||
|
||||
return {
|
||||
id: created.id,
|
||||
title: created.title,
|
||||
start: created.startsAt.toISOString(),
|
||||
end: (created.endsAt ?? created.startsAt).toISOString(),
|
||||
contact: created.contact?.name ?? "",
|
||||
note: created.note ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
async function createCommunication(auth: AuthContext | null, input: {
|
||||
contact: string;
|
||||
channel?: string;
|
||||
kind?: "message" | "call";
|
||||
direction?: "in" | "out";
|
||||
text?: string;
|
||||
at?: string;
|
||||
durationSec?: number;
|
||||
transcript?: string[];
|
||||
}) {
|
||||
const ctx = requireAuth(auth);
|
||||
|
||||
const contactName = (input?.contact ?? "").trim();
|
||||
if (!contactName) throw new Error("contact 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 occurredAt = input?.at ? new Date(input.at) : new Date();
|
||||
if (Number.isNaN(occurredAt.getTime())) throw new Error("at is invalid");
|
||||
|
||||
const created = await prisma.contactMessage.create({
|
||||
data: {
|
||||
contactId: contact.id,
|
||||
kind: input?.kind === "call" ? "CALL" : "MESSAGE",
|
||||
direction: input?.direction === "in" ? "IN" : "OUT",
|
||||
channel: toDbChannel(input?.channel ?? "Phone") as any,
|
||||
content: (input?.text ?? "").trim(),
|
||||
durationSec: typeof input?.durationSec === "number" ? input.durationSec : null,
|
||||
transcriptJson: Array.isArray(input?.transcript) ? input.transcript : undefined,
|
||||
occurredAt,
|
||||
},
|
||||
});
|
||||
|
||||
return { ok: true, id: created.id };
|
||||
}
|
||||
|
||||
async function updateFeedDecision(auth: AuthContext | null, id: string, decision: "accepted" | "rejected" | "pending", decisionNote?: string) {
|
||||
const ctx = requireAuth(auth);
|
||||
|
||||
if (!id) throw new Error("id is required");
|
||||
if (!decision) throw new Error("decision is required");
|
||||
|
||||
const nextDecision = decision === "accepted" ? "ACCEPTED" : decision === "rejected" ? "REJECTED" : "PENDING";
|
||||
|
||||
const res = await prisma.feedCard.updateMany({
|
||||
where: { id, teamId: ctx.teamId },
|
||||
data: { decision: nextDecision, decisionNote: decisionNote ?? null },
|
||||
});
|
||||
if (res.count === 0) throw new Error("feed card not found");
|
||||
|
||||
return { ok: true, id };
|
||||
}
|
||||
|
||||
async function sendPilotMessage(auth: AuthContext | null, textInput: string) {
|
||||
const ctx = requireAuth(auth);
|
||||
const text = (textInput ?? "").trim();
|
||||
if (!text) throw new Error("text is required");
|
||||
|
||||
await persistChatMessage({
|
||||
teamId: ctx.teamId,
|
||||
conversationId: ctx.conversationId,
|
||||
authorUserId: ctx.userId,
|
||||
role: "USER",
|
||||
text,
|
||||
});
|
||||
|
||||
const reply = await runCrmAgentFor({ teamId: ctx.teamId, userId: ctx.userId, userText: text });
|
||||
await persistChatMessage({
|
||||
teamId: ctx.teamId,
|
||||
conversationId: ctx.conversationId,
|
||||
authorUserId: null,
|
||||
role: "ASSISTANT",
|
||||
text: reply.text,
|
||||
plan: reply.plan,
|
||||
thinking: reply.thinking ?? reply.plan,
|
||||
tools: reply.tools,
|
||||
toolRuns: reply.toolRuns ?? [],
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
async function logPilotNote(auth: AuthContext | null, textInput: string) {
|
||||
const ctx = requireAuth(auth);
|
||||
const text = (textInput ?? "").trim();
|
||||
if (!text) throw new Error("text is required");
|
||||
|
||||
await persistChatMessage({
|
||||
teamId: ctx.teamId,
|
||||
conversationId: ctx.conversationId,
|
||||
authorUserId: null,
|
||||
role: "ASSISTANT",
|
||||
text,
|
||||
plan: [],
|
||||
thinking: [],
|
||||
tools: [],
|
||||
toolRuns: [],
|
||||
});
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
export const crmGraphqlSchema = buildSchema(`
|
||||
type Query {
|
||||
me: MePayload!
|
||||
chatMessages: [PilotMessage!]!
|
||||
chatConversations: [Conversation!]!
|
||||
dashboard: DashboardPayload!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
login(phone: String!, password: String!): MutationResult!
|
||||
logout: MutationResult!
|
||||
createChatConversation(title: String): Conversation!
|
||||
selectChatConversation(id: ID!): MutationResult!
|
||||
sendPilotMessage(text: String!): MutationResult!
|
||||
logPilotNote(text: String!): MutationResult!
|
||||
createCalendarEvent(input: CreateCalendarEventInput!): CalendarEvent!
|
||||
createCommunication(input: CreateCommunicationInput!): MutationWithIdResult!
|
||||
updateFeedDecision(id: ID!, decision: String!, decisionNote: String): MutationWithIdResult!
|
||||
}
|
||||
|
||||
type MutationResult {
|
||||
ok: Boolean!
|
||||
}
|
||||
|
||||
type MutationWithIdResult {
|
||||
ok: Boolean!
|
||||
id: ID!
|
||||
}
|
||||
|
||||
input CreateCalendarEventInput {
|
||||
title: String!
|
||||
start: String!
|
||||
end: String
|
||||
contact: String
|
||||
note: String
|
||||
status: String
|
||||
}
|
||||
|
||||
input CreateCommunicationInput {
|
||||
contact: String!
|
||||
channel: String
|
||||
kind: String
|
||||
direction: String
|
||||
text: String
|
||||
at: String
|
||||
durationSec: Int
|
||||
transcript: [String!]
|
||||
}
|
||||
|
||||
type MePayload {
|
||||
user: MeUser!
|
||||
team: MeTeam!
|
||||
conversation: Conversation!
|
||||
}
|
||||
|
||||
type MeUser {
|
||||
id: ID!
|
||||
phone: String!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type MeTeam {
|
||||
id: ID!
|
||||
name: String!
|
||||
}
|
||||
|
||||
type Conversation {
|
||||
id: ID!
|
||||
title: String!
|
||||
createdAt: String!
|
||||
updatedAt: String!
|
||||
lastMessageAt: String
|
||||
lastMessageText: String
|
||||
}
|
||||
|
||||
type PilotMessage {
|
||||
id: ID!
|
||||
role: String!
|
||||
text: String!
|
||||
plan: [String!]!
|
||||
thinking: [String!]!
|
||||
tools: [String!]!
|
||||
toolRuns: [PilotToolRun!]!
|
||||
createdAt: String!
|
||||
}
|
||||
|
||||
type PilotToolRun {
|
||||
name: String!
|
||||
status: String!
|
||||
input: String!
|
||||
output: String!
|
||||
at: String!
|
||||
}
|
||||
|
||||
type DashboardPayload {
|
||||
contacts: [Contact!]!
|
||||
communications: [CommItem!]!
|
||||
calendar: [CalendarEvent!]!
|
||||
deals: [Deal!]!
|
||||
feed: [FeedCard!]!
|
||||
pins: [CommPin!]!
|
||||
documents: [WorkspaceDocument!]!
|
||||
}
|
||||
|
||||
type Contact {
|
||||
id: ID!
|
||||
name: String!
|
||||
avatar: String!
|
||||
company: String!
|
||||
country: String!
|
||||
location: String!
|
||||
channels: [String!]!
|
||||
lastContactAt: String!
|
||||
description: String!
|
||||
}
|
||||
|
||||
type CommItem {
|
||||
id: ID!
|
||||
at: String!
|
||||
contactId: String!
|
||||
contact: String!
|
||||
channel: String!
|
||||
kind: String!
|
||||
direction: String!
|
||||
text: String!
|
||||
duration: String!
|
||||
transcript: [String!]!
|
||||
}
|
||||
|
||||
type CalendarEvent {
|
||||
id: ID!
|
||||
title: String!
|
||||
start: String!
|
||||
end: String!
|
||||
contact: String!
|
||||
note: String!
|
||||
}
|
||||
|
||||
type Deal {
|
||||
id: ID!
|
||||
contact: String!
|
||||
title: String!
|
||||
company: String!
|
||||
stage: String!
|
||||
amount: String!
|
||||
nextStep: String!
|
||||
summary: String!
|
||||
}
|
||||
|
||||
type FeedCard {
|
||||
id: ID!
|
||||
at: String!
|
||||
contact: String!
|
||||
text: String!
|
||||
proposal: FeedProposal!
|
||||
decision: String!
|
||||
decisionNote: String!
|
||||
}
|
||||
|
||||
type FeedProposal {
|
||||
title: String!
|
||||
details: [String!]!
|
||||
key: String!
|
||||
}
|
||||
|
||||
type CommPin {
|
||||
id: ID!
|
||||
contact: String!
|
||||
text: String!
|
||||
}
|
||||
|
||||
type WorkspaceDocument {
|
||||
id: ID!
|
||||
title: String!
|
||||
type: String!
|
||||
owner: String!
|
||||
scope: String!
|
||||
updatedAt: String!
|
||||
summary: String!
|
||||
body: String!
|
||||
}
|
||||
`);
|
||||
|
||||
export const crmGraphqlRoot = {
|
||||
me: async (_args: unknown, context: GraphQLContext) => getAuthPayload(context.auth),
|
||||
chatMessages: async (_args: unknown, context: GraphQLContext) => getChatMessages(context.auth),
|
||||
chatConversations: async (_args: unknown, context: GraphQLContext) => getChatConversations(context.auth),
|
||||
dashboard: async (_args: unknown, context: GraphQLContext) => getDashboard(context.auth),
|
||||
|
||||
login: async (args: { phone: string; password: string }, context: GraphQLContext) =>
|
||||
loginWithPassword(context.event, args.phone, args.password),
|
||||
|
||||
logout: async (_args: unknown, context: GraphQLContext) => {
|
||||
clearAuthSession(context.event);
|
||||
return { ok: true };
|
||||
},
|
||||
|
||||
createChatConversation: async (args: { title?: string }, context: GraphQLContext) =>
|
||||
createChatConversation(context.auth, context.event, args.title),
|
||||
|
||||
selectChatConversation: async (args: { id: string }, context: GraphQLContext) =>
|
||||
selectChatConversation(context.auth, context.event, args.id),
|
||||
|
||||
sendPilotMessage: async (args: { text: string }, context: GraphQLContext) =>
|
||||
sendPilotMessage(context.auth, args.text),
|
||||
|
||||
logPilotNote: async (args: { text: string }, context: GraphQLContext) =>
|
||||
logPilotNote(context.auth, 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),
|
||||
|
||||
createCommunication: async (
|
||||
args: {
|
||||
input: {
|
||||
contact: string;
|
||||
channel?: string;
|
||||
kind?: "message" | "call";
|
||||
direction?: "in" | "out";
|
||||
text?: string;
|
||||
at?: string;
|
||||
durationSec?: number;
|
||||
transcript?: string[];
|
||||
};
|
||||
},
|
||||
context: GraphQLContext,
|
||||
) => createCommunication(context.auth, args.input),
|
||||
|
||||
updateFeedDecision: async (
|
||||
args: { id: string; decision: "accepted" | "rejected" | "pending"; decisionNote?: string },
|
||||
context: GraphQLContext,
|
||||
) => updateFeedDecision(context.auth, args.id, args.decision, args.decisionNote),
|
||||
};
|
||||
Reference in New Issue
Block a user