//#region src/types.d.ts export type ContactStatus = "lead" | "active" | "customer" | "churned"; export type DealStage = string; export type StageType = "open" | "won" | "lost"; export type PipelineKind = "standard" | "recurring"; export type ValueInterval = "one_time" | "monthly" | "annual"; /** Named once so SQL predicates and UI fallbacks cannot drift apart. */ export declare const OPEN_STAGE_TYPE: "open"; /** A closed deal is a historical fact: automation never reopens one. */ export declare function isClosedStageType(type: StageType): boolean; export interface PipelineStageConfig { color: string | null; id: string; key: DealStage; label: string; position: number; type: StageType; valueFixed: number | null; } export interface PipelineConfig { id: string; isDefault: boolean; kind: PipelineKind; name: string; slug: string; sourceMappingId: string | null; stages: PipelineStageConfig[]; } export type TaskStatus = "todo" | "in_progress" | "done" | "cancelled"; export type TaskPriority = "low" | "medium" | "high"; export type TaskEntityType = "contact" | "company" | "custom" | "deal"; export type ActivityType = "note" | "email" | "task" | "deal" | "call"; export type CrmRole = "owner" | "admin" | "member" | "developer" | "support_agent"; export interface Contact { companyId: string | null; createdAt: Date; email: string; id: string; lastActivityAt: Date | null; name: string; orgId: string; phone: string | null; /** * The host row this contact mirrors, or null when it was created in the CRM. * * Carried on the domain type because it decides whether a user may edit the * identity fields: sync rewrites them from the host view on every run, so an * edit to a sourced row would be silently reverted. See `host-owned-fields`. */ sourceRef: string | null; status: ContactStatus; title: string | null; } export interface Company { /** * How many non-archived contacts are attached to this company. * * Derived per read, never stored — distinct from {@link Company.size}, which * is the mappable headcount of the business itself. A provider with 400 staff * and 4 CRM contacts has `size: "400"` and `contactCount: 4`; conflating them * would state a falsehood in the authoritative-looking direction. */ contactCount: number; createdAt: Date; domain: string | null; email: string | null; id: string; industry: string | null; name: string; orgId: string; phone: string | null; /** * The one contact to talk to at this company, if chosen. * * Constrained to a contact of this same company, and cleared when that * contact is archived — so a non-null value always names someone live and * attached here. Unlike the other fields it is never host-owned: it is a CRM * judgement, so it stays editable on synced rows. */ primaryContactId: string | null; size: string | null; /** See {@link Contact.sourceRef}. */ sourceRef: string | null; } export interface Deal { closeDate: Date | null; contactId: string | null; createdAt: Date; id: string; name: string; orgId: string; owner: string | null; position: number | null; stage: DealStage; /** * When a human last moved this deal by hand. Non-null means stage derivation * no longer touches it, so a board must be able to mark it as overridden and * offer a way back — otherwise an overridden deal is indistinguishable from * one the rules simply never matched. */ stageOverrideAt: Date | null; value: number; /** * When a human last set this deal's value by hand. Non-null means value * derivation no longer touches it. Mirrors {@link Deal.stageOverrideAt} — and * needs the same visibility, since an honoured-but-invisible override reads * as a stuck deal rather than a pinned one. */ valueOverrideAt: Date | null; } export interface Task { archivedAt: Date | null; assignee: string | null; completedAt: Date | null; contactId: string | null; createdAt: Date; createdBy: string | null; description: string | null; dueAt: Date | null; entityId: string | null; entityType: TaskEntityType | null; id: string; orgId: string; priority: TaskPriority; status: TaskStatus; title: string; } export interface Note { archivedAt: Date | null; author: string | null; body: string; contactId: string | null; createdAt: Date; createdBy: string | null; entityId: string | null; entityType: TaskEntityType | null; id: string; isTemplate: boolean; orgId: string; title: string; updatedAt: Date; } export interface Activity { actor: string | null; contactId: string | null; createdAt: Date; id: string; orgId: string; summary: string; type: ActivityType; } export interface CreateContactInput { companyId?: string; email: string; name: string; phone?: string; status?: ContactStatus; title?: string; } export interface UpdateContactInput { companyId?: string | null; email?: string; name?: string; phone?: string | null; status?: ContactStatus; title?: string | null; } export interface CreateCompanyInput { domain?: string; email?: string; industry?: string; name: string; phone?: string; size?: string; } export interface UpdateCompanyInput { domain?: string | null; email?: string | null; industry?: string | null; name?: string; phone?: string | null; /** Null clears the point of contact; see {@link Company.primaryContactId}. */ primaryContactId?: string | null; size?: string | null; } export interface CreateDealInput { contactId?: string; name: string; owner?: string; pipelineId?: string; stage?: DealStage; value?: number; } export interface UpdateDealInput { closeDate?: Date | null; contactId?: string | null; name?: string; owner?: string | null; stage?: DealStage; value?: number; } export interface CreateTaskInput { assignee?: string; createdBy?: string; description?: string; dueAt?: Date; entityId?: string; entityType?: TaskEntityType; priority?: TaskPriority; title: string; } export interface UpdateTaskInput { assignee?: string | null; description?: string | null; dueAt?: Date | null; entityId?: string | null; entityType?: TaskEntityType | null; priority?: TaskPriority; title?: string; } export interface CreateActivityInput { actor?: string; contactId?: string; summary: string; type: ActivityType; } export type RecordSource = "all" | "synced"; export interface ContactFilters { companyId?: string; /** * Exclude archived rows. Defaults to false — the adapter returns everything * — because most callers are lookup maps, not user-facing lists: * `importContacts` builds its dedup set from this (hiding archived rows * would resurrect a contact the user archived as a duplicate) and * `deals.list` builds its contact-name map from it (an archived contact on * a live deal would render blank). * * The user-facing `contacts.list` / `companies.list` routes opt in, because * there `archiveContact` is the delete affordance and a row still visible * after archiving reads as a failed delete. * * Independent of `source`: `source: "synced"` already excludes archived * rows, since sync archives a row when it leaves the host view and such a * row is no longer sync-provenanced. */ excludeArchived?: boolean; search?: string; source?: RecordSource; status?: ContactStatus; } /** * One page of a keyset-paginated read. * * Keyset, not offset: `LIMIT/OFFSET` re-scans every skipped row, so page 200 * costs 200 pages of work, and a row inserted mid-listing shifts everything * after it — the reader sees a record twice or never. A cursor anchored to * `(createdAt, id)` is stable under concurrent writes and costs the same at * page 1 and page 200. * * `audit.list` already does this; these types generalise its shape so the CRM * lists cannot drift into a second convention. */ export interface PageRequest { /** Opaque `${createdAtISO}|${id}`. Rows strictly older than this. */ cursor?: string; limit?: number; } export interface Page { /** Null when this is the last page. */ nextCursor: string | null; rows: T[]; } export interface CompanyFilters { /** See {@link ContactFilters.excludeArchived}. */ excludeArchived?: boolean; source?: RecordSource; } export interface TaskFilters { assignee?: string; entityId?: string; entityType?: TaskEntityType; includeArchived?: boolean; includeCompleted?: boolean; priority?: TaskPriority; status?: TaskStatus; } //#endregion