wacrm/src/types/index.ts
Arnas Donauskas 4e89b2b902
feat(flows): soft-GA — open to all users + Beta label
The Flows feature was previously gated on a per-account
`profiles.beta_features` opt-in: the sidebar entry was invisible
to non-beta accounts, every /api/flows route 404'd, the webhook
dispatcher was a no-op, and the agent-handoff pause was skipped.

This PR opens the surface to every authenticated user. A small
"Beta" chip in the sidebar nav + on the /flows page H1 keeps the
"young feature" signal without holding anyone out.

Removed:
- `src/lib/flows/feature-flag.ts` + its tests (-11 tests).
- The `requireFlowsBeta` / `requireFlowsBetaAndOwnership` helpers
  in the API routes — collapsed to plain `requireUser` / RLS
  ownership.
- The `isProfileFlowsEnabled` helper + branch in the webhook
  dispatcher — the runner takes the cheap "no_match" exit on
  accounts with no active flows (single indexed SELECT).
- The agent-handoff pause gate in /api/whatsapp/send — the UPDATE
  matches zero rows when no active run exists, also cheap.
- Client-side beta gates in the 3 flows pages (and the redundant
  `useAuth` + `profileLoading` plumbing they needed).
- The defensive message-bubble gate that would have rendered
  interactive replies as plain text for non-beta accounts.
- The `betaKey` field on the sidebar NavItem schema, replaced by a
  simpler boolean `beta` that purely drives the visual chip.

Kept:
- `profiles.beta_features` column. Generic mechanism for future
  beta gates; no current feature reads it. Comments in
  `src/types/index.ts` + `use-auth.tsx` updated to say so.
- The chip can be removed in a one-liner once the team's satisfied
  Flows has bedded in.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 17:16:58 +03:00

412 lines
9.3 KiB
TypeScript

export interface Profile {
id: string;
user_id: string;
full_name: string;
email: string;
avatar_url?: string;
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[];
created_at: string;
}
export interface Contact {
id: string;
user_id: string;
phone: 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;
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;
}
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;
}
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;
body_text: string;
footer_text?: string;
buttons?: Record<string, unknown>[];
status?: 'Draft' | 'Pending' | 'Approved' | 'Rejected';
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 {
field: string;
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;
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;
}