mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
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>
412 lines
9.3 KiB
TypeScript
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;
|
|
}
|