refactor: distribute types from crm-types.ts to owning composables

Each composable now owns its types and exports them. Other composables
import types from the owning composable. Deleted centralized crm-types.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ruslan Bakiev
2026-02-24 15:21:30 +07:00
parent a4d8d81de9
commit d892d0c604
14 changed files with 425 additions and 435 deletions

View File

@@ -33,29 +33,15 @@ import { useChangeReview } from "~~/app/composables/useChangeReview";
import { useCrmRealtime } from "~~/app/composables/useCrmRealtime"; import { useCrmRealtime } from "~~/app/composables/useCrmRealtime";
import { useWorkspaceRouting } from "~~/app/composables/useWorkspaceRouting"; import { useWorkspaceRouting } from "~~/app/composables/useWorkspaceRouting";
// Types // Types from composables
import type { Contact, CommItem } from "~~/app/composables/useContacts";
import type { ContactInbox } from "~~/app/composables/useContactInboxes";
import type { import type {
TabId,
CalendarView, CalendarView,
PeopleLeftMode,
PeopleSortMode,
PeopleVisibilityMode,
FeedCard,
Contact,
CalendarEvent, CalendarEvent,
EventLifecyclePhase, EventLifecyclePhase,
CommItem, } from "~~/app/composables/useCalendar";
ContactInbox,
Deal,
DealStep,
WorkspaceDocument,
PilotMessage,
PilotChangeItem,
ContextScope,
} from "~~/app/composables/crm-types";
import { import {
safeTrim,
dayKey, dayKey,
formatDay, formatDay,
formatTime, formatTime,
@@ -65,7 +51,22 @@ import {
isEventFinalStatus, isEventFinalStatus,
eventRelativeLabel, eventRelativeLabel,
eventPhaseToneClass, eventPhaseToneClass,
} from "~~/app/composables/crm-types"; } from "~~/app/composables/useCalendar";
import type { Deal, DealStep } from "~~/app/composables/useDeals";
import type { WorkspaceDocument } from "~~/app/composables/useDocuments";
import type { FeedCard } from "~~/app/composables/useFeed";
import type {
PilotMessage,
PilotChangeItem,
ContextScope,
} from "~~/app/composables/usePilotChat";
import type { TabId, PeopleLeftMode } from "~~/app/composables/useWorkspaceRouting";
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
type PeopleSortMode = "name" | "lastContact";
type PeopleVisibilityMode = "all" | "hidden";
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// 1. Auth // 1. Auth

View File

@@ -1,373 +0,0 @@
// ---------------------------------------------------------------------------
// Shared CRM types & utility functions
// ---------------------------------------------------------------------------
export type TabId = "communications" | "documents";
export type CalendarView = "day" | "week" | "month" | "year" | "agenda";
export type SortMode = "name" | "lastContact";
export type PeopleLeftMode = "contacts" | "calendar";
export type PeopleSortMode = "name" | "lastContact";
export type PeopleVisibilityMode = "all" | "hidden";
export type DocumentSortMode = "updatedAt" | "title" | "owner";
export type FeedCard = {
id: string;
at: string;
contact: string;
text: string;
proposal: {
title: string;
details: string[];
key: "create_followup" | "open_comm" | "call" | "draft_message" | "run_summary" | "prepare_question";
};
decision: "pending" | "accepted" | "rejected";
decisionNote?: string;
};
export type Contact = {
id: string;
name: string;
avatar: string;
channels: string[];
lastContactAt: string;
description: string;
};
export type CalendarEvent = {
id: string;
title: string;
start: string;
end: string;
contact: string;
note: string;
isArchived: boolean;
createdAt: string;
archiveNote: string;
archivedAt: string;
};
export type EventLifecyclePhase = "scheduled" | "due_soon" | "awaiting_outcome" | "closed";
export type CommItem = {
id: string;
at: string;
contact: string;
contactInboxId: string;
sourceExternalId: string;
sourceTitle: string;
channel: "Telegram" | "WhatsApp" | "Instagram" | "Phone" | "Email";
kind: "message" | "call";
direction: "in" | "out";
text: string;
audioUrl?: string;
duration?: string;
waveform?: number[];
transcript?: string[];
deliveryStatus?: "PENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | string | null;
};
export type ContactInbox = {
id: string;
contactId: string;
contactName: string;
channel: CommItem["channel"];
sourceExternalId: string;
title: string;
isHidden: boolean;
lastMessageAt: string;
updatedAt: string;
};
export type CommPin = {
id: string;
contact: string;
text: string;
};
export type Deal = {
id: string;
contact: string;
title: string;
stage: string;
amount: string;
nextStep: string;
summary: string;
currentStepId: string;
steps: DealStep[];
};
export type DealStep = {
id: string;
title: string;
description: string;
status: "todo" | "in_progress" | "done" | "blocked" | string;
dueAt: string;
order: number;
completedAt: string;
};
export type WorkspaceDocument = {
id: string;
title: string;
type: "Regulation" | "Playbook" | "Policy" | "Template";
owner: string;
scope: string;
updatedAt: string;
summary: string;
body: string;
};
export type ClientTimelineItem = {
id: string;
contactId: string;
contentType: "message" | "calendar_event" | "document" | "recommendation" | string;
contentId: string;
datetime: string;
message?: CommItem | null;
calendarEvent?: CalendarEvent | null;
recommendation?: FeedCard | null;
document?: WorkspaceDocument | null;
};
export type PilotMessage = {
id: string;
role: "user" | "assistant" | "system";
text: string;
messageKind?: string | null;
requestId?: string | null;
eventType?: string | null;
phase?: string | null;
transient?: boolean | null;
thinking?: string[] | null;
tools?: string[] | null;
toolRuns?: Array<{
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
}> | null;
changeSetId?: string | null;
changeStatus?: "pending" | "confirmed" | "rolled_back" | null;
changeSummary?: string | null;
changeItems?: PilotChangeItem[] | null;
createdAt?: string;
_live?: boolean;
};
export type PilotChangeItem = {
id: string;
entity: string;
entityId?: string | null;
action: string;
title: string;
before: string;
after: string;
rolledBack?: boolean;
};
export type ContextScope = "summary" | "deal" | "message" | "calendar";
export type PilotContextPayload = {
scopes: ContextScope[];
summary?: {
contactId: string;
name: string;
};
deal?: {
dealId: string;
title: string;
contact: string;
};
message?: {
contactId?: string;
contact?: string;
intent: "add_message_or_reminder";
};
calendar?: {
view: CalendarView;
period: string;
selectedDateKey: string;
focusedEventId?: string;
eventIds: string[];
};
};
export type ChatConversation = {
id: string;
title: string;
createdAt: string;
updatedAt: string;
lastMessageAt?: string | null;
lastMessageText?: string | null;
};
// ---------------------------------------------------------------------------
// Utility functions
// ---------------------------------------------------------------------------
export function safeTrim(value: unknown) {
return String(value ?? "").trim();
}
export function dayKey(date: Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
export function formatDay(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(new Date(iso));
}
export function formatTime(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
}).format(new Date(iso));
}
export function formatThreadTime(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
.format(new Date(iso))
.replace(":", ".");
}
export function formatStamp(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(iso));
}
export function atOffset(days: number, hour: number, minute: number) {
const d = new Date();
d.setDate(d.getDate() + days);
d.setHours(hour, minute, 0, 0);
return d.toISOString();
}
export function inMinutes(minutes: number) {
const d = new Date();
d.setMinutes(d.getMinutes() + minutes, 0, 0);
return d.toISOString();
}
export function endAfter(startIso: string, minutes: number) {
const d = new Date(startIso);
d.setMinutes(d.getMinutes() + minutes);
return d.toISOString();
}
export function isEventFinalStatus(isArchived: boolean) {
return Boolean(isArchived);
}
export function eventPreDueAt(event: CalendarEvent) {
return new Date(new Date(event.start).getTime() - 30 * 60 * 1000).toISOString();
}
export function eventDueAt(event: CalendarEvent) {
return event.start;
}
export function eventLifecyclePhase(event: CalendarEvent, nowMs: number): EventLifecyclePhase {
if (event.isArchived) return "closed";
const dueMs = new Date(eventDueAt(event)).getTime();
const preDueMs = new Date(eventPreDueAt(event)).getTime();
if (nowMs >= dueMs) return "awaiting_outcome";
if (nowMs >= preDueMs) return "due_soon";
return "scheduled";
}
export function eventTimelineAt(event: CalendarEvent, phase: EventLifecyclePhase) {
if (phase === "scheduled") return event.createdAt || event.start;
if (phase === "due_soon") return eventPreDueAt(event);
return eventDueAt(event);
}
export function eventRelativeLabel(event: CalendarEvent, nowMs: number) {
if (event.isArchived) return "Archived";
const diffMs = new Date(event.start).getTime() - nowMs;
const minuteMs = 60 * 1000;
const hourMs = 60 * minuteMs;
const dayMs = 24 * hourMs;
const abs = Math.abs(diffMs);
if (diffMs >= 0) {
if (abs >= dayMs) {
const days = Math.round(abs / dayMs);
return `Event in ${days} day${days === 1 ? "" : "s"}`;
}
if (abs >= hourMs) {
const hours = Math.round(abs / hourMs);
return `Event in ${hours} hour${hours === 1 ? "" : "s"}`;
}
const minutes = Math.max(1, Math.round(abs / minuteMs));
return `Event in ${minutes} minute${minutes === 1 ? "" : "s"}`;
}
if (abs >= dayMs) {
const days = Math.round(abs / dayMs);
return `Overdue by ${days} day${days === 1 ? "" : "s"}`;
}
if (abs >= hourMs) {
const hours = Math.round(abs / hourMs);
return `Overdue by ${hours} hour${hours === 1 ? "" : "s"}`;
}
const minutes = Math.max(1, Math.round(abs / minuteMs));
return `Overdue by ${minutes} minute${minutes === 1 ? "" : "s"}`;
}
export function eventPhaseToneClass(phase: EventLifecyclePhase) {
if (phase === "awaiting_outcome") return "border-warning/50 bg-warning/10";
if (phase === "due_soon") return "border-info/50 bg-info/10";
if (phase === "closed") return "border-success/40 bg-success/10";
return "border-base-300 bg-base-100";
}
export function toInputDate(date: Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
export function toInputTime(date: Date) {
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
}
export function roundToNextQuarter(date = new Date()) {
const d = new Date(date);
d.setSeconds(0, 0);
const minutes = d.getMinutes();
const rounded = Math.ceil(minutes / 15) * 15;
if (rounded >= 60) {
d.setHours(d.getHours() + 1, 0, 0, 0);
} else {
d.setMinutes(rounded, 0, 0);
}
return d;
}
export function roundToPrevQuarter(date = new Date()) {
const d = new Date(date);
d.setSeconds(0, 0);
const minutes = d.getMinutes();
const rounded = Math.floor(minutes / 15) * 15;
d.setMinutes(rounded, 0, 0);
return d;
}

View File

@@ -5,21 +5,189 @@ import {
CreateCalendarEventMutationDocument, CreateCalendarEventMutationDocument,
ArchiveCalendarEventMutationDocument, ArchiveCalendarEventMutationDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { CalendarEvent, CalendarView } from "~/composables/crm-types";
import {
dayKey,
formatDay,
formatTime,
toInputDate,
toInputTime,
roundToNextQuarter,
roundToPrevQuarter,
isEventFinalStatus,
} from "~/composables/crm-types";
type CalendarHierarchyView = "year" | "month" | "week" | "day"; type CalendarHierarchyView = "year" | "month" | "week" | "day";
type CalendarRect = { left: number; top: number; width: number; height: number }; type CalendarRect = { left: number; top: number; width: number; height: number };
export type CalendarView = "day" | "week" | "month" | "year" | "agenda";
export type CalendarEvent = {
id: string;
title: string;
start: string;
end: string;
contact: string;
note: string;
isArchived: boolean;
createdAt: string;
archiveNote: string;
archivedAt: string;
};
export type EventLifecyclePhase = "scheduled" | "due_soon" | "awaiting_outcome" | "closed";
export function dayKey(date: Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
export function formatDay(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
year: "numeric",
}).format(new Date(iso));
}
export function formatTime(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
}).format(new Date(iso));
}
export function formatThreadTime(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
hour12: false,
})
.format(new Date(iso))
.replace(":", ".");
}
export function formatStamp(iso: string) {
return new Intl.DateTimeFormat("en-GB", {
day: "2-digit",
month: "short",
hour: "2-digit",
minute: "2-digit",
}).format(new Date(iso));
}
export function toInputDate(date: Date) {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
}
export function toInputTime(date: Date) {
const hh = String(date.getHours()).padStart(2, "0");
const mm = String(date.getMinutes()).padStart(2, "0");
return `${hh}:${mm}`;
}
export function roundToNextQuarter(date = new Date()) {
const d = new Date(date);
d.setSeconds(0, 0);
const minutes = d.getMinutes();
const rounded = Math.ceil(minutes / 15) * 15;
if (rounded >= 60) {
d.setHours(d.getHours() + 1, 0, 0, 0);
} else {
d.setMinutes(rounded, 0, 0);
}
return d;
}
export function roundToPrevQuarter(date = new Date()) {
const d = new Date(date);
d.setSeconds(0, 0);
const minutes = d.getMinutes();
const rounded = Math.floor(minutes / 15) * 15;
d.setMinutes(rounded, 0, 0);
return d;
}
export function atOffset(days: number, hour: number, minute: number) {
const d = new Date();
d.setDate(d.getDate() + days);
d.setHours(hour, minute, 0, 0);
return d.toISOString();
}
export function inMinutes(minutes: number) {
const d = new Date();
d.setMinutes(d.getMinutes() + minutes, 0, 0);
return d.toISOString();
}
export function endAfter(startIso: string, minutes: number) {
const d = new Date(startIso);
d.setMinutes(d.getMinutes() + minutes);
return d.toISOString();
}
export function isEventFinalStatus(isArchived: boolean) {
return Boolean(isArchived);
}
export function eventPreDueAt(event: CalendarEvent) {
return new Date(new Date(event.start).getTime() - 30 * 60 * 1000).toISOString();
}
export function eventDueAt(event: CalendarEvent) {
return event.start;
}
export function eventLifecyclePhase(event: CalendarEvent, nowMs: number): EventLifecyclePhase {
if (event.isArchived) return "closed";
const dueMs = new Date(eventDueAt(event)).getTime();
const preDueMs = new Date(eventPreDueAt(event)).getTime();
if (nowMs >= dueMs) return "awaiting_outcome";
if (nowMs >= preDueMs) return "due_soon";
return "scheduled";
}
export function eventTimelineAt(event: CalendarEvent, phase: EventLifecyclePhase) {
if (phase === "scheduled") return event.createdAt || event.start;
if (phase === "due_soon") return eventPreDueAt(event);
return eventDueAt(event);
}
export function eventRelativeLabel(event: CalendarEvent, nowMs: number) {
if (event.isArchived) return "Archived";
const diffMs = new Date(event.start).getTime() - nowMs;
const minuteMs = 60 * 1000;
const hourMs = 60 * minuteMs;
const dayMs = 24 * hourMs;
const abs = Math.abs(diffMs);
if (diffMs >= 0) {
if (abs >= dayMs) {
const days = Math.round(abs / dayMs);
return `Event in ${days} day${days === 1 ? "" : "s"}`;
}
if (abs >= hourMs) {
const hours = Math.round(abs / hourMs);
return `Event in ${hours} hour${hours === 1 ? "" : "s"}`;
}
const minutes = Math.max(1, Math.round(abs / minuteMs));
return `Event in ${minutes} minute${minutes === 1 ? "" : "s"}`;
}
if (abs >= dayMs) {
const days = Math.round(abs / dayMs);
return `Overdue by ${days} day${days === 1 ? "" : "s"}`;
}
if (abs >= hourMs) {
const hours = Math.round(abs / hourMs);
return `Overdue by ${hours} hour${hours === 1 ? "" : "s"}`;
}
const minutes = Math.max(1, Math.round(abs / minuteMs));
return `Overdue by ${minutes} minute${minutes === 1 ? "" : "s"}`;
}
export function eventPhaseToneClass(phase: EventLifecyclePhase) {
if (phase === "awaiting_outcome") return "border-warning/50 bg-warning/10";
if (phase === "due_soon") return "border-info/50 bg-info/10";
if (phase === "closed") return "border-success/40 bg-success/10";
return "border-base-300 bg-base-100";
}
export function useCalendar(opts: { apolloAuthReady: ComputedRef<boolean> }) { export function useCalendar(opts: { apolloAuthReady: ComputedRef<boolean> }) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Apollo query & mutation // Apollo query & mutation

View File

@@ -5,7 +5,8 @@ import {
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import { useMutation } from "@vue/apollo-composable"; import { useMutation } from "@vue/apollo-composable";
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription"; import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
import type { CommItem } from "~/composables/crm-types";
import type { CommItem } from "~/composables/useContacts";
export function useCallAudio() { export function useCallAudio() {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View File

@@ -15,7 +15,8 @@ import {
PinsQueryDocument, PinsQueryDocument,
DocumentsQueryDocument, DocumentsQueryDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { PilotMessage, PilotChangeItem } from "~/composables/crm-types";
import type { PilotMessage, PilotChangeItem } from "~/composables/usePilotChat";
export function useChangeReview(opts: { export function useChangeReview(opts: {
pilotMessages: Ref<PilotMessage[]>; pilotMessages: Ref<PilotMessage[]>;

View File

@@ -4,7 +4,20 @@ import {
ContactInboxesQueryDocument, ContactInboxesQueryDocument,
SetContactInboxHiddenDocument, SetContactInboxHiddenDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { ContactInbox } from "~/composables/crm-types";
import type { CommItem } from "~/composables/useContacts";
export type ContactInbox = {
id: string;
contactId: string;
contactName: string;
channel: CommItem["channel"];
sourceExternalId: string;
title: string;
isHidden: boolean;
lastMessageAt: string;
updatedAt: string;
};
export function useContactInboxes(opts: { apolloAuthReady: ComputedRef<boolean> }) { export function useContactInboxes(opts: { apolloAuthReady: ComputedRef<boolean> }) {
const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery( const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery(

View File

@@ -4,7 +4,35 @@ import {
ContactsQueryDocument, ContactsQueryDocument,
CommunicationsQueryDocument, CommunicationsQueryDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { Contact, CommItem, SortMode } from "~/composables/crm-types";
export type Contact = {
id: string;
name: string;
avatar: string;
channels: string[];
lastContactAt: string;
description: string;
};
export type CommItem = {
id: string;
at: string;
contact: string;
contactInboxId: string;
sourceExternalId: string;
sourceTitle: string;
channel: "Telegram" | "WhatsApp" | "Instagram" | "Phone" | "Email";
kind: "message" | "call";
direction: "in" | "out";
text: string;
audioUrl?: string;
duration?: string;
waveform?: number[];
transcript?: string[];
deliveryStatus?: "PENDING" | "SENT" | "DELIVERED" | "READ" | "FAILED" | string | null;
};
export type SortMode = "name" | "lastContact";
export function useContacts(opts: { apolloAuthReady: ComputedRef<boolean> }) { export function useContacts(opts: { apolloAuthReady: ComputedRef<boolean> }) {
const { result: contactsResult, refetch: refetchContacts } = useQuery( const { result: contactsResult, refetch: refetchContacts } = useQuery(

View File

@@ -1,8 +1,34 @@
import { ref, computed, watch, type ComputedRef, type Ref } from "vue"; import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
import { useQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import { DealsQueryDocument } from "~~/graphql/generated"; import { DealsQueryDocument } from "~~/graphql/generated";
import type { Deal, DealStep, CalendarEvent, Contact } from "~/composables/crm-types";
import { safeTrim, formatDay } from "~/composables/crm-types"; import type { Contact } from "~/composables/useContacts";
import type { CalendarEvent } from "~/composables/useCalendar";
import { formatDay } from "~/composables/useCalendar";
export type DealStep = {
id: string;
title: string;
description: string;
status: "todo" | "in_progress" | "done" | "blocked" | string;
dueAt: string;
order: number;
completedAt: string;
};
export type Deal = {
id: string;
contact: string;
title: string;
stage: string;
amount: string;
nextStep: string;
summary: string;
currentStepId: string;
steps: DealStep[];
};
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
export function useDeals(opts: { export function useDeals(opts: {
apolloAuthReady: ComputedRef<boolean>; apolloAuthReady: ComputedRef<boolean>;

View File

@@ -5,10 +5,24 @@ import {
CreateWorkspaceDocumentDocument, CreateWorkspaceDocumentDocument,
DeleteWorkspaceDocumentDocument, DeleteWorkspaceDocumentDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { WorkspaceDocument, DocumentSortMode, ClientTimelineItem } from "~/composables/crm-types";
import { safeTrim } from "~/composables/crm-types";
import { formatDocumentScope } from "~/composables/useWorkspaceDocuments"; import { formatDocumentScope } from "~/composables/useWorkspaceDocuments";
export type DocumentSortMode = "updatedAt" | "title" | "owner";
export type WorkspaceDocument = {
id: string;
title: string;
type: "Regulation" | "Playbook" | "Policy" | "Template";
owner: string;
scope: string;
updatedAt: string;
summary: string;
body: string;
};
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
export function useDocuments(opts: { apolloAuthReady: ComputedRef<boolean> }) { export function useDocuments(opts: { apolloAuthReady: ComputedRef<boolean> }) {
const { result: documentsResult, refetch: refetchDocuments } = useQuery( const { result: documentsResult, refetch: refetchDocuments } = useQuery(
DocumentsQueryDocument, DocumentsQueryDocument,

View File

@@ -12,8 +12,23 @@ import {
ChatMessagesQueryDocument, ChatMessagesQueryDocument,
ChatConversationsQueryDocument, ChatConversationsQueryDocument,
} from "~~/graphql/generated"; } from "~~/graphql/generated";
import type { FeedCard, CalendarEvent } from "~/composables/crm-types";
import { dayKey, formatDay, formatTime } from "~/composables/crm-types"; import type { CalendarEvent } from "~/composables/useCalendar";
import { dayKey, formatDay, formatTime } from "~/composables/useCalendar";
export type FeedCard = {
id: string;
at: string;
contact: string;
text: string;
proposal: {
title: string;
details: string[];
key: "create_followup" | "open_comm" | "call" | "draft_message" | "run_summary" | "prepare_question";
};
decision: "pending" | "accepted" | "rejected";
decisionNote?: string;
};
export function useFeed(opts: { export function useFeed(opts: {
apolloAuthReady: ComputedRef<boolean>; apolloAuthReady: ComputedRef<boolean>;

View File

@@ -12,17 +12,85 @@ import {
import { Chat as AiChat } from "@ai-sdk/vue"; import { Chat as AiChat } from "@ai-sdk/vue";
import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai"; import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai";
import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription"; import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription";
import type {
PilotMessage, import type { Contact } from "~/composables/useContacts";
ChatConversation, import type { CalendarView, CalendarEvent } from "~/composables/useCalendar";
ContextScope, import type { Deal } from "~/composables/useDeals";
PilotContextPayload,
CalendarView, export type PilotChangeItem = {
Contact, id: string;
CalendarEvent, entity: string;
Deal, entityId?: string | null;
} from "~/composables/crm-types"; action: string;
import { safeTrim } from "~/composables/crm-types"; title: string;
before: string;
after: string;
rolledBack?: boolean;
};
export type PilotMessage = {
id: string;
role: "user" | "assistant" | "system";
text: string;
messageKind?: string | null;
requestId?: string | null;
eventType?: string | null;
phase?: string | null;
transient?: boolean | null;
thinking?: string[] | null;
tools?: string[] | null;
toolRuns?: Array<{
name: string;
status: "ok" | "error";
input: string;
output: string;
at: string;
}> | null;
changeSetId?: string | null;
changeStatus?: "pending" | "confirmed" | "rolled_back" | null;
changeSummary?: string | null;
changeItems?: PilotChangeItem[] | null;
createdAt?: string;
_live?: boolean;
};
export type ContextScope = "summary" | "deal" | "message" | "calendar";
export type PilotContextPayload = {
scopes: ContextScope[];
summary?: {
contactId: string;
name: string;
};
deal?: {
dealId: string;
title: string;
contact: string;
};
message?: {
contactId?: string;
contact?: string;
intent: "add_message_or_reminder";
};
calendar?: {
view: CalendarView;
period: string;
selectedDateKey: string;
focusedEventId?: string;
eventIds: string[];
};
};
export type ChatConversation = {
id: string;
title: string;
createdAt: string;
updatedAt: string;
lastMessageAt?: string | null;
lastMessageText?: string | null;
};
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
export function usePilotChat(opts: { export function usePilotChat(opts: {
apolloAuthReady: ComputedRef<boolean>; apolloAuthReady: ComputedRef<boolean>;

View File

@@ -1,13 +1,16 @@
import { ref, computed, watch, type ComputedRef, type Ref } from "vue"; import { ref, computed, watch, type ComputedRef, type Ref } from "vue";
import { useQuery, useMutation } from "@vue/apollo-composable"; import { useQuery, useMutation } from "@vue/apollo-composable";
import { PinsQueryDocument, ToggleContactPinMutationDocument } from "~~/graphql/generated"; import { PinsQueryDocument, ToggleContactPinMutationDocument } from "~~/graphql/generated";
import type { CommPin, CalendarEvent, CommItem, ClientTimelineItem } from "~/composables/crm-types";
import { import type { CommItem } from "~/composables/useContacts";
formatDay, import type { CalendarEvent, EventLifecyclePhase } from "~/composables/useCalendar";
isEventFinalStatus, import { formatDay, isEventFinalStatus } from "~/composables/useCalendar";
eventLifecyclePhase,
} from "~/composables/crm-types"; export type CommPin = {
import type { EventLifecyclePhase } from "~/composables/crm-types"; id: string;
contact: string;
text: string;
};
export function usePins(opts: { export function usePins(opts: {
apolloAuthReady: ComputedRef<boolean>; apolloAuthReady: ComputedRef<boolean>;

View File

@@ -1,7 +1,23 @@
import { ref, computed, watch, type ComputedRef } from "vue"; import { ref, computed, watch, type ComputedRef } from "vue";
import { useQuery } from "@vue/apollo-composable"; import { useQuery } from "@vue/apollo-composable";
import { GetClientTimelineQueryDocument } from "~~/graphql/generated"; import { GetClientTimelineQueryDocument } from "~~/graphql/generated";
import type { ClientTimelineItem } from "~/composables/crm-types";
import type { CommItem } from "~/composables/useContacts";
import type { CalendarEvent } from "~/composables/useCalendar";
import type { FeedCard } from "~/composables/useFeed";
import type { WorkspaceDocument } from "~/composables/useDocuments";
export type ClientTimelineItem = {
id: string;
contactId: string;
contentType: "message" | "calendar_event" | "document" | "recommendation" | string;
contentId: string;
datetime: string;
message?: CommItem | null;
calendarEvent?: CalendarEvent | null;
recommendation?: FeedCard | null;
document?: WorkspaceDocument | null;
};
export function useTimeline(opts: { apolloAuthReady: ComputedRef<boolean> }) { export function useTimeline(opts: { apolloAuthReady: ComputedRef<boolean> }) {
const timelineContactId = ref(""); const timelineContactId = ref("");

View File

@@ -1,6 +1,15 @@
import { ref, type Ref, type ComputedRef } from "vue"; import { ref, type Ref, type ComputedRef } from "vue";
import type { TabId, CalendarView, PeopleLeftMode, CalendarEvent, PilotChangeItem } from "~/composables/crm-types";
import { safeTrim, dayKey } from "~/composables/crm-types"; import type { CommItem } from "~/composables/useContacts";
import type { CalendarView, CalendarEvent } from "~/composables/useCalendar";
import { dayKey } from "~/composables/useCalendar";
import type { PilotChangeItem } from "~/composables/usePilotChat";
export type TabId = "communications" | "documents";
export type PeopleLeftMode = "contacts" | "calendar";
function safeTrim(value: unknown) { return String(value ?? "").trim(); }
export function useWorkspaceRouting(opts: { export function useWorkspaceRouting(opts: {
selectedTab: Ref<TabId>; selectedTab: Ref<TabId>;