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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<boolean> }) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// Apollo query & mutation
|
||||
|
||||
@@ -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() {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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<PilotMessage[]>;
|
||||
|
||||
@@ -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<boolean> }) {
|
||||
const { result: contactInboxesResult, refetch: refetchContactInboxes } = useQuery(
|
||||
|
||||
@@ -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<boolean> }) {
|
||||
const { result: contactsResult, refetch: refetchContacts } = useQuery(
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
@@ -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<boolean> }) {
|
||||
const { result: documentsResult, refetch: refetchDocuments } = useQuery(
|
||||
DocumentsQueryDocument,
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
@@ -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<boolean>;
|
||||
|
||||
@@ -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<boolean> }) {
|
||||
const timelineContactId = ref("");
|
||||
|
||||
@@ -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<TabId>;
|
||||
|
||||
Reference in New Issue
Block a user