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

@@ -10,7 +10,14 @@ import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.v
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
import meQuery from "~~/graphql/operations/me.graphql?raw";
import chatMessagesQuery from "~~/graphql/operations/chat-messages.graphql?raw";
import dashboardQuery from "~~/graphql/operations/dashboard.graphql?raw";
import contactsQuery from "~~/graphql/operations/contacts.graphql?raw";
import communicationsQuery from "~~/graphql/operations/communications.graphql?raw";
import contactInboxesQuery from "~~/graphql/operations/contact-inboxes.graphql?raw";
import calendarQuery from "~~/graphql/operations/calendar.graphql?raw";
import dealsQuery from "~~/graphql/operations/deals.graphql?raw";
import feedQuery from "~~/graphql/operations/feed.graphql?raw";
import pinsQuery from "~~/graphql/operations/pins.graphql?raw";
import documentsQuery from "~~/graphql/operations/documents.graphql?raw";
import getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.graphql?raw";
import logoutMutation from "~~/graphql/operations/logout.graphql?raw";
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
@@ -982,27 +989,34 @@ async function logout() {
}
async function refreshCrmData() {
const data = await gqlFetch<{
dashboard: {
contacts: Contact[];
communications: CommItem[];
contactInboxes: ContactInbox[];
calendar: CalendarEvent[];
deals: Deal[];
feed: FeedCard[];
pins: CommPin[];
documents: WorkspaceDocument[];
};
}>(dashboardQuery);
const [
contactsData,
communicationsData,
contactInboxesData,
calendarData,
dealsData,
feedData,
pinsData,
documentsData,
] = await Promise.all([
gqlFetch<{ contacts: Contact[] }>(contactsQuery),
gqlFetch<{ communications: CommItem[] }>(communicationsQuery),
gqlFetch<{ contactInboxes: ContactInbox[] }>(contactInboxesQuery),
gqlFetch<{ calendar: CalendarEvent[] }>(calendarQuery),
gqlFetch<{ deals: Deal[] }>(dealsQuery),
gqlFetch<{ feed: FeedCard[] }>(feedQuery),
gqlFetch<{ pins: CommPin[] }>(pinsQuery),
gqlFetch<{ documents: WorkspaceDocument[] }>(documentsQuery),
]);
contacts.value = data.dashboard.contacts ?? [];
commItems.value = data.dashboard.communications ?? [];
contactInboxes.value = data.dashboard.contactInboxes ?? [];
calendarEvents.value = data.dashboard.calendar ?? [];
deals.value = data.dashboard.deals ?? [];
feedCards.value = data.dashboard.feed ?? [];
commPins.value = data.dashboard.pins ?? [];
documents.value = data.dashboard.documents ?? [];
contacts.value = contactsData.contacts ?? [];
commItems.value = communicationsData.communications ?? [];
contactInboxes.value = contactInboxesData.contactInboxes ?? [];
calendarEvents.value = calendarData.calendar ?? [];
deals.value = dealsData.deals ?? [];
feedCards.value = feedData.feed ?? [];
commPins.value = pinsData.pins ?? [];
documents.value = documentsData.documents ?? [];
// Derive channels per contact from communication items.
const byName = new Map<string, Set<string>>();

View File

@@ -0,0 +1,14 @@
query CalendarQuery {
calendar {
id
title
start
end
contact
note
isArchived
createdAt
archiveNote
archivedAt
}
}

View File

@@ -0,0 +1,19 @@
query CommunicationsQuery {
communications {
id
at
contactId
contact
contactInboxId
sourceExternalId
sourceTitle
channel
kind
direction
text
audioUrl
duration
transcript
deliveryStatus
}
}

View File

@@ -0,0 +1,13 @@
query ContactInboxesQuery {
contactInboxes {
id
contactId
contactName
channel
sourceExternalId
title
isHidden
lastMessageAt
updatedAt
}
}

View File

@@ -0,0 +1,13 @@
query ContactsQuery {
contacts {
id
name
avatar
company
country
location
channels
lastContactAt
description
}
}

View File

@@ -1,103 +0,0 @@
query DashboardQuery {
dashboard {
contacts {
id
name
avatar
company
country
location
channels
lastContactAt
description
}
communications {
id
at
contactId
contact
contactInboxId
sourceExternalId
sourceTitle
channel
kind
direction
text
audioUrl
duration
transcript
deliveryStatus
}
contactInboxes {
id
contactId
contactName
channel
sourceExternalId
title
isHidden
lastMessageAt
updatedAt
}
calendar {
id
title
start
end
contact
note
isArchived
createdAt
archiveNote
archivedAt
}
deals {
id
contact
title
company
stage
amount
nextStep
summary
currentStepId
steps {
id
title
description
status
dueAt
order
completedAt
}
}
feed {
id
at
contact
text
proposal {
title
details
key
}
decision
decisionNote
}
pins {
id
contact
text
}
documents {
id
title
type
owner
scope
updatedAt
summary
body
}
}
}

View File

@@ -0,0 +1,22 @@
query DealsQuery {
deals {
id
contact
title
company
stage
amount
nextStep
summary
currentStepId
steps {
id
title
description
status
dueAt
order
completedAt
}
}
}

View File

@@ -0,0 +1,12 @@
query DocumentsQuery {
documents {
id
title
type
owner
scope
updatedAt
summary
body
}
}

View File

@@ -0,0 +1,15 @@
query FeedQuery {
feed {
id
at
contact
text
proposal {
title
details
key
}
decision
decisionNote
}
}

View File

@@ -0,0 +1,7 @@
query PinsQuery {
pins {
id
contact
text
}
}

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,