mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
567 lines
14 KiB
TypeScript
567 lines
14 KiB
TypeScript
import type { AccountRole } from "@/lib/auth/roles";
|
|
|
|
export interface Profile {
|
|
id: string;
|
|
user_id: string;
|
|
full_name: string;
|
|
email: string;
|
|
avatar_url?: string;
|
|
/**
|
|
* Legacy free-form role column from migration 001. Never read
|
|
* by the app since 017_account_sharing.sql introduced the typed
|
|
* `account_role` enum. Flagged for removal in a later cleanup
|
|
* migration — kept on the type so existing destructures don't
|
|
* break.
|
|
*/
|
|
role: string;
|
|
/**
|
|
* Opted-in beta feature keys for this account. The column survives
|
|
* for future beta gates; no current feature reads it (Flows was
|
|
* the last user and went to soft-GA in PR #134). Defaults to `[]`
|
|
* for every profile; toggled per-account via a direct UPDATE on
|
|
* the `profiles` row.
|
|
*/
|
|
beta_features?: string[];
|
|
/**
|
|
* Account this profile is a member of. Added by
|
|
* `017_account_sharing.sql`; NOT NULL in the DB post-backfill.
|
|
* Optional on the type only because older serialised payloads
|
|
* (cached client state, test fixtures) may not have it yet.
|
|
*/
|
|
account_id?: string;
|
|
/**
|
|
* Caller's role within their account. Source of truth for every
|
|
* role-gated UI / API check — call `hasMinRole` from
|
|
* `@/lib/auth/roles` rather than comparing this string directly.
|
|
*/
|
|
account_role?: AccountRole;
|
|
created_at: string;
|
|
}
|
|
|
|
// ============================================================
|
|
// Account-sharing entities (017_account_sharing.sql)
|
|
// ============================================================
|
|
|
|
export interface Account {
|
|
id: string;
|
|
name: string;
|
|
/** auth.users.id of the immutable owner. */
|
|
owner_user_id: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
/**
|
|
* Hydrated member row for the Settings → Members tab. Combines
|
|
* the profile and its account_role for a single member of the
|
|
* caller's account. Sensitive fields (email) are populated only
|
|
* when the caller has admin+ — agents and viewers see name +
|
|
* avatar + role only.
|
|
*/
|
|
export interface AccountMember {
|
|
user_id: string;
|
|
full_name: string;
|
|
email: string | null;
|
|
avatar_url: string | null;
|
|
role: AccountRole;
|
|
joined_at: string;
|
|
}
|
|
|
|
/**
|
|
* Outstanding invite link row. `token_hash` is intentionally
|
|
* absent — it lives only in the DB and on the server. The
|
|
* plaintext token is returned once at creation time and surfaced
|
|
* via the invite URL; never re-emitted.
|
|
*/
|
|
export interface AccountInvitation {
|
|
id: string;
|
|
account_id: string;
|
|
/** Roles offered via invite — owner is never offered. */
|
|
role: Exclude<AccountRole, "owner">;
|
|
created_by_user_id: string | null;
|
|
label: string | null;
|
|
created_at: string;
|
|
expires_at: string;
|
|
accepted_at: string | null;
|
|
accepted_by_user_id: string | null;
|
|
}
|
|
|
|
export interface Contact {
|
|
id: string;
|
|
user_id: string;
|
|
account_id: string;
|
|
phone: string;
|
|
/** Digits-only form of `phone`, generated by the DB (migration 022)
|
|
* and unique per account. Read-only. */
|
|
phone_normalized?: string;
|
|
name?: string;
|
|
email?: string;
|
|
company?: string;
|
|
avatar_url?: string;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface Tag {
|
|
id: string;
|
|
user_id: string;
|
|
name: string;
|
|
color: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ContactTag {
|
|
id: string;
|
|
contact_id: string;
|
|
tag_id: string;
|
|
}
|
|
|
|
export interface CustomField {
|
|
id: string;
|
|
user_id: string;
|
|
/** Tenancy key — NOT NULL since migration 017. */
|
|
account_id: string;
|
|
field_name: string;
|
|
field_type: string;
|
|
field_options?: Record<string, unknown>;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface ContactCustomValue {
|
|
id: string;
|
|
contact_id: string;
|
|
custom_field_id: string;
|
|
value?: string;
|
|
}
|
|
|
|
export interface ContactNote {
|
|
id: string;
|
|
contact_id: string;
|
|
user_id: string;
|
|
note_text: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export type ConversationStatus = 'open' | 'pending' | 'closed';
|
|
|
|
export interface Conversation {
|
|
id: string;
|
|
user_id: string;
|
|
contact_id: string;
|
|
status: ConversationStatus;
|
|
assigned_agent_id?: string;
|
|
last_message_text?: string;
|
|
last_message_at?: string;
|
|
unread_count: number;
|
|
created_at: string;
|
|
updated_at: string;
|
|
contact?: Contact;
|
|
}
|
|
|
|
// ============================================================
|
|
// Notifications (migration 023)
|
|
// ============================================================
|
|
|
|
export type NotificationType = 'conversation_assigned';
|
|
|
|
export interface Notification {
|
|
id: string;
|
|
account_id: string;
|
|
/** Recipient — the agent this notification is for. */
|
|
user_id: string;
|
|
type: NotificationType;
|
|
conversation_id?: string;
|
|
contact_id?: string;
|
|
/** Who triggered it. Null when an automation/system assigned it. */
|
|
actor_user_id?: string;
|
|
title: string;
|
|
body?: string;
|
|
read_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export type SenderType = 'customer' | 'agent' | 'bot';
|
|
export type ContentType =
|
|
| 'text'
|
|
| 'image'
|
|
| 'document'
|
|
| 'audio'
|
|
| 'video'
|
|
| 'location'
|
|
| 'template'
|
|
/** Customer tapped a reply button or list row on a message we sent. */
|
|
| 'interactive';
|
|
export type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed';
|
|
|
|
export interface Message {
|
|
id: string;
|
|
conversation_id: string;
|
|
sender_type: SenderType;
|
|
sender_id?: string;
|
|
content_type: ContentType;
|
|
content_text?: string;
|
|
media_url?: string;
|
|
template_name?: string;
|
|
message_id?: string;
|
|
status: MessageStatus;
|
|
created_at: string;
|
|
reply_to_message_id?: string;
|
|
/**
|
|
* Only set when `content_type === 'interactive'` — the stable id of
|
|
* the button or list row the customer tapped. The Flows engine uses
|
|
* this to route the next node; the inbox bubble uses it as a styling
|
|
* cue (renders with a "↩ button reply" affordance).
|
|
*/
|
|
interactive_reply_id?: string;
|
|
}
|
|
|
|
export type ReactionActor = 'customer' | 'agent';
|
|
|
|
export interface MessageReaction {
|
|
id: string;
|
|
message_id: string;
|
|
conversation_id: string;
|
|
actor_type: ReactionActor;
|
|
actor_id?: string;
|
|
emoji: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface WhatsAppConfig {
|
|
id: string;
|
|
user_id: string;
|
|
phone_number_id: string;
|
|
waba_id?: string;
|
|
access_token: string;
|
|
verify_token?: string;
|
|
status: 'connected' | 'disconnected';
|
|
connected_at?: string;
|
|
/**
|
|
* Set when POST /{phone_number_id}/register last succeeded. NULL
|
|
* means the number was saved but never actually subscribed for
|
|
* webhooks on Meta's side — inbound events will be silently lost.
|
|
*/
|
|
registered_at?: string;
|
|
/** Set when POST /{waba_id}/subscribed_apps last succeeded. */
|
|
subscribed_apps_at?: string;
|
|
/** Last error from /register; cleared on success. */
|
|
last_registration_error?: string;
|
|
}
|
|
|
|
// Raw Meta status enum. We persist this verbatim from Meta (sync + webhook)
|
|
// rather than collapsing to a local TitleCase set — distinctions like
|
|
// PAUSED vs DISABLED vs IN_APPEAL drive the edit/resubmit/delete flows.
|
|
// DRAFT is the local-only state before the row is submitted to Meta.
|
|
export type MessageTemplateStatus =
|
|
| 'DRAFT'
|
|
| 'PENDING'
|
|
| 'APPROVED'
|
|
| 'REJECTED'
|
|
| 'PAUSED'
|
|
| 'DISABLED'
|
|
| 'IN_APPEAL'
|
|
| 'PENDING_DELETION';
|
|
|
|
export type TemplateButton =
|
|
| { type: 'QUICK_REPLY'; text: string }
|
|
| { type: 'URL'; text: string; url: string; example?: string }
|
|
| { type: 'PHONE_NUMBER'; text: string; phone_number: string }
|
|
| { type: 'COPY_CODE'; text: string; example: string };
|
|
|
|
export interface TemplateSampleValues {
|
|
body?: string[];
|
|
header?: string[];
|
|
}
|
|
|
|
export interface MessageTemplate {
|
|
id: string;
|
|
user_id: string;
|
|
name: string;
|
|
category: 'Marketing' | 'Utility' | 'Authentication';
|
|
language?: string;
|
|
header_type?: 'text' | 'image' | 'video' | 'document';
|
|
header_content?: string;
|
|
header_handle?: string;
|
|
header_media_url?: string;
|
|
body_text: string;
|
|
footer_text?: string;
|
|
buttons?: TemplateButton[];
|
|
sample_values?: TemplateSampleValues;
|
|
status?: MessageTemplateStatus;
|
|
meta_template_id?: string;
|
|
rejection_reason?: string;
|
|
quality_score?: 'GREEN' | 'YELLOW' | 'RED';
|
|
submission_error?: string;
|
|
last_submitted_at?: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface Pipeline {
|
|
id: string;
|
|
user_id: string;
|
|
name: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface PipelineStage {
|
|
id: string;
|
|
pipeline_id: string;
|
|
name: string;
|
|
position: number;
|
|
color: string;
|
|
created_at: string;
|
|
}
|
|
|
|
export type DealStatus = 'open' | 'won' | 'lost';
|
|
|
|
export interface Deal {
|
|
id: string;
|
|
user_id: string;
|
|
pipeline_id: string;
|
|
stage_id: string;
|
|
/**
|
|
* Nullable after migration 004 — becomes NULL when the referenced
|
|
* contact is deleted (ON DELETE SET NULL). History preserved.
|
|
*/
|
|
contact_id: string | null;
|
|
conversation_id?: string;
|
|
assigned_to?: string;
|
|
title: string;
|
|
value: number;
|
|
currency?: string;
|
|
notes?: string;
|
|
expected_close_date?: string;
|
|
status?: DealStatus;
|
|
created_at: string;
|
|
updated_at?: string;
|
|
contact?: Contact;
|
|
stage?: PipelineStage;
|
|
assignee?: Profile;
|
|
}
|
|
|
|
export type BroadcastStatus = 'draft' | 'scheduled' | 'sending' | 'sent' | 'failed';
|
|
export type RecipientStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'replied' | 'failed';
|
|
|
|
export interface Broadcast {
|
|
id: string;
|
|
user_id: string;
|
|
name: string;
|
|
template_name: string;
|
|
template_language: string;
|
|
template_variables?: Record<string, unknown>;
|
|
audience_filter?: Record<string, unknown>;
|
|
scheduled_at?: string;
|
|
status: BroadcastStatus;
|
|
total_recipients: number;
|
|
sent_count: number;
|
|
delivered_count: number;
|
|
read_count: number;
|
|
replied_count: number;
|
|
failed_count: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface BroadcastRecipient {
|
|
id: string;
|
|
broadcast_id: string;
|
|
/**
|
|
* Nullable after migration 004 — becomes NULL when the referenced
|
|
* contact is deleted (ON DELETE SET NULL). History preserved; the
|
|
* UI renders "Unknown" for orphaned rows.
|
|
*/
|
|
contact_id: string | null;
|
|
status: RecipientStatus;
|
|
sent_at?: string;
|
|
delivered_at?: string;
|
|
read_at?: string;
|
|
replied_at?: string;
|
|
error_message?: string;
|
|
/**
|
|
* Meta's message id, persisted when the broadcast send succeeds so
|
|
* the webhook can mirror status updates back onto the recipient row.
|
|
* Added in migration 003.
|
|
*/
|
|
whatsapp_message_id?: string;
|
|
created_at: string;
|
|
contact?: Contact;
|
|
}
|
|
|
|
// ============================================================
|
|
// Automations (migration 006)
|
|
// ============================================================
|
|
|
|
export type AutomationTriggerType =
|
|
| 'new_message_received'
|
|
| 'first_inbound_message'
|
|
| 'keyword_match'
|
|
| 'new_contact_created'
|
|
| 'conversation_assigned'
|
|
| 'tag_added'
|
|
| 'time_based';
|
|
|
|
export type AutomationStepType =
|
|
| 'send_message'
|
|
| 'send_template'
|
|
| 'add_tag'
|
|
| 'remove_tag'
|
|
| 'assign_conversation'
|
|
| 'update_contact_field'
|
|
| 'create_deal'
|
|
| 'wait'
|
|
| 'condition'
|
|
| 'send_webhook'
|
|
| 'close_conversation';
|
|
|
|
export type AutomationLogStatus = 'success' | 'partial' | 'failed';
|
|
|
|
export interface KeywordMatchTriggerConfig {
|
|
keywords: string[];
|
|
match_type: 'exact' | 'contains';
|
|
case_sensitive?: boolean;
|
|
}
|
|
|
|
export interface TagTriggerConfig {
|
|
tag_id: string;
|
|
}
|
|
|
|
export interface TimeBasedTriggerConfig {
|
|
/** Cron expression or simple HH:mm string; engine can accept either. */
|
|
schedule: string;
|
|
timezone?: string;
|
|
}
|
|
|
|
export type AutomationTriggerConfig =
|
|
| Record<string, never>
|
|
| KeywordMatchTriggerConfig
|
|
| TagTriggerConfig
|
|
| TimeBasedTriggerConfig
|
|
| Record<string, unknown>;
|
|
|
|
export interface SendMessageStepConfig {
|
|
text: string;
|
|
}
|
|
|
|
export interface SendTemplateStepConfig {
|
|
template_name: string;
|
|
language?: string;
|
|
variables?: Record<string, string>;
|
|
}
|
|
|
|
export interface TagStepConfig {
|
|
tag_id: string;
|
|
}
|
|
|
|
export interface AssignConversationStepConfig {
|
|
mode: 'specific' | 'round_robin';
|
|
agent_id?: string;
|
|
}
|
|
|
|
export interface UpdateContactFieldStepConfig {
|
|
/**
|
|
* Either a built-in contact column (`name` | `email` | `company`) or a
|
|
* custom field encoded as `custom:<custom_field_id>`. The `custom:` prefix
|
|
* is how the engine distinguishes a `contact_custom_values` write from a
|
|
* direct `contacts` column update. Older configs store the bare column name,
|
|
* so this stays backward compatible.
|
|
*/
|
|
field: string;
|
|
/** Supports `{{ vars.* }}` / `{{ message.text }}` interpolation at runtime. */
|
|
value: string;
|
|
}
|
|
|
|
export interface CreateDealStepConfig {
|
|
pipeline_id: string;
|
|
stage_id: string;
|
|
title: string;
|
|
value?: number;
|
|
}
|
|
|
|
export interface WaitStepConfig {
|
|
amount: number;
|
|
unit: 'minutes' | 'hours' | 'days';
|
|
}
|
|
|
|
export type ConditionSubject =
|
|
| 'contact_field'
|
|
| 'tag_presence'
|
|
| 'message_content'
|
|
| 'time_of_day';
|
|
|
|
export interface ConditionStepConfig {
|
|
subject: ConditionSubject;
|
|
/** e.g. field name, tag id, substring, or "HH:mm-HH:mm" depending on subject */
|
|
operand?: string;
|
|
/** For contact_field equals / message_content contains — comparison value */
|
|
value?: string;
|
|
}
|
|
|
|
export interface SendWebhookStepConfig {
|
|
url: string;
|
|
headers?: Record<string, string>;
|
|
body_template?: string;
|
|
}
|
|
|
|
export type AutomationStepConfig =
|
|
| SendMessageStepConfig
|
|
| SendTemplateStepConfig
|
|
| TagStepConfig
|
|
| AssignConversationStepConfig
|
|
| UpdateContactFieldStepConfig
|
|
| CreateDealStepConfig
|
|
| WaitStepConfig
|
|
| ConditionStepConfig
|
|
| SendWebhookStepConfig
|
|
| Record<string, never>
|
|
| Record<string, unknown>;
|
|
|
|
export interface Automation {
|
|
id: string;
|
|
/** Account tenancy key — every automation belongs to one account
|
|
* (migration 017 made the column NOT NULL). The engine looks up
|
|
* active automations by this field on inbound webhook events. */
|
|
account_id: string;
|
|
/** Original author. Used for log audit + outbound message
|
|
* sender-of-record, never for tenancy isolation. */
|
|
user_id: string;
|
|
name: string;
|
|
description?: string;
|
|
trigger_type: AutomationTriggerType;
|
|
trigger_config: AutomationTriggerConfig;
|
|
is_active: boolean;
|
|
execution_count: number;
|
|
last_executed_at?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface AutomationStep {
|
|
id: string;
|
|
automation_id: string;
|
|
parent_step_id?: string | null;
|
|
branch?: 'yes' | 'no' | null;
|
|
step_type: AutomationStepType;
|
|
step_config: AutomationStepConfig;
|
|
position: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface AutomationLogStepResult {
|
|
step_id: string;
|
|
step_type: AutomationStepType;
|
|
status: 'success' | 'skipped' | 'failed';
|
|
detail?: string;
|
|
}
|
|
|
|
export interface AutomationLog {
|
|
id: string;
|
|
automation_id: string;
|
|
user_id: string;
|
|
contact_id: string | null;
|
|
trigger_event: string;
|
|
steps_executed: AutomationLogStepResult[];
|
|
status: AutomationLogStatus;
|
|
error_message?: string | null;
|
|
created_at: string;
|
|
contact?: Contact;
|
|
}
|