refactor(graphql): replace dashboard query with resource queries
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user