diff --git a/frontend/app/components/workspace/CrmWorkspaceApp.vue b/frontend/app/components/workspace/CrmWorkspaceApp.vue index f6a5c18..8635202 100644 --- a/frontend/app/components/workspace/CrmWorkspaceApp.vue +++ b/frontend/app/components/workspace/CrmWorkspaceApp.vue @@ -33,29 +33,15 @@ import { useChangeReview } from "~~/app/composables/useChangeReview"; import { useCrmRealtime } from "~~/app/composables/useCrmRealtime"; 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 { - TabId, CalendarView, - PeopleLeftMode, - PeopleSortMode, - PeopleVisibilityMode, - FeedCard, - Contact, CalendarEvent, EventLifecyclePhase, - CommItem, - ContactInbox, - Deal, - DealStep, - WorkspaceDocument, - PilotMessage, - PilotChangeItem, - ContextScope, -} from "~~/app/composables/crm-types"; - +} from "~~/app/composables/useCalendar"; import { - safeTrim, dayKey, formatDay, formatTime, @@ -65,7 +51,22 @@ import { isEventFinalStatus, eventRelativeLabel, 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 diff --git a/frontend/app/composables/crm-types.ts b/frontend/app/composables/crm-types.ts deleted file mode 100644 index 2b77abc..0000000 --- a/frontend/app/composables/crm-types.ts +++ /dev/null @@ -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; -} diff --git a/frontend/app/composables/useCalendar.ts b/frontend/app/composables/useCalendar.ts index 31c05d6..6c950b9 100644 --- a/frontend/app/composables/useCalendar.ts +++ b/frontend/app/composables/useCalendar.ts @@ -5,21 +5,189 @@ import { CreateCalendarEventMutationDocument, ArchiveCalendarEventMutationDocument, } 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 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 }) { // --------------------------------------------------------------------------- // Apollo query & mutation diff --git a/frontend/app/composables/useCallAudio.ts b/frontend/app/composables/useCallAudio.ts index ef1546f..80ee194 100644 --- a/frontend/app/composables/useCallAudio.ts +++ b/frontend/app/composables/useCallAudio.ts @@ -5,7 +5,8 @@ import { } from "~~/graphql/generated"; import { useMutation } from "@vue/apollo-composable"; import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription"; -import type { CommItem } from "~/composables/crm-types"; + +import type { CommItem } from "~/composables/useContacts"; export function useCallAudio() { // --------------------------------------------------------------------------- diff --git a/frontend/app/composables/useChangeReview.ts b/frontend/app/composables/useChangeReview.ts index 9588528..3673427 100644 --- a/frontend/app/composables/useChangeReview.ts +++ b/frontend/app/composables/useChangeReview.ts @@ -15,7 +15,8 @@ import { PinsQueryDocument, DocumentsQueryDocument, } from "~~/graphql/generated"; -import type { PilotMessage, PilotChangeItem } from "~/composables/crm-types"; + +import type { PilotMessage, PilotChangeItem } from "~/composables/usePilotChat"; export function useChangeReview(opts: { pilotMessages: Ref; diff --git a/frontend/app/composables/useContactInboxes.ts b/frontend/app/composables/useContactInboxes.ts index d3782d2..3c71190 100644 --- a/frontend/app/composables/useContactInboxes.ts +++ b/frontend/app/composables/useContactInboxes.ts @@ -4,7 +4,20 @@ import { ContactInboxesQueryDocument, SetContactInboxHiddenDocument, } 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 }) { const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery( diff --git a/frontend/app/composables/useContacts.ts b/frontend/app/composables/useContacts.ts index 0222020..634c548 100644 --- a/frontend/app/composables/useContacts.ts +++ b/frontend/app/composables/useContacts.ts @@ -4,7 +4,35 @@ import { ContactsQueryDocument, CommunicationsQueryDocument, } 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 }) { const { result: contactsResult, refetch: refetchContacts } = useQuery( diff --git a/frontend/app/composables/useDeals.ts b/frontend/app/composables/useDeals.ts index fe49dd4..b46e114 100644 --- a/frontend/app/composables/useDeals.ts +++ b/frontend/app/composables/useDeals.ts @@ -1,8 +1,34 @@ import { ref, computed, watch, type ComputedRef, type Ref } from "vue"; import { useQuery } from "@vue/apollo-composable"; 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: { apolloAuthReady: ComputedRef; diff --git a/frontend/app/composables/useDocuments.ts b/frontend/app/composables/useDocuments.ts index fa74ef2..1df4d31 100644 --- a/frontend/app/composables/useDocuments.ts +++ b/frontend/app/composables/useDocuments.ts @@ -5,10 +5,24 @@ import { CreateWorkspaceDocumentDocument, DeleteWorkspaceDocumentDocument, } from "~~/graphql/generated"; -import type { WorkspaceDocument, DocumentSortMode, ClientTimelineItem } from "~/composables/crm-types"; -import { safeTrim } from "~/composables/crm-types"; 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 }) { const { result: documentsResult, refetch: refetchDocuments } = useQuery( DocumentsQueryDocument, diff --git a/frontend/app/composables/useFeed.ts b/frontend/app/composables/useFeed.ts index 0a13c90..635fbb8 100644 --- a/frontend/app/composables/useFeed.ts +++ b/frontend/app/composables/useFeed.ts @@ -12,8 +12,23 @@ import { ChatMessagesQueryDocument, ChatConversationsQueryDocument, } 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: { apolloAuthReady: ComputedRef; diff --git a/frontend/app/composables/usePilotChat.ts b/frontend/app/composables/usePilotChat.ts index 06f9d93..28a495b 100644 --- a/frontend/app/composables/usePilotChat.ts +++ b/frontend/app/composables/usePilotChat.ts @@ -12,17 +12,85 @@ import { import { Chat as AiChat } from "@ai-sdk/vue"; import { DefaultChatTransport, isTextUIPart, type UIMessage } from "ai"; import { isVoiceCaptureSupported, transcribeAudioBlob } from "~/composables/useVoiceTranscription"; -import type { - PilotMessage, - ChatConversation, - ContextScope, - PilotContextPayload, - CalendarView, - Contact, - CalendarEvent, - Deal, -} from "~/composables/crm-types"; -import { safeTrim } from "~/composables/crm-types"; + +import type { Contact } from "~/composables/useContacts"; +import type { CalendarView, CalendarEvent } from "~/composables/useCalendar"; +import type { Deal } from "~/composables/useDeals"; + +export type PilotChangeItem = { + id: string; + entity: string; + entityId?: string | null; + action: string; + 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: { apolloAuthReady: ComputedRef; diff --git a/frontend/app/composables/usePins.ts b/frontend/app/composables/usePins.ts index 531d936..1a1f13c 100644 --- a/frontend/app/composables/usePins.ts +++ b/frontend/app/composables/usePins.ts @@ -1,13 +1,16 @@ import { ref, computed, watch, type ComputedRef, type Ref } from "vue"; import { useQuery, useMutation } from "@vue/apollo-composable"; import { PinsQueryDocument, ToggleContactPinMutationDocument } from "~~/graphql/generated"; -import type { CommPin, CalendarEvent, CommItem, ClientTimelineItem } from "~/composables/crm-types"; -import { - formatDay, - isEventFinalStatus, - eventLifecyclePhase, -} from "~/composables/crm-types"; -import type { EventLifecyclePhase } from "~/composables/crm-types"; + +import type { CommItem } from "~/composables/useContacts"; +import type { CalendarEvent, EventLifecyclePhase } from "~/composables/useCalendar"; +import { formatDay, isEventFinalStatus } from "~/composables/useCalendar"; + +export type CommPin = { + id: string; + contact: string; + text: string; +}; export function usePins(opts: { apolloAuthReady: ComputedRef; diff --git a/frontend/app/composables/useTimeline.ts b/frontend/app/composables/useTimeline.ts index 4696fa9..7fa5464 100644 --- a/frontend/app/composables/useTimeline.ts +++ b/frontend/app/composables/useTimeline.ts @@ -1,7 +1,23 @@ import { ref, computed, watch, type ComputedRef } from "vue"; import { useQuery } from "@vue/apollo-composable"; 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 }) { const timelineContactId = ref(""); diff --git a/frontend/app/composables/useWorkspaceRouting.ts b/frontend/app/composables/useWorkspaceRouting.ts index b759174..b34dda3 100644 --- a/frontend/app/composables/useWorkspaceRouting.ts +++ b/frontend/app/composables/useWorkspaceRouting.ts @@ -1,6 +1,15 @@ 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: { selectedTab: Ref;