refactor(graphql): replace dashboard query with resource queries
This commit is contained in:
@@ -10,7 +10,14 @@ import CrmPilotSidebar from "~~/app/components/workspace/pilot/CrmPilotSidebar.v
|
|||||||
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
import CrmChangeReviewOverlay from "~~/app/components/workspace/review/CrmChangeReviewOverlay.vue";
|
||||||
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
import meQuery from "~~/graphql/operations/me.graphql?raw";
|
||||||
import chatMessagesQuery from "~~/graphql/operations/chat-messages.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 getClientTimelineQuery from "~~/graphql/operations/get-client-timeline.graphql?raw";
|
||||||
import logoutMutation from "~~/graphql/operations/logout.graphql?raw";
|
import logoutMutation from "~~/graphql/operations/logout.graphql?raw";
|
||||||
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
import logPilotNoteMutation from "~~/graphql/operations/log-pilot-note.graphql?raw";
|
||||||
@@ -982,27 +989,34 @@ async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function refreshCrmData() {
|
async function refreshCrmData() {
|
||||||
const data = await gqlFetch<{
|
const [
|
||||||
dashboard: {
|
contactsData,
|
||||||
contacts: Contact[];
|
communicationsData,
|
||||||
communications: CommItem[];
|
contactInboxesData,
|
||||||
contactInboxes: ContactInbox[];
|
calendarData,
|
||||||
calendar: CalendarEvent[];
|
dealsData,
|
||||||
deals: Deal[];
|
feedData,
|
||||||
feed: FeedCard[];
|
pinsData,
|
||||||
pins: CommPin[];
|
documentsData,
|
||||||
documents: WorkspaceDocument[];
|
] = await Promise.all([
|
||||||
};
|
gqlFetch<{ contacts: Contact[] }>(contactsQuery),
|
||||||
}>(dashboardQuery);
|
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 ?? [];
|
contacts.value = contactsData.contacts ?? [];
|
||||||
commItems.value = data.dashboard.communications ?? [];
|
commItems.value = communicationsData.communications ?? [];
|
||||||
contactInboxes.value = data.dashboard.contactInboxes ?? [];
|
contactInboxes.value = contactInboxesData.contactInboxes ?? [];
|
||||||
calendarEvents.value = data.dashboard.calendar ?? [];
|
calendarEvents.value = calendarData.calendar ?? [];
|
||||||
deals.value = data.dashboard.deals ?? [];
|
deals.value = dealsData.deals ?? [];
|
||||||
feedCards.value = data.dashboard.feed ?? [];
|
feedCards.value = feedData.feed ?? [];
|
||||||
commPins.value = data.dashboard.pins ?? [];
|
commPins.value = pinsData.pins ?? [];
|
||||||
documents.value = data.dashboard.documents ?? [];
|
documents.value = documentsData.documents ?? [];
|
||||||
|
|
||||||
// Derive channels per contact from communication items.
|
// Derive channels per contact from communication items.
|
||||||
const byName = new Map<string, Set<string>>();
|
const byName = new Map<string, Set<string>>();
|
||||||
|
|||||||
14
frontend/graphql/operations/calendar.graphql
Normal file
14
frontend/graphql/operations/calendar.graphql
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
query CalendarQuery {
|
||||||
|
calendar {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
start
|
||||||
|
end
|
||||||
|
contact
|
||||||
|
note
|
||||||
|
isArchived
|
||||||
|
createdAt
|
||||||
|
archiveNote
|
||||||
|
archivedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
19
frontend/graphql/operations/communications.graphql
Normal file
19
frontend/graphql/operations/communications.graphql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
query CommunicationsQuery {
|
||||||
|
communications {
|
||||||
|
id
|
||||||
|
at
|
||||||
|
contactId
|
||||||
|
contact
|
||||||
|
contactInboxId
|
||||||
|
sourceExternalId
|
||||||
|
sourceTitle
|
||||||
|
channel
|
||||||
|
kind
|
||||||
|
direction
|
||||||
|
text
|
||||||
|
audioUrl
|
||||||
|
duration
|
||||||
|
transcript
|
||||||
|
deliveryStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/graphql/operations/contact-inboxes.graphql
Normal file
13
frontend/graphql/operations/contact-inboxes.graphql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
query ContactInboxesQuery {
|
||||||
|
contactInboxes {
|
||||||
|
id
|
||||||
|
contactId
|
||||||
|
contactName
|
||||||
|
channel
|
||||||
|
sourceExternalId
|
||||||
|
title
|
||||||
|
isHidden
|
||||||
|
lastMessageAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
}
|
||||||
13
frontend/graphql/operations/contacts.graphql
Normal file
13
frontend/graphql/operations/contacts.graphql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
query ContactsQuery {
|
||||||
|
contacts {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
avatar
|
||||||
|
company
|
||||||
|
country
|
||||||
|
location
|
||||||
|
channels
|
||||||
|
lastContactAt
|
||||||
|
description
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
22
frontend/graphql/operations/deals.graphql
Normal file
22
frontend/graphql/operations/deals.graphql
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
frontend/graphql/operations/documents.graphql
Normal file
12
frontend/graphql/operations/documents.graphql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
query DocumentsQuery {
|
||||||
|
documents {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
type
|
||||||
|
owner
|
||||||
|
scope
|
||||||
|
updatedAt
|
||||||
|
summary
|
||||||
|
body
|
||||||
|
}
|
||||||
|
}
|
||||||
15
frontend/graphql/operations/feed.graphql
Normal file
15
frontend/graphql/operations/feed.graphql
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
query FeedQuery {
|
||||||
|
feed {
|
||||||
|
id
|
||||||
|
at
|
||||||
|
contact
|
||||||
|
text
|
||||||
|
proposal {
|
||||||
|
title
|
||||||
|
details
|
||||||
|
key
|
||||||
|
}
|
||||||
|
decision
|
||||||
|
decisionNote
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/graphql/operations/pins.graphql
Normal file
7
frontend/graphql/operations/pins.graphql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
query PinsQuery {
|
||||||
|
pins {
|
||||||
|
id
|
||||||
|
contact
|
||||||
|
text
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -441,31 +441,21 @@ async function getChatMessages(auth: AuthContext | null) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getDashboard(auth: AuthContext | null) {
|
async function getHiddenInboxIds(teamId: string, userId: string) {
|
||||||
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 hiddenPrefRows = await prisma.contactInboxPreference.findMany({
|
const hiddenPrefRows = await prisma.contactInboxPreference.findMany({
|
||||||
where: {
|
where: { teamId, userId, isHidden: true },
|
||||||
teamId: ctx.teamId,
|
|
||||||
userId: ctx.userId,
|
|
||||||
isHidden: true,
|
|
||||||
},
|
|
||||||
select: { contactInboxId: true },
|
select: { contactInboxId: true },
|
||||||
});
|
});
|
||||||
const hiddenInboxIds = hiddenPrefRows.map((row) => row.contactInboxId);
|
return hiddenPrefRows.map((row) => row.contactInboxId);
|
||||||
const messageWhere = visibleMessageWhere(hiddenInboxIds);
|
}
|
||||||
|
|
||||||
const [
|
async function getContacts(auth: AuthContext | null) {
|
||||||
contactsRaw,
|
const ctx = requireAuth(auth);
|
||||||
communicationsRaw,
|
const hiddenInboxIds = await getHiddenInboxIds(ctx.teamId, ctx.userId);
|
||||||
contactInboxesRaw,
|
const messageWhere = visibleMessageWhere(hiddenInboxIds);
|
||||||
calendarRaw,
|
const hiddenInboxIdSet = new Set(hiddenInboxIds);
|
||||||
dealsRaw,
|
|
||||||
feedRaw,
|
const [contactsRaw, contactInboxesRaw, communicationsRaw] = await Promise.all([
|
||||||
pinsRaw,
|
|
||||||
documentsRaw,
|
|
||||||
] = await Promise.all([
|
|
||||||
prisma.contact.findMany({
|
prisma.contact.findMany({
|
||||||
where: { teamId: ctx.teamId },
|
where: { teamId: ctx.teamId },
|
||||||
include: {
|
include: {
|
||||||
@@ -475,7 +465,65 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
orderBy: { updatedAt: "desc" },
|
orderBy: { updatedAt: "desc" },
|
||||||
take: 500,
|
take: 500,
|
||||||
}),
|
}),
|
||||||
|
prisma.contactInbox.findMany({
|
||||||
|
where: { teamId: ctx.teamId },
|
||||||
|
select: { id: true, contactId: true, channel: true },
|
||||||
|
orderBy: { updatedAt: "desc" },
|
||||||
|
take: 5000,
|
||||||
|
}),
|
||||||
prisma.contactMessage.findMany({
|
prisma.contactMessage.findMany({
|
||||||
|
where: {
|
||||||
|
contact: { teamId: ctx.teamId },
|
||||||
|
...(messageWhere ?? {}),
|
||||||
|
},
|
||||||
|
select: { contactId: true, channel: true },
|
||||||
|
orderBy: { occurredAt: "asc" },
|
||||||
|
take: 2000,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
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: {
|
where: {
|
||||||
contact: { teamId: ctx.teamId },
|
contact: { teamId: ctx.teamId },
|
||||||
...(messageWhere ?? {}),
|
...(messageWhere ?? {}),
|
||||||
@@ -486,54 +534,7 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
contact: { select: { id: true, name: true } },
|
contact: { select: { id: true, name: true } },
|
||||||
contactInbox: { select: { id: true, sourceExternalId: true, title: 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,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
let omniMessagesRaw: Array<{
|
let omniMessagesRaw: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -558,10 +559,7 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
where: {
|
where: {
|
||||||
teamId: ctx.teamId,
|
teamId: ctx.teamId,
|
||||||
contactId: { in: contactIds },
|
contactId: { in: contactIds },
|
||||||
occurredAt: {
|
occurredAt: { gte: fromOccurredAt, lte: toOccurredAt },
|
||||||
gte: fromOccurredAt,
|
|
||||||
lte: toOccurredAt,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
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>();
|
const omniByKey = new Map<string, typeof omniMessagesRaw>();
|
||||||
for (const row of omniMessagesRaw) {
|
for (const row of omniMessagesRaw) {
|
||||||
const normalizedText = extractOmniNormalizedText(row.rawJson, row.text);
|
const normalizedText = extractOmniNormalizedText(row.rawJson, row.text);
|
||||||
@@ -664,7 +622,7 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
return best.status;
|
return best.status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const communications = communicationsRaw.map((m) => ({
|
return communicationsRaw.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
at: m.occurredAt.toISOString(),
|
at: m.occurredAt.toISOString(),
|
||||||
contactId: m.contactId,
|
contactId: m.contactId,
|
||||||
@@ -681,9 +639,30 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
|
transcript: Array.isArray(m.transcriptJson) ? ((m.transcriptJson as any) as string[]) : [],
|
||||||
deliveryStatus: resolveDeliveryStatus(m),
|
deliveryStatus: resolveDeliveryStatus(m),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const contactInboxes = contactInboxesRaw
|
async function getContactInboxes(auth: AuthContext | null) {
|
||||||
.map((inbox) => ({
|
const ctx = requireAuth(auth);
|
||||||
|
const hiddenInboxIds = await getHiddenInboxIds(ctx.teamId, ctx.userId);
|
||||||
|
const messageWhere = visibleMessageWhere(hiddenInboxIds);
|
||||||
|
const hiddenInboxIdSet = new Set(hiddenInboxIds);
|
||||||
|
|
||||||
|
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,
|
id: inbox.id,
|
||||||
contactId: inbox.contactId,
|
contactId: inbox.contactId,
|
||||||
contactName: inbox.contact.name,
|
contactName: inbox.contact.name,
|
||||||
@@ -694,8 +673,21 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
lastMessageAt: inbox.messages[0]?.occurredAt?.toISOString?.() ?? "",
|
lastMessageAt: inbox.messages[0]?.occurredAt?.toISOString?.() ?? "",
|
||||||
updatedAt: inbox.updatedAt.toISOString(),
|
updatedAt: inbox.updatedAt.toISOString(),
|
||||||
}));
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
const calendar = calendarRaw.map((e) => ({
|
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,
|
id: e.id,
|
||||||
title: e.title,
|
title: e.title,
|
||||||
start: e.startsAt.toISOString(),
|
start: e.startsAt.toISOString(),
|
||||||
@@ -707,8 +699,21 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
archiveNote: e.archiveNote ?? "",
|
archiveNote: e.archiveNote ?? "",
|
||||||
archivedAt: e.archivedAt?.toISOString() ?? "",
|
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,
|
id: d.id,
|
||||||
contact: d.contact.name,
|
contact: d.contact.name,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
@@ -728,8 +733,18 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
completedAt: step.completedAt?.toISOString() ?? "",
|
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,
|
id: c.id,
|
||||||
at: c.happenedAt.toISOString(),
|
at: c.happenedAt.toISOString(),
|
||||||
contact: c.contact?.name ?? "",
|
contact: c.contact?.name ?? "",
|
||||||
@@ -742,14 +757,33 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
decision: c.decision === "ACCEPTED" ? "accepted" : c.decision === "REJECTED" ? "rejected" : "pending",
|
decision: c.decision === "ACCEPTED" ? "accepted" : c.decision === "REJECTED" ? "rejected" : "pending",
|
||||||
decisionNote: c.decisionNote ?? "",
|
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,
|
id: p.id,
|
||||||
contact: p.contact.name,
|
contact: p.contact.name,
|
||||||
text: p.text,
|
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,
|
id: d.id,
|
||||||
title: d.title,
|
title: d.title,
|
||||||
type: d.type,
|
type: d.type,
|
||||||
@@ -759,17 +793,6 @@ async function getDashboard(auth: AuthContext | null) {
|
|||||||
summary: d.summary,
|
summary: d.summary,
|
||||||
body: d.body,
|
body: d.body,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return {
|
|
||||||
contacts,
|
|
||||||
communications,
|
|
||||||
contactInboxes,
|
|
||||||
calendar,
|
|
||||||
deals,
|
|
||||||
feed,
|
|
||||||
pins,
|
|
||||||
documents,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getClientTimeline(auth: AuthContext | null, contactIdInput: string, limitInput?: number) {
|
async function getClientTimeline(auth: AuthContext | null, contactIdInput: string, limitInput?: number) {
|
||||||
@@ -1779,7 +1802,14 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
me: MePayload!
|
me: MePayload!
|
||||||
chatMessages: [PilotMessage!]!
|
chatMessages: [PilotMessage!]!
|
||||||
chatConversations: [Conversation!]!
|
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!]!
|
getClientTimeline(contactId: ID!, limit: Int): [ClientTimelineItem!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1917,17 +1947,6 @@ export const crmGraphqlSchema = buildSchema(`
|
|||||||
at: String!
|
at: String!
|
||||||
}
|
}
|
||||||
|
|
||||||
type DashboardPayload {
|
|
||||||
contacts: [Contact!]!
|
|
||||||
communications: [CommItem!]!
|
|
||||||
contactInboxes: [ContactInbox!]!
|
|
||||||
calendar: [CalendarEvent!]!
|
|
||||||
deals: [Deal!]!
|
|
||||||
feed: [FeedCard!]!
|
|
||||||
pins: [CommPin!]!
|
|
||||||
documents: [WorkspaceDocument!]!
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientTimelineItem {
|
type ClientTimelineItem {
|
||||||
id: ID!
|
id: ID!
|
||||||
contactId: String!
|
contactId: String!
|
||||||
@@ -2056,7 +2075,14 @@ export const crmGraphqlRoot = {
|
|||||||
me: async (_args: unknown, context: GraphQLContext) => getAuthPayload(context.auth),
|
me: async (_args: unknown, context: GraphQLContext) => getAuthPayload(context.auth),
|
||||||
chatMessages: async (_args: unknown, context: GraphQLContext) => getChatMessages(context.auth),
|
chatMessages: async (_args: unknown, context: GraphQLContext) => getChatMessages(context.auth),
|
||||||
chatConversations: async (_args: unknown, context: GraphQLContext) => getChatConversations(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 (
|
getClientTimeline: async (
|
||||||
args: { contactId: string; limit?: number },
|
args: { contactId: string; limit?: number },
|
||||||
context: GraphQLContext,
|
context: GraphQLContext,
|
||||||
|
|||||||
Reference in New Issue
Block a user