     1→/**
     2→ * Prompts Hub API routes
     3→ * Manages prompt templates, AI generation, and bulk sending
     4→ */
     5→
     6→const express = require('express');
     7→const router = express.Router();
     8→const { supabase } = require('../config/database');
     9→const { generateChatCompletion } = require('../services/openai');
    10→const { sendSMS, sendBatchSMS, isConfigured } = require('../services/twilio');
    11→const { normalizeAustralianPhone } = require('../utils/phone');
    12→const { sanitizeString } = require('../utils/validation');
    13→
    14→// ============ TEMPLATE CRUD ============
    15→
    16→/**
    17→ * GET /api/prompts - List all templates
    18→ */
    19→router.get('/', async (req, res) => {
    20→  try {
    21→    const { category, target_type, target_stage, active_only, search } = req.query;
    22→
    23→    let query = supabase
    24→      .from('prompt_templates')
    25→      .select('*')
    26→      .order('times_used', { ascending: false });
    27→
    28→    if (category && category !== 'all') {
    29→      query = query.eq('category', category);
    30→    }
    31→    if (target_type && target_type !== 'all') {
    32→      query = query.eq('target_type', target_type);
    33→    }
    34→    if (target_stage) {
    35→      query = query.eq('target_stage', target_stage);
    36→    }
    37→    if (active_only === 'true') {
    38→      query = query.eq('active', true);
    39→    }
    40→    if (search) {
    41→      // Sanitize search input and escape SQL wildcards
    42→      const sanitized = sanitizeString(search, 100)
    43→        .replace(/%/g, '\\%')  // Escape % wildcard
    44→        .replace(/_/g, '\\_'); // Escape _ wildcard
    45→      if (sanitized) {
    46→        query = query.or(`name.ilike.%${sanitized}%,template.ilike.%${sanitized}%`);
    47→      }
    48→    }
    49→
    50→    const { data: templates, error } = await query;
    51→
    52→    if (error) throw error;
    53→
    54→    // Group by category for UI
    55→    const categories = [...new Set(templates.map(t => t.category))];
    56→
    57→    res.json({
    58→      templates,
    59→      categories,
    60→      total: templates.length
    61→    });
    62→  } catch (error) {
    63→    console.error('Error fetching prompts:', error);
    64→    res.status(500).json({ error: 'Failed to fetch prompts' });
    65→  }
    66→});
    67→
    68→/**
    69→ * GET /api/prompts/:id - Get single template
    70→ */
    71→router.get('/:id', async (req, res) => {
    72→  try {
    73→    const { id } = req.params;
    74→
    75→    const { data: template, error } = await supabase
    76→      .from('prompt_templates')
    77→      .select('*')
    78→      .eq('id', id)
    79→      .single();
    80→
    81→    if (error) throw error;
    82→    if (!template) {
    83→      return res.status(404).json({ error: 'Template not found' });
    84→    }
    85→
    86→    // Get recent send history for this template
    87→    const { data: history } = await supabase
    88→      .from('bulk_send_history')
    89→      .select('id, recipient_count, sent_at, status, delivered_count, reply_count')
    90→      .eq('prompt_template_id', id)
    91→      .order('created_at', { ascending: false })
    92→      .limit(5);
    93→
    94→    res.json({
    95→      template,
    96→      recentHistory: history || []
    97→    });
    98→  } catch (error) {
    99→    console.error('Error fetching prompt:', error);
   100→    res.status(500).json({ error: 'Failed to fetch prompt' });
   101→  }
   102→});
   103→
   104→/**
   105→ * POST /api/prompts - Create template
   106→ */
   107→router.post('/', async (req, res) => {
   108→  try {
   109→    const {
   110→      name,
   111→      category,
   112→      description,
   113→      template,
   114→      variables,
   115→      target_type,
   116→      target_stage,
   117→      tone,
   118→      ai_generated,
   119→      ai_prompt
   120→    } = req.body;
   121→
   122→    if (!name || !template) {
   123→      return res.status(400).json({ error: 'Name and template are required' });
   124→    }
   125→
   126→    // Auto-extract variables from template if not provided
   127→    let extractedVars = variables || [];
   128→    if (!variables || variables.length === 0) {
   129→      const varRegex = /\{(\w+)\}/g;
   130→      let match;
   131→      while ((match = varRegex.exec(template)) !== null) {
   132→        if (!extractedVars.includes(match[1])) {
   133→          extractedVars.push(match[1]);
   134→        }
   135→      }
   136→    }
   137→
   138→    const { data: newTemplate, error } = await supabase
   139→      .from('prompt_templates')
   140→      .insert({
   141→        name,
   142→        category: category || 'custom',
   143→        description,
   144→        template,
   145→        variables: extractedVars,
   146→        target_type: target_type || 'all',
   147→        target_stage,
   148→        tone: tone || 'friendly',
   149→        ai_generated: ai_generated || false,
   150→        ai_prompt
   151→      })
   152→      .select()
   153→      .single();
   154→
   155→    if (error) throw error;
   156→
   157→    res.status(201).json({ template: newTemplate });
   158→  } catch (error) {
   159→    console.error('Error creating prompt:', error);
   160→    res.status(500).json({ error: 'Failed to create prompt' });
   161→  }
   162→});
   163→
   164→/**
   165→ * PUT /api/prompts/:id - Update template
   166→ */
   167→router.put('/:id', async (req, res) => {
   168→  try {
   169→    const { id } = req.params;
   170→    const updates = req.body;
   171→
   172→    // Remove fields that shouldn't be updated directly
   173→    delete updates.id;
   174→    delete updates.times_used;
   175→    delete updates.send_count;
   176→    delete updates.delivery_count;
   177→    delete updates.reply_count;
   178→    delete updates.conversion_count;
   179→    delete updates.created_at;
   180→
   181→    const { data: template, error } = await supabase
   182→      .from('prompt_templates')
   183→      .update(updates)
   184→      .eq('id', id)
   185→      .select()
   186→      .single();
   187→
   188→    if (error) throw error;
   189→
   190→    res.json({ template });
   191→  } catch (error) {
   192→    console.error('Error updating prompt:', error);
   193→    res.status(500).json({ error: 'Failed to update prompt' });
   194→  }
   195→});
   196→
   197→/**
   198→ * DELETE /api/prompts/:id - Delete template
   199→ */
   200→router.delete('/:id', async (req, res) => {
   201→  try {
   202→    const { id } = req.params;
   203→
   204→    // Don't allow deleting default templates
   205→    const { data: template } = await supabase
   206→      .from('prompt_templates')
   207→      .select('is_default')
   208→      .eq('id', id)
   209→      .single();
   210→
   211→    if (template?.is_default) {
   212→      return res.status(400).json({ error: 'Cannot delete default templates' });
   213→    }
   214→
   215→    const { error } = await supabase
   216→      .from('prompt_templates')
   217→      .delete()
   218→      .eq('id', id);
   219→
   220→    if (error) throw error;
   221→
   222→    res.status(204).send();
   223→  } catch (error) {
   224→    console.error('Error deleting prompt:', error);
   225→    res.status(500).json({ error: 'Failed to delete prompt' });
   226→  }
   227→});
   228→
   229→// ============ AI GENERATION ============
   230→
   231→/**
   232→ * POST /api/prompts/generate - AI generate prompts
   233→ */
   234→router.post('/generate', async (req, res) => {
   235→  try {
   236→    const { intent, tone, target_type, category, count = 3 } = req.body;
   237→
   238→    if (!intent) {
   239→      return res.status(400).json({ error: 'Intent is required' });
   240→    }
   241→
   242→    const toneGuide = {
   243→      friendly: 'casual, warm, and approachable like texting a friend',
   244→      professional: 'polished but not stiff, business-casual tone',
   245→      urgent: 'creates urgency without being pushy, time-sensitive feel',
   246→      casual: 'very relaxed, uses common abbreviations and emoji sparingly'
   247→    };
   248→
   249→    const targetGuide = {
   250→      worker: 'blue-collar tradespeople looking for work opportunities',
   251→      contractor: 'business owners and hiring managers looking to hire workers',
   252→      all: 'both workers and contractors in the construction/trades industry'
   253→    };
   254→
   255→    const categoryGuide = {
   256→      cold_call: 'first contact with someone who hasn\'t engaged yet',
   257→      follow_up: 'checking in after previous contact, no response yet',
   258→      reengagement: 'reconnecting with someone who went cold/inactive',
   259→      closing: 'pushing for a decision or commitment',
   260→      onboarding: 'welcoming new signups and guiding next steps',
   261→      objection: 'handling specific concerns or pushback'
   262→    };
   263→
   264→    const systemPrompt = `You are an expert sales copywriter for RateRight, a platform that connects tradespeople with job opportunities.
   265→
   266→Generate ${count} SMS message templates based on the user's intent.
   267→
   268→Rules:
   269→- Keep messages under 160 characters when possible (max 320)
   270→- Use {firstName} for personalization (not {{firstName}})
   271→- Include a clear call-to-action
   272→- Sound human, not robotic
   273→- No spam-like language or excessive punctuation
   274→- Australian English (favour, colour, etc.)
   275→
   276→Target audience: ${targetGuide[target_type] || targetGuide.all}
   277→Tone: ${toneGuide[tone] || toneGuide.friendly}
   278→${category ? `Category: ${categoryGuide[category] || category}` : ''}
   279→
   280→Return a JSON array of objects with:
   281→- template: the SMS text with {variables}
   282→- variables: array of variable names used
   283→- name: short descriptive name for the template`;
   284→
   285→    const response = await generateChatCompletion([
   286→      { role: 'system', content: systemPrompt },
   287→      { role: 'user', content: `Generate ${count} SMS templates for this intent: "${intent}"` }
   288→    ], {
   289→      model: 'gpt-4o-mini',
   290→      temperature: 0.8,
   291→      response_format: { type: 'json_object' }
   292→    });
   293→
   294→    let generated;
   295→    try {
   296→      const parsed = JSON.parse(response);
   297→      generated = parsed.templates || parsed.options || parsed;
   298→      if (!Array.isArray(generated)) {
   299→        generated = [generated];
   300→      }
   301→    } catch (e) {
   302→      console.error('Failed to parse AI response:', e);
   303→      return res.status(500).json({ error: 'Failed to parse AI response' });
   304→    }
   305→
   306→    // Add metadata to generated templates
   307→    const templates = generated.map((t, i) => ({
   308→      ...t,
   309→      category: category || 'custom',
   310→      tone: tone || 'friendly',
   311→      target_type: target_type || 'all',
   312→      ai_generated: true,
   313→      ai_prompt: intent
   314→    }));
   315→
   316→    res.json({ templates });
   317→  } catch (error) {
   318→    console.error('Error generating prompts:', error);
   319→    res.status(500).json({ error: 'Failed to generate prompts' });
   320→  }
   321→});
   322→
   323→// ============ SENDING ============
   324→
   325→/**
   326→ * POST /api/prompts/:id/preview - Preview personalized messages
   327→ */
   328→router.post('/:id/preview', async (req, res) => {
   329→  try {
   330→    const { id } = req.params;
   331→    const { lead_ids, limit = 3 } = req.body;
   332→
   333→    // Get template
   334→    const { data: template, error: templateError } = await supabase
   335→      .from('prompt_templates')
   336→      .select('*')
   337→      .eq('id', id)
   338→      .single();
   339→
   340→    if (templateError || !template) {
   341→      return res.status(404).json({ error: 'Template not found' });
   342→    }
   343→
   344→    // Get leads for preview
   345→    let query = supabase
   346→      .from('leads')
   347→      .select('id, first_name, last_name, company, phone, lead_type, platform_stage')
   348→      .not('phone', 'is', null);
   349→
   350→    if (lead_ids && lead_ids.length > 0) {
   351→      query = query.in('id', lead_ids.slice(0, limit));
   352→    } else {
   353→      query = query.limit(limit);
   354→    }
   355→
   356→    const { data: leads, error: leadsError } = await query;
   357→
   358→    if (leadsError) throw leadsError;
   359→
   360→    // Generate personalized previews
   361→    const previews = leads.map(lead => {
   362→      let message = template.template;
   363→
   364→      // Replace variables (support both {var} and {{var}} formats)
   365→      const vars = {
   366→        firstName: lead.first_name || 'there',
   367→        first_name: lead.first_name || 'there',
   368→        lastName: lead.last_name || '',
   369→        last_name: lead.last_name || '',
   370→        company: lead.company || 'your company',
   371→        name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   372→        full_name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   373→        agent_name: 'Team RateRight',
   374→        sender_name: 'Team RateRight'
   375→      };
   376→
   377→      Object.entries(vars).forEach(([key, value]) => {
   378→        // Handle both {var} and {{var}} formats
   379→        message = message.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'gi'), value);
   380→        message = message.replace(new RegExp(`\\{${key}\\}`, 'gi'), value);
   381→      });
   382→
   383→      // Remove any remaining unmatched variables
   384→      message = message.replace(/\{\{\s*\w+\s*\}\}/g, '');
   385→      message = message.replace(/\{\w+\}/g, '');
   386→      // Clean up double spaces
   387→      message = message.replace(/  +/g, ' ').trim();
   388→
   389→      return {
   390→        leadId: lead.id,
   391→        leadName: `${lead.first_name || ''} ${lead.last_name || ''}`.trim(),
   392→        phone: lead.phone,
   393→        message,
   394→        characterCount: message.length
   395→      };
   396→    });
   397→
   398→    res.json({
   399→      template: {
   400→        id: template.id,
   401→        name: template.name,
   402→        original: template.template
   403→      },
   404→      previews,
   405→      estimatedCost: previews.length * 0.05 // Rough SMS cost estimate
   406→    });
   407→  } catch (error) {
   408→    console.error('Error generating preview:', error);
   409→    res.status(500).json({ error: 'Failed to generate preview' });
   410→  }
   411→});
   412→
   413→/**
   414→ * POST /api/prompts/:id/send - Send to individual
   415→ */
   416→router.post('/:id/send', async (req, res) => {
   417→  try {
   418→    const { id } = req.params;
   419→    const { lead_id, custom_message } = req.body;
   420→
   421→    if (!lead_id) {
   422→      return res.status(400).json({ error: 'lead_id is required' });
   423→    }
   424→
   425→    // Get template
   426→    const { data: template, error: templateError } = await supabase
   427→      .from('prompt_templates')
   428→      .select('*')
   429→      .eq('id', id)
   430→      .single();
   431→
   432→    if (templateError || !template) {
   433→      return res.status(404).json({ error: 'Template not found' });
   434→    }
   435→
   436→    // Get lead
   437→    const { data: lead, error: leadError } = await supabase
   438→      .from('leads')
   439→      .select('*')
   440→      .eq('id', lead_id)
   441→      .single();
   442→
   443→    if (leadError || !lead) {
   444→      return res.status(404).json({ error: 'Lead not found' });
   445→    }
   446→
   447→    if (!lead.phone) {
   448→      return res.status(400).json({ error: 'Lead has no phone number' });
   449→    }
   450→
   451→    // Personalize message (support both {var} and {{var}} formats)
   452→    let message = custom_message || template.template;
   453→    const vars = {
   454→      firstName: lead.first_name || 'there',
   455→      first_name: lead.first_name || 'there',
   456→      lastName: lead.last_name || '',
   457→      last_name: lead.last_name || '',
   458→      company: lead.company || 'your company',
   459→      name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   460→      full_name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   461→      agent_name: 'Team RateRight',
   462→      sender_name: 'Team RateRight'
   463→    };
   464→
   465→    Object.entries(vars).forEach(([key, value]) => {
   466→      message = message.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'gi'), value);
   467→      message = message.replace(new RegExp(`\\{${key}\\}`, 'gi'), value);
   468→    });
   469→
   470→    // Remove any remaining unmatched variables
   471→    message = message.replace(/\{\{\s*\w+\s*\}\}/g, '');
   472→    message = message.replace(/\{\w+\}/g, '');
   473→    message = message.replace(/  +/g, ' ').trim();
   474→
   475→    // Send SMS
   476→    const phone = normalizeAustralianPhone(lead.phone);
   477→    const result = await sendSMS(phone, message);
   478→
   479→    // Log the send
   480→    await supabase.from('prompt_send_log').insert({
   481→      prompt_template_id: id,
   482→      lead_id,
   483→      personalized_message: message,
   484→      delivery_status: result.success ? 'sent' : 'failed',
   485→      day_of_week: new Date().toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase(),
   486→      hour_of_day: new Date().getHours()
   487→    });
   488→
   489→    // Update template usage
   490→    await supabase
   491→      .from('prompt_templates')
   492→      .update({
   493→        times_used: template.times_used + 1,
   494→        send_count: (template.send_count || 0) + 1,
   495→        last_used_at: new Date().toISOString()
   496→      })
   497→      .eq('id', id);
   498→
   499→    // Update template metrics (delivery rate, reply rate, etc.)
   500→    await supabase.rpc('update_prompt_template_metrics', { template_id: id });
   501→
   502→    // Log communication
   503→    await supabase.from('communications').insert({
   504→      lead_id,
   505→      type: 'sms_outbound',
   506→      direction: 'outbound',
   507→      phone: lead.phone,
   508→      content: message,
   509→      external_id: result.sid,
   510→      delivery_status: result.success ? 'sent' : 'failed'
   511→    });
   512→
   513→    // Update last contact
   514→    await supabase
   515→      .from('leads')
   516→      .update({
   517→        last_contact_at: new Date().toISOString(),
   518→        last_contact_type: 'sms_outbound'
   519→      })
   520→      .eq('id', lead_id);
   521→
   522→    res.json({
   523→      success: result.success,
   524→      message: result.success ? 'Message sent' : 'Failed to send',
   525→      sid: result.sid
   526→    });
   527→  } catch (error) {
   528→    console.error('Error sending prompt:', error);
   529→    res.status(500).json({ error: 'Failed to send message' });
   530→  }
   531→});
   532→
   533→/**
   534→ * POST /api/prompts/:id/bulk-send - Bulk send with filters
   535→ */
   536→router.post('/:id/bulk-send', async (req, res) => {
   537→  try {
   538→    const { id } = req.params;
   539→    const { lead_ids, filter, scheduled_at } = req.body;
   540→
   541→    if (!lead_ids || lead_ids.length === 0) {
   542→      return res.status(400).json({ error: 'lead_ids are required' });
   543→    }
   544→
   545→    // Get template
   546→    const { data: template, error: templateError } = await supabase
   547→      .from('prompt_templates')
   548→      .select('*')
   549→      .eq('id', id)
   550→      .single();
   551→
   552→    if (templateError || !template) {
   553→      return res.status(404).json({ error: 'Template not found' });
   554→    }
   555→
   556→    // Get leads
   557→    const { data: leads, error: leadsError } = await supabase
   558→      .from('leads')
   559→      .select('id, first_name, last_name, company, phone')
   560→      .in('id', lead_ids)
   561→      .not('phone', 'is', null);
   562→
   563→    if (leadsError) throw leadsError;
   564→
   565→    if (!leads || leads.length === 0) {
   566→      return res.status(400).json({ error: 'No valid leads with phone numbers found' });
   567→    }
   568→
   569→    // Create bulk send history record
   570→    const { data: bulkSend, error: bulkError } = await supabase
   571→      .from('bulk_send_history')
   572→      .insert({
   573→        prompt_template_id: id,
   574→        template_snapshot: template.template,
   575→        recipient_count: leads.length,
   576→        recipient_filter: filter || null,
   577→        recipient_ids: lead_ids,
   578→        scheduled_at: scheduled_at || null,
   579→        status: scheduled_at ? 'scheduled' : 'sending'
   580→      })
   581→      .select()
   582→      .single();
   583→
   584→    if (bulkError) throw bulkError;
   585→
   586→    // If scheduled, return early
   587→    if (scheduled_at) {
   588→      return res.json({
   589→        success: true,
   590→        bulkSendId: bulkSend.id,
   591→        status: 'scheduled',
   592→        scheduledAt: scheduled_at,
   593→        recipientCount: leads.length
   594→      });
   595→    }
   596→
   597→    // Send messages
   598→    let sentCount = 0;
   599→    let failedCount = 0;
   600→    const errors = [];
   601→
   602→    for (const lead of leads) {
   603→      try {
   604→        // Personalize message (support both {var} and {{var}} formats)
   605→        let message = template.template;
   606→        const vars = {
   607→          firstName: lead.first_name || 'there',
   608→          first_name: lead.first_name || 'there',
   609→          lastName: lead.last_name || '',
   610→          last_name: lead.last_name || '',
   611→          company: lead.company || 'your company',
   612→          name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   613→          full_name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'there',
   614→          agent_name: 'Team RateRight',
   615→          sender_name: 'Team RateRight'
   616→        };
   617→
   618→        Object.entries(vars).forEach(([key, value]) => {
   619→          message = message.replace(new RegExp(`\\{\\{\\s*${key}\\s*\\}\\}`, 'gi'), value);
   620→          message = message.replace(new RegExp(`\\{${key}\\}`, 'gi'), value);
   621→        });
   622→
   623→        // Remove any remaining unmatched variables
   624→        message = message.replace(/\{\{\s*\w+\s*\}\}/g, '');
   625→        message = message.replace(/\{\w+\}/g, '');
   626→        message = message.replace(/  +/g, ' ').trim();
   627→
   628→        // Send SMS
   629→        const phone = normalizeAustralianPhone(lead.phone);
   630→        const result = await sendSMS(phone, message);
   631→
   632→        if (result.success) {
   633→          sentCount++;
   634→
   635→          // Log the send
   636→          await supabase.from('prompt_send_log').insert({
   637→            prompt_template_id: id,
   638→            bulk_send_id: bulkSend.id,
   639→            lead_id: lead.id,
   640→            personalized_message: message,
   641→            delivery_status: 'sent',
   642→            day_of_week: new Date().toLocaleDateString('en-US', { weekday: 'long' }).toLowerCase(),
   643→            hour_of_day: new Date().getHours()
   644→          });
   645→
   646→          // Log communication
   647→          await supabase.from('communications').insert({
   648→            lead_id: lead.id,
   649→            type: 'sms_outbound',
   650→            direction: 'outbound',
   651→            phone: lead.phone,
   652→            content: message,
   653→            external_id: result.sid,
   654→            delivery_status: 'sent'
   655→          });
   656→
   657→          // Update last contact
   658→          await supabase
   659→            .from('leads')
   660→            .update({
   661→              last_contact_at: new Date().toISOString(),
   662→              last_contact_type: 'sms_outbound'
   663→            })
   664→            .eq('id', lead.id);
   665→        } else {
   666→          failedCount++;
   667→          errors.push({ leadId: lead.id, error: result.error || 'Unknown error' });
   668→        }
   669→      } catch (err) {
   670→        failedCount++;
   671→        errors.push({ leadId: lead.id, error: err.message });
   672→      }
   673→    }
   674→
   675→    // Update bulk send record
   676→    await supabase
   677→      .from('bulk_send_history')
   678→      .update({
   679→        status: failedCount === leads.length ? 'failed' : (failedCount > 0 ? 'partial' : 'completed'),
   680→        sent_at: new Date().toISOString(),
   681→        sent_count: sentCount,
   682→        failed_count: failedCount,
   683→        errors: errors.length > 0 ? errors : null,
   684→        completed_at: new Date().toISOString()
   685→      })
   686→      .eq('id', bulkSend.id);
   687→
   688→    // Update template usage
   689→    await supabase
   690→      .from('prompt_templates')
   691→      .update({
   692→        times_used: template.times_used + sentCount,
   693→        send_count: template.send_count + sentCount,
   694→        last_used_at: new Date().toISOString()
   695→      })
   696→      .eq('id', id);
   697→
   698→    // Update template metrics (delivery rate, reply rate, etc.)
   699→    await supabase.rpc('update_prompt_template_metrics', { template_id: id });
   700→
   701→    res.json({
   702→      success: true,
   703→      bulkSendId: bulkSend.id,
   704→      status: failedCount === leads.length ? 'failed' : (failedCount > 0 ? 'partial' : 'completed'),
   705→      sentCount,
   706→      failedCount,
   707→      errors: errors.length > 0 ? errors : undefined
   708→    });
   709→  } catch (error) {
   710→    console.error('Error bulk sending:', error);
   711→    res.status(500).json({ error: 'Failed to send messages' });
   712→  }
   713→});
   714→
   715→// ============ RECIPIENTS ============
   716→
   717→/**
   718→ * POST /api/prompts/recipients - Get matching recipients
   719→ */
   720→router.post('/recipients', async (req, res) => {
   721→  try {
   722→    const { filter, limit = 50 } = req.body;
   723→
   724→    let query = supabase
   725→      .from('leads')
   726→      .select('id, first_name, last_name, company, phone, lead_type, platform_stage, status, last_contact_at, score')
   727→      .not('phone', 'is', null)
   728→      .is('deleted_at', null);
   729→
   730→    // Apply filters
   731→    if (filter) {
   732→      // Quick filters
   733→      if (filter.quick === 'slipping') {
   734→        // Leads not contacted in 7+ days
   735→        const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
   736→        query = query.or(`last_contact_at.lt.${sevenDaysAgo},last_contact_at.is.null`);
   737→        query = query.in('status', ['hot', 'warm', 'active']);
   738→      } else if (filter.quick === 'new_signups') {
   739→        // New in last 7 days
   740→        const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString();
   741→        query = query.gte('created_at', sevenDaysAgo);
   742→      } else if (filter.quick === 'hot') {
   743→        query = query.eq('status', 'hot');
   744→      } else if (filter.quick === 'stuck') {
   745→        // In same stage for 14+ days
   746→        query = query.gte('days_in_stage', 14);
   747→      }
   748→
   749→      // Type filter
   750→      if (filter.type && filter.type !== 'all') {
   751→        query = query.eq('lead_type', filter.type);
   752→      }
   753→
   754→      // Stage filter
   755→      if (filter.stage) {
   756→        query = query.eq('platform_stage', filter.stage);
   757→      }
   758→
   759→      // Status filter
   760→      if (filter.status) {
   761→        query = query.eq('status', filter.status);
   762→      }
   763→
   764→      // Last contact filter
   765→      if (filter.lastContactDays) {
   766→        const daysAgo = new Date(Date.now() - filter.lastContactDays * 24 * 60 * 60 * 1000).toISOString();
   767→        query = query.lt('last_contact_at', daysAgo);
   768→      }
   769→
   770→      // Never contacted
   771→      if (filter.neverContacted) {
   772→        query = query.is('last_contact_at', null);
   773→      }
   774→    }
   775→
   776→    query = query.order('score', { ascending: false }).limit(limit);
   777→
   778→    const { data: leads, error } = await query;
   779→
   780→    if (error) throw error;
   781→
   782→    // Get total count for pagination info
   783→    let countQuery = supabase
   784→      .from('leads')
   785→      .select('id', { count: 'exact', head: true })
   786→      .not('phone', 'is', null)
   787→      .is('deleted_at', null);
   788→
   789→    // Apply same filters for count
   790→    if (filter?.type && filter.type !== 'all') {
   791→      countQuery = countQuery.eq('lead_type', filter.type);
   792→    }
   793→
   794→    const { count } = await countQuery;
   795→
   796→    res.json({
   797→      recipients: leads.map(lead => ({
   798→        id: lead.id,
   799→        name: `${lead.first_name || ''} ${lead.last_name || ''}`.trim() || 'Unknown',
   800→        firstName: lead.first_name,
   801→        company: lead.company,
   802→        phone: lead.phone,
   803→        type: lead.lead_type,
   804→        stage: lead.platform_stage,
   805→        status: lead.status,
   806→        lastContact: lead.last_contact_at,
   807→        score: lead.score
   808→      })),
   809→      total: count || leads.length,
   810→      returned: leads.length
   811→    });
   812→  } catch (error) {
   813→    console.error('Error fetching recipients:', error);
   814→    res.status(500).json({ error: 'Failed to fetch recipients' });
   815→  }
   816→});
   817→
   818→// ============ HISTORY & STATS ============
   819→
   820→/**
   821→ * GET /api/prompts/history - Get bulk send history
   822→ */
   823→router.get('/history', async (req, res) => {
   824→  try {
   825→    const { limit = 20 } = req.query;
   826→
   827→    const { data: history, error } = await supabase
   828→      .from('bulk_send_history')
   829→      .select(`
   830→        *,
   831→        prompt_templates (
   832→          id,
   833→          name,
   834→          category
   835→        )
   836→      `)
   837→      .order('created_at', { ascending: false })
   838→      .limit(parseInt(limit));
   839→
   840→    if (error) throw error;
   841→
   842→    res.json({ history });
   843→  } catch (error) {
   844→    console.error('Error fetching history:', error);
   845→    res.status(500).json({ error: 'Failed to fetch history' });
   846→  }
   847→});
   848→
   849→/**
   850→ * GET /api/prompts/stats - Get overall prompt stats
   851→ */
   852→router.get('/stats', async (req, res) => {
   853→  try {
   854→    // Get aggregate stats
   855→    const { data: templates, error } = await supabase
   856→      .from('prompt_templates')
   857→      .select('send_count, delivery_count, reply_count, conversion_count, category')
   858→      .eq('active', true);
   859→
   860→    if (error) throw error;
   861→
   862→    const totals = templates.reduce((acc, t) => ({
   863→      totalSent: acc.totalSent + (t.send_count || 0),
   864→      totalDelivered: acc.totalDelivered + (t.delivery_count || 0),
   865→      totalReplies: acc.totalReplies + (t.reply_count || 0),
   866→      totalConversions: acc.totalConversions + (t.conversion_count || 0)
   867→    }), { totalSent: 0, totalDelivered: 0, totalReplies: 0, totalConversions: 0 });
   868→
   869→    // Category breakdown
   870→    const byCategory = templates.reduce((acc, t) => {
   871→      const cat = t.category || 'other';
   872→      if (!acc[cat]) {
   873→        acc[cat] = { sent: 0, replies: 0, conversions: 0 };
   874→      }
   875→      acc[cat].sent += t.send_count || 0;
   876→      acc[cat].replies += t.reply_count || 0;
   877→      acc[cat].conversions += t.conversion_count || 0;
   878→      return acc;
   879→    }, {});
   880→
   881→    // Get top performing templates
   882→    const { data: topTemplates } = await supabase
   883→      .from('prompt_templates')
   884→      .select('id, name, category, reply_rate, conversion_rate, times_used')
   885→      .eq('active', true)
   886→      .gte('times_used', 5) // Only templates with meaningful usage
   887→      .order('reply_rate', { ascending: false })
   888→      .limit(5);
   889→
   890→    res.json({
   891→      totals,
   892→      rates: {
   893→        deliveryRate: totals.totalSent > 0 ? Math.round((totals.totalDelivered / totals.totalSent) * 100) : 0,
   894→        replyRate: totals.totalDelivered > 0 ? Math.round((totals.totalReplies / totals.totalDelivered) * 100) : 0,
   895→        conversionRate: totals.totalSent > 0 ? Math.round((totals.totalConversions / totals.totalSent) * 100) : 0
   896→      },
   897→      byCategory,
   898→      topTemplates: topTemplates || []
   899→    });
   900→  } catch (error) {
   901→    console.error('Error fetching stats:', error);
   902→    res.status(500).json({ error: 'Failed to fetch stats' });
   903→  }
   904→});
   905→
   906→module.exports = router;
   907→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>