refactor(graphql): replace dashboard query with resource queries

This commit is contained in:
Ruslan Bakiev
2026-02-23 12:46:29 +07:00
parent aa465f65bd
commit d3b751db65
11 changed files with 333 additions and 281 deletions

View File

@@ -441,31 +441,21 @@ async function getChatMessages(auth: AuthContext | null) {
});
}
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);
async function getHiddenInboxIds(teamId: string, userId: string) {
const hiddenPrefRows = await prisma.contactInboxPreference.findMany({
where: {
teamId: ctx.teamId,
userId: ctx.userId,
isHidden: true,
},
where: { teamId, userId, isHidden: true },
select: { contactInboxId: true },
});
const hiddenInboxIds = hiddenPrefRows.map((row) => row.contactInboxId);
const messageWhere = visibleMessageWhere(hiddenInboxIds);
return hiddenPrefRows.map((row) => row.contactInboxId);
}
const [
contactsRaw,
communicationsRaw,
contactInboxesRaw,
calendarRaw,
dealsRaw,
feedRaw,
pinsRaw,
documentsRaw,
] = await Promise.all([
async function getContacts(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const hiddenInboxIds = await getHiddenInboxIds(ctx.teamId, ctx.userId);
const messageWhere = visibleMessageWhere(hiddenInboxIds);
const hiddenInboxIdSet = new Set(hiddenInboxIds);
const [contactsRaw, contactInboxesRaw, communicationsRaw] = await Promise.all([
prisma.contact.findMany({
where: { teamId: ctx.teamId },
include: {
@@ -475,66 +465,77 @@ async function getDashboard(auth: AuthContext | null) {
orderBy: { updatedAt: "desc" },
take: 500,
}),
prisma.contactInbox.findMany({
where: { teamId: ctx.teamId },
select: { id: true, contactId: true, channel: true },
orderBy: { updatedAt: "desc" },
take: 5000,
}),
prisma.contactMessage.findMany({
where: {
contact: { teamId: ctx.teamId },
...(messageWhere ?? {}),
},
select: { contactId: true, channel: true },
orderBy: { occurredAt: "asc" },
take: 2000,
include: {
contact: { select: { id: true, name: true } },
contactInbox: { select: { id: true, sourceExternalId: true, title: true } },
},
}),
prisma.contactInbox.findMany({
where: { teamId: ctx.teamId },
orderBy: { updatedAt: "desc" },
include: {
contact: { select: { name: true } },
messages: {
where: messageWhere,
select: { occurredAt: true },
orderBy: { occurredAt: "desc" },
take: 1,
},
},
take: 5000,
}),
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 } },
steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
},
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>>();
const totalInboxesByContactId = new Map<string, number>();
const visibleInboxesByContactId = new Map<string, number>();
for (const inbox of contactInboxesRaw) {
totalInboxesByContactId.set(inbox.contactId, (totalInboxesByContactId.get(inbox.contactId) ?? 0) + 1);
if (hiddenInboxIdSet.has(inbox.id)) continue;
visibleInboxesByContactId.set(inbox.contactId, (visibleInboxesByContactId.get(inbox.contactId) ?? 0) + 1);
if (!channelsByContactId.has(inbox.contactId)) channelsByContactId.set(inbox.contactId, new Set());
channelsByContactId.get(inbox.contactId)?.add(mapChannel(inbox.channel));
}
for (const item of communicationsRaw) {
if (!channelsByContactId.has(item.contactId)) channelsByContactId.set(item.contactId, new Set());
channelsByContactId.get(item.contactId)?.add(mapChannel(item.channel));
}
return contactsRaw
.filter((c) => {
const total = totalInboxesByContactId.get(c.id) ?? 0;
if (total === 0) return true;
return (visibleInboxesByContactId.get(c.id) ?? 0) > 0;
})
.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 ?? "",
}));
}
async function getCommunications(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const hiddenInboxIds = await getHiddenInboxIds(ctx.teamId, ctx.userId);
const messageWhere = visibleMessageWhere(hiddenInboxIds);
const communicationsRaw = await prisma.contactMessage.findMany({
where: {
contact: { teamId: ctx.teamId },
...(messageWhere ?? {}),
},
orderBy: { occurredAt: "asc" },
take: 2000,
include: {
contact: { select: { id: true, name: true } },
contactInbox: { select: { id: true, sourceExternalId: true, title: true } },
},
});
let omniMessagesRaw: Array<{
id: string;
contactId: string;
@@ -558,10 +559,7 @@ async function getDashboard(auth: AuthContext | null) {
where: {
teamId: ctx.teamId,
contactId: { in: contactIds },
occurredAt: {
gte: fromOccurredAt,
lte: toOccurredAt,
},
occurredAt: { gte: fromOccurredAt, lte: toOccurredAt },
},
select: {
id: true,
@@ -579,46 +577,6 @@ async function getDashboard(auth: AuthContext | null) {
});
}
const hiddenInboxIdSet = new Set(hiddenInboxIds);
const channelsByContactId = new Map<string, Set<string>>();
const totalInboxesByContactId = new Map<string, number>();
const visibleInboxesByContactId = new Map<string, number>();
for (const inbox of contactInboxesRaw) {
totalInboxesByContactId.set(inbox.contactId, (totalInboxesByContactId.get(inbox.contactId) ?? 0) + 1);
if (hiddenInboxIdSet.has(inbox.id)) continue;
visibleInboxesByContactId.set(inbox.contactId, (visibleInboxesByContactId.get(inbox.contactId) ?? 0) + 1);
if (!channelsByContactId.has(inbox.contactId)) {
channelsByContactId.set(inbox.contactId, new Set());
}
channelsByContactId.get(inbox.contactId)?.add(mapChannel(inbox.channel));
}
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
.filter((c) => {
const total = totalInboxesByContactId.get(c.id) ?? 0;
if (total === 0) return true;
return (visibleInboxesByContactId.get(c.id) ?? 0) > 0;
})
.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 omniByKey = new Map<string, typeof omniMessagesRaw>();
for (const row of omniMessagesRaw) {
const normalizedText = extractOmniNormalizedText(row.rawJson, row.text);
@@ -664,7 +622,7 @@ async function getDashboard(auth: AuthContext | null) {
return best.status;
};
const communications = communicationsRaw.map((m) => ({
return communicationsRaw.map((m) => ({
id: m.id,
at: m.occurredAt.toISOString(),
contactId: m.contactId,
@@ -681,21 +639,55 @@ async function getDashboard(auth: AuthContext | null) {
transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
deliveryStatus: resolveDeliveryStatus(m),
}));
}
const contactInboxes = contactInboxesRaw
.map((inbox) => ({
id: inbox.id,
contactId: inbox.contactId,
contactName: inbox.contact.name,
channel: mapChannel(inbox.channel),
sourceExternalId: inbox.sourceExternalId,
title: inbox.title ?? "",
isHidden: hiddenInboxIdSet.has(inbox.id),
lastMessageAt: inbox.messages[0]?.occurredAt?.toISOString?.() ?? "",
updatedAt: inbox.updatedAt.toISOString(),
}));
async function getContactInboxes(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const hiddenInboxIds = await getHiddenInboxIds(ctx.teamId, ctx.userId);
const messageWhere = visibleMessageWhere(hiddenInboxIds);
const hiddenInboxIdSet = new Set(hiddenInboxIds);
const calendar = calendarRaw.map((e) => ({
const contactInboxesRaw = await prisma.contactInbox.findMany({
where: { teamId: ctx.teamId },
orderBy: { updatedAt: "desc" },
include: {
contact: { select: { name: true } },
messages: {
where: messageWhere,
select: { occurredAt: true },
orderBy: { occurredAt: "desc" },
take: 1,
},
},
take: 5000,
});
return contactInboxesRaw.map((inbox) => ({
id: inbox.id,
contactId: inbox.contactId,
contactName: inbox.contact.name,
channel: mapChannel(inbox.channel),
sourceExternalId: inbox.sourceExternalId,
title: inbox.title ?? "",
isHidden: hiddenInboxIdSet.has(inbox.id),
lastMessageAt: inbox.messages[0]?.occurredAt?.toISOString?.() ?? "",
updatedAt: inbox.updatedAt.toISOString(),
}));
}
async function getCalendar(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 calendarRaw = await prisma.calendarEvent.findMany({
where: { teamId: ctx.teamId, startsAt: { gte: from, lte: to } },
include: { contact: { select: { name: true } } },
orderBy: { startsAt: "asc" },
take: 500,
});
return calendarRaw.map((e) => ({
id: e.id,
title: e.title,
start: e.startsAt.toISOString(),
@@ -707,8 +699,21 @@ async function getDashboard(auth: AuthContext | null) {
archiveNote: e.archiveNote ?? "",
archivedAt: e.archivedAt?.toISOString() ?? "",
}));
}
const deals = dealsRaw.map((d) => ({
async function getDeals(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const dealsRaw = await prisma.deal.findMany({
where: { teamId: ctx.teamId },
include: {
contact: { select: { name: true, company: true } },
steps: { orderBy: [{ order: "asc" }, { createdAt: "asc" }] },
},
orderBy: { updatedAt: "desc" },
take: 500,
});
return dealsRaw.map((d) => ({
id: d.id,
contact: d.contact.name,
title: d.title,
@@ -728,8 +733,18 @@ async function getDashboard(auth: AuthContext | null) {
completedAt: step.completedAt?.toISOString() ?? "",
})),
}));
}
const feed = feedRaw.map((c) => ({
async function getFeed(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const feedRaw = await prisma.feedCard.findMany({
where: { teamId: ctx.teamId },
include: { contact: { select: { name: true } } },
orderBy: { happenedAt: "desc" },
take: 200,
});
return feedRaw.map((c) => ({
id: c.id,
at: c.happenedAt.toISOString(),
contact: c.contact?.name ?? "",
@@ -742,14 +757,33 @@ async function getDashboard(auth: AuthContext | null) {
decision: c.decision === "ACCEPTED" ? "accepted" : c.decision === "REJECTED" ? "rejected" : "pending",
decisionNote: c.decisionNote ?? "",
}));
}
const pins = pinsRaw.map((p) => ({
async function getPins(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const pinsRaw = await prisma.contactPin.findMany({
where: { teamId: ctx.teamId },
include: { contact: { select: { name: true } } },
orderBy: { updatedAt: "desc" },
take: 500,
});
return pinsRaw.map((p) => ({
id: p.id,
contact: p.contact.name,
text: p.text,
}));
}
const documents = documentsRaw.map((d) => ({
async function getDocuments(auth: AuthContext | null) {
const ctx = requireAuth(auth);
const documentsRaw = await prisma.workspaceDocument.findMany({
where: { teamId: ctx.teamId },
orderBy: { updatedAt: "desc" },
take: 200,
});
return documentsRaw.map((d) => ({
id: d.id,
title: d.title,
type: d.type,
@@ -759,17 +793,6 @@ async function getDashboard(auth: AuthContext | null) {
summary: d.summary,
body: d.body,
}));
return {
contacts,
communications,
contactInboxes,
calendar,
deals,
feed,
pins,
documents,
};
}
async function getClientTimeline(auth: AuthContext | null, contactIdInput: string, limitInput?: number) {
@@ -1779,7 +1802,14 @@ export const crmGraphqlSchema = buildSchema(`
me: MePayload!
chatMessages: [PilotMessage!]!
chatConversations: [Conversation!]!
dashboard: DashboardPayload!
contacts: [Contact!]!
communications: [CommItem!]!
contactInboxes: [ContactInbox!]!
calendar: [CalendarEvent!]!
deals: [Deal!]!
feed: [FeedCard!]!
pins: [CommPin!]!
documents: [WorkspaceDocument!]!
getClientTimeline(contactId: ID!, limit: Int): [ClientTimelineItem!]!
}
@@ -1917,17 +1947,6 @@ export const crmGraphqlSchema = buildSchema(`
at: String!
}
type DashboardPayload {
contacts: [Contact!]!
communications: [CommItem!]!
contactInboxes: [ContactInbox!]!
calendar: [CalendarEvent!]!
deals: [Deal!]!
feed: [FeedCard!]!
pins: [CommPin!]!
documents: [WorkspaceDocument!]!
}
type ClientTimelineItem {
id: ID!
contactId: String!
@@ -2056,7 +2075,14 @@ 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),
contacts: async (_args: unknown, context: GraphQLContext) => getContacts(context.auth),
communications: async (_args: unknown, context: GraphQLContext) => getCommunications(context.auth),
contactInboxes: async (_args: unknown, context: GraphQLContext) => getContactInboxes(context.auth),
calendar: async (_args: unknown, context: GraphQLContext) => getCalendar(context.auth),
deals: async (_args: unknown, context: GraphQLContext) => getDeals(context.auth),
feed: async (_args: unknown, context: GraphQLContext) => getFeed(context.auth),
pins: async (_args: unknown, context: GraphQLContext) => getPins(context.auth),
documents: async (_args: unknown, context: GraphQLContext) => getDocuments(context.auth),
getClientTimeline: async (
args: { contactId: string; limit?: number },
context: GraphQLContext,