# Gamification 0.1% Plan: Sales Team Battleground

**Priority:** #7
**Status:** ✅ COMPLETE - Deployed Jan 17, 2026
**Created:** Jan 17, 2026

---

## The Vision

Turn the CRM into a **competitive arena** where 10 salespeople battle for glory. Every call matters. Every conversion celebrated. Records stand forever. This isn't corporate gamification - this is **WAR** (the fun kind).

---

## Current State (What We Have)

| Component | Location | Status |
|-----------|----------|--------|
| XP System (10 levels) | `src/routes/xp.js` | ✅ Working |
| 21 Achievements | `src/routes/achievements.js` | ✅ Working |
| Streaks | `user_xp.current_streak` | ✅ Basic |
| Celebration Overlays | `CelebrationOverlay.jsx` | ✅ Working |
| Leaderboard | `src/routes/xp.js` → `/leaderboard` | ✅ All-time only |
| MyStats Page | `admin/src/pages/MyStats.jsx` | ✅ Working |

**What's Missing:** Competition. Bragging rights. GLORY.

---

## Build Sequence for CC2

### Step 1: Database Migration

**File:** `supabase/gamification-battleground-migration.sql`

```sql
-- ================================================
-- GAMIFICATION BATTLEGROUND - DATABASE MIGRATION
-- ================================================

-- 1. HALL OF FAME - Records that stand forever
CREATE TABLE IF NOT EXISTS hall_of_fame (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  record_type VARCHAR(50) NOT NULL,  -- 'most_calls_day', 'biggest_deal', etc.
  user_id UUID NOT NULL,
  user_name VARCHAR(100) NOT NULL,
  value NUMERIC NOT NULL,            -- The record value
  details JSONB,                      -- Extra context (lead name, etc.)
  achieved_at TIMESTAMPTZ DEFAULT NOW(),
  previous_holder_id UUID,
  previous_value NUMERIC,
  UNIQUE(record_type)                -- Only one record per type
);

CREATE INDEX idx_hall_of_fame_type ON hall_of_fame(record_type);

-- 2. WEEKLY BADGES - Who owned the week
CREATE TABLE IF NOT EXISTS weekly_badges (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL,
  badge_type VARCHAR(50) NOT NULL,   -- 'closer', 'dialer', 'speed_demon', etc.
  week_start DATE NOT NULL,
  week_end DATE NOT NULL,
  value NUMERIC,                      -- Stat that earned it
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(badge_type, week_start)     -- One badge per type per week
);

CREATE INDEX idx_weekly_badges_user ON weekly_badges(user_id);
CREATE INDEX idx_weekly_badges_week ON weekly_badges(week_start DESC);

-- 3. DAILY CHALLENGES - Individual + Team
CREATE TABLE IF NOT EXISTS daily_challenges (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  date DATE NOT NULL DEFAULT CURRENT_DATE,
  challenge_type VARCHAR(20) NOT NULL,  -- 'individual' or 'team'
  title VARCHAR(100) NOT NULL,
  description TEXT,
  target_value INTEGER NOT NULL,
  current_value INTEGER DEFAULT 0,
  bonus_xp INTEGER DEFAULT 50,
  completed BOOLEAN DEFAULT FALSE,
  completed_at TIMESTAMPTZ,
  user_id UUID,                         -- NULL for team challenges
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(date, challenge_type, user_id)
);

CREATE INDEX idx_daily_challenges_date ON daily_challenges(date DESC);

-- 4. TEAM PULSE FEED - Live activity stream
CREATE TABLE IF NOT EXISTS team_pulse (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  event_type VARCHAR(50) NOT NULL,    -- 'conversion', 'record_broken', 'overtake', etc.
  user_id UUID NOT NULL,
  user_name VARCHAR(100) NOT NULL,
  title VARCHAR(200) NOT NULL,
  subtitle TEXT,
  emoji VARCHAR(10),
  link_type VARCHAR(50),              -- 'lead', 'conversation', 'leaderboard'
  link_id UUID,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_team_pulse_created ON team_pulse(created_at DESC);

-- 5. LEADERBOARD SNAPSHOTS - For position tracking
CREATE TABLE IF NOT EXISTS leaderboard_snapshots (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  user_id UUID NOT NULL,
  period VARCHAR(20) NOT NULL,        -- 'today', 'week', 'month', 'all'
  snapshot_date DATE NOT NULL,
  position INTEGER NOT NULL,
  calls INTEGER DEFAULT 0,
  conversions INTEGER DEFAULT 0,
  xp INTEGER DEFAULT 0,
  streak INTEGER DEFAULT 0,
  created_at TIMESTAMPTZ DEFAULT NOW(),
  UNIQUE(user_id, period, snapshot_date)
);

CREATE INDEX idx_lb_snapshots_date ON leaderboard_snapshots(snapshot_date DESC, period);

-- 6. STREAK MILESTONES - Track streak achievements
ALTER TABLE user_xp ADD COLUMN IF NOT EXISTS streak_tier VARCHAR(20) DEFAULT 'none';
-- Tiers: 'none', 'bronze' (3), 'silver' (7), 'gold' (14), 'diamond' (30)

-- 7. USER PROFILE EXTENSIONS
ALTER TABLE user_xp ADD COLUMN IF NOT EXISTS display_name VARCHAR(100);
ALTER TABLE user_xp ADD COLUMN IF NOT EXISTS avatar_url TEXT;
ALTER TABLE user_xp ADD COLUMN IF NOT EXISTS current_badges JSONB DEFAULT '[]';
ALTER TABLE user_xp ADD COLUMN IF NOT EXISTS status VARCHAR(50) DEFAULT 'grinding';
-- Statuses: 'grinding', 'on_fire', 'crushing_it', 'in_the_zone'

-- Enable realtime on team_pulse for live updates
ALTER PUBLICATION supabase_realtime ADD TABLE team_pulse;
```

---

### Step 2: Backend - Leaderboard Service

**File:** `src/services/leaderboardService.js` (CREATE NEW)

```javascript
/**
 * Leaderboard Service - Real-time competitive rankings
 */
const { supabase } = require('../config/database');

// Record types for Hall of Fame
const RECORD_TYPES = {
  MOST_CALLS_DAY: 'most_calls_day',
  BIGGEST_DEAL: 'biggest_deal',
  LONGEST_STREAK: 'longest_streak',
  FASTEST_CLOSE: 'fastest_close',       // Minutes from first contact to conversion
  MOST_WINS_WEEK: 'most_wins_week',
};

// Weekly badge types
const WEEKLY_BADGES = {
  CLOSER: { id: 'closer', emoji: '🦁', name: 'Closer', stat: 'conversions' },
  DIALER: { id: 'dialer', emoji: '📞', name: 'Dialer', stat: 'calls' },
  SPEED_DEMON: { id: 'speed_demon', emoji: '⚡', name: 'Speed Demon', stat: 'avg_response_time' },
  SNIPER: { id: 'sniper', emoji: '🎯', name: 'Sniper', stat: 'conversion_rate' },
  CONSISTENT: { id: 'consistent', emoji: '🛡️', name: 'Consistent', stat: 'days_active' },
  EARLY_BIRD: { id: 'early_bird', emoji: '🌅', name: 'Early Bird', stat: 'calls_before_9am' },
  NIGHT_OWL: { id: 'night_owl', emoji: '🦉', name: 'Night Owl', stat: 'calls_after_5pm' },
};

// Streak tiers
const STREAK_TIERS = {
  3: { tier: 'bronze', emoji: '🔥', name: 'Bronze Flame' },
  7: { tier: 'silver', emoji: '🔥🔥', name: 'Silver Blaze' },
  14: { tier: 'gold', emoji: '🔥🔥🔥', name: 'Gold Inferno' },
  30: { tier: 'diamond', emoji: '💎🔥', name: 'Diamond Legend' },
};

/**
 * Get leaderboard for a specific period
 */
async function getLeaderboard(period = 'today', limit = 10) {
  const now = new Date();
  let startDate;

  switch (period) {
    case 'today':
      startDate = new Date(now.setHours(0, 0, 0, 0)).toISOString();
      break;
    case 'week':
      const dayOfWeek = now.getDay();
      const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
      startDate = new Date(now.setDate(diff)).toISOString().split('T')[0];
      break;
    case 'month':
      startDate = new Date(now.getFullYear(), now.getMonth(), 1).toISOString();
      break;
    default:
      startDate = null; // all time
  }

  // Get XP earned in period from xp_history
  let query = supabase
    .from('xp_history')
    .select('user_id, amount');

  if (startDate) {
    query = query.gte('created_at', startDate);
  }

  const { data: xpData } = await query;

  // Aggregate by user
  const userXP = {};
  (xpData || []).forEach(row => {
    userXP[row.user_id] = (userXP[row.user_id] || 0) + row.amount;
  });

  // Get user profiles
  const { data: users } = await supabase
    .from('user_xp')
    .select('user_id, display_name, level, current_streak, current_badges, status, total_xp');

  // Get previous positions for comparison
  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);
  const { data: prevSnapshot } = await supabase
    .from('leaderboard_snapshots')
    .select('user_id, position')
    .eq('period', period)
    .eq('snapshot_date', yesterday.toISOString().split('T')[0]);

  const prevPositions = {};
  (prevSnapshot || []).forEach(row => {
    prevPositions[row.user_id] = row.position;
  });

  // Get call/conversion counts for period
  const { data: callStats } = await supabase
    .from('call_log')
    .select('user_id, outcome')
    .gte('created_at', startDate || '1970-01-01');

  const userCalls = {};
  const userConversions = {};
  (callStats || []).forEach(row => {
    userCalls[row.user_id] = (userCalls[row.user_id] || 0) + 1;
    if (row.outcome === 'converted' || row.outcome === 'won') {
      userConversions[row.user_id] = (userConversions[row.user_id] || 0) + 1;
    }
  });

  // Build leaderboard
  const leaderboard = (users || [])
    .map(user => ({
      userId: user.user_id,
      name: user.display_name || 'Anonymous',
      xp: period === 'all' ? user.total_xp : (userXP[user.user_id] || 0),
      level: user.level,
      streak: user.current_streak,
      badges: user.current_badges || [],
      status: user.status || 'grinding',
      calls: userCalls[user.user_id] || 0,
      conversions: userConversions[user.user_id] || 0,
    }))
    .sort((a, b) => b.xp - a.xp)
    .slice(0, limit)
    .map((user, index) => {
      const prevPos = prevPositions[user.userId];
      let positionChange = 0;
      if (prevPos !== undefined) {
        positionChange = prevPos - (index + 1); // Positive = moved up
      }
      return {
        ...user,
        position: index + 1,
        positionChange,
        isLeader: index === 0,
      };
    });

  return { leaderboard, period };
}

/**
 * Check for overtake and create alert
 */
async function checkOvertake(userId, userName, newPosition, period) {
  const { data: prev } = await supabase
    .from('leaderboard_snapshots')
    .select('position')
    .eq('user_id', userId)
    .eq('period', period)
    .order('snapshot_date', { ascending: false })
    .limit(1)
    .single();

  if (prev && newPosition < prev.position) {
    // User moved up - find who they overtook
    const { data: leaderboard } = await getLeaderboard(period, 10);
    const overtaken = leaderboard.find(u => u.position === newPosition + 1);

    if (overtaken) {
      await createPulseEvent({
        event_type: 'overtake',
        user_id: userId,
        user_name: userName,
        title: `${userName} just overtook ${overtaken.name}!`,
        subtitle: `Now #${newPosition} on the ${period} leaderboard`,
        emoji: '🚀',
        link_type: 'leaderboard',
      });

      return { overtook: overtaken.name, newPosition };
    }
  }
  return null;
}

/**
 * Check and update Hall of Fame record
 */
async function checkRecord(recordType, userId, userName, value, details = {}) {
  const { data: current } = await supabase
    .from('hall_of_fame')
    .select('*')
    .eq('record_type', recordType)
    .single();

  if (!current || value > current.value) {
    // NEW RECORD!
    await supabase
      .from('hall_of_fame')
      .upsert({
        record_type: recordType,
        user_id: userId,
        user_name: userName,
        value,
        details,
        previous_holder_id: current?.user_id,
        previous_value: current?.value,
        achieved_at: new Date().toISOString(),
      });

    // Create pulse event
    await createPulseEvent({
      event_type: 'record_broken',
      user_id: userId,
      user_name: userName,
      title: `🏆 NEW RECORD! ${userName} broke the ${formatRecordName(recordType)} record!`,
      subtitle: current ? `Previous: ${current.user_name} (${current.value})` : 'First ever!',
      emoji: '🏆',
    });

    // Notify Slack
    await notifySlackRecord(recordType, userName, value, current);

    return { newRecord: true, previousHolder: current?.user_name };
  }

  return { newRecord: false };
}

/**
 * Get Hall of Fame
 */
async function getHallOfFame() {
  const { data } = await supabase
    .from('hall_of_fame')
    .select('*')
    .order('achieved_at', { ascending: false });

  return data || [];
}

/**
 * Create team pulse event
 */
async function createPulseEvent({ event_type, user_id, user_name, title, subtitle, emoji, link_type, link_id }) {
  await supabase
    .from('team_pulse')
    .insert({
      event_type,
      user_id,
      user_name,
      title,
      subtitle,
      emoji,
      link_type,
      link_id,
    });
}

/**
 * Get team pulse feed
 */
async function getTeamPulse(limit = 20) {
  const { data } = await supabase
    .from('team_pulse')
    .select('*')
    .order('created_at', { ascending: false })
    .limit(limit);

  return data || [];
}

/**
 * Update streak tier
 */
async function updateStreakTier(userId, streakDays) {
  let tier = 'none';
  for (const [threshold, tierData] of Object.entries(STREAK_TIERS).reverse()) {
    if (streakDays >= parseInt(threshold)) {
      tier = tierData.tier;
      break;
    }
  }

  await supabase
    .from('user_xp')
    .update({ streak_tier: tier })
    .eq('user_id', userId);

  return tier;
}

/**
 * Notify Slack of record
 */
async function notifySlackRecord(recordType, userName, value, previous) {
  const slackService = require('./slack');
  const recordName = formatRecordName(recordType);

  let message = `🏆 *HALL OF FAME RECORD BROKEN!*\n\n`;
  message += `*${userName}* just set a new record for *${recordName}*!\n`;
  message += `📊 New record: *${value}*\n`;
  if (previous) {
    message += `Previous holder: ${previous.user_name} (${previous.value})`;
  }

  await slackService.sendNotification(message, '#sales-wins');
}

function formatRecordName(recordType) {
  const names = {
    most_calls_day: 'Most Calls in a Day',
    biggest_deal: 'Biggest Deal',
    longest_streak: 'Longest Streak',
    fastest_close: 'Fastest Close',
    most_wins_week: 'Most Wins in a Week',
  };
  return names[recordType] || recordType;
}

module.exports = {
  RECORD_TYPES,
  WEEKLY_BADGES,
  STREAK_TIERS,
  getLeaderboard,
  checkOvertake,
  checkRecord,
  getHallOfFame,
  createPulseEvent,
  getTeamPulse,
  updateStreakTier,
};
```

---

### Step 3: Backend - Challenges Service

**File:** `src/services/challengesService.js` (CREATE NEW)

```javascript
/**
 * Challenges Service - Daily individual + team challenges
 */
const { supabase } = require('../config/database');
const { createPulseEvent } = require('./leaderboardService');

// Challenge templates
const INDIVIDUAL_CHALLENGES = [
  { title: '5 calls before lunch', target: 5, type: 'calls_before_noon', xp: 30 },
  { title: 'Beat your daily average', target: 0, type: 'beat_average', xp: 40 },
  { title: 'Get 2 callbacks scheduled', target: 2, type: 'callbacks', xp: 35 },
  { title: 'First call before 9am', target: 1, type: 'early_call', xp: 25 },
  { title: 'Close a deal today', target: 1, type: 'conversion', xp: 50 },
  { title: 'Send 10 follow-up SMS', target: 10, type: 'sms_sent', xp: 25 },
  { title: '3 conversations (not voicemail)', target: 3, type: 'conversations', xp: 35 },
  { title: 'Use AI Copilot on 2 calls', target: 2, type: 'copilot_used', xp: 30 },
];

const TEAM_CHALLENGES = [
  { title: 'Team 50 calls', target: 50, xp: 100, description: 'Everyone gets bonus when team hits 50 calls' },
  { title: 'Team 5 wins', target: 5, xp: 150, description: '5 conversions as a team' },
  { title: 'Full squad active', target: 10, xp: 75, description: 'All 10 team members make at least 1 call' },
  { title: 'Zero missed callbacks', target: 0, xp: 100, description: 'No overdue callbacks by EOD' },
];

/**
 * Generate today's challenges
 */
async function generateDailyChallenges(userId) {
  const today = new Date().toISOString().split('T')[0];

  // Check if already generated
  const { data: existing } = await supabase
    .from('daily_challenges')
    .select('*')
    .eq('date', today)
    .eq('user_id', userId);

  if (existing?.length > 0) {
    return existing;
  }

  // Pick 2 random individual challenges
  const shuffled = [...INDIVIDUAL_CHALLENGES].sort(() => Math.random() - 0.5);
  const selected = shuffled.slice(0, 2);

  // Get user's average for "beat average" challenge
  const { data: history } = await supabase
    .from('call_log')
    .select('id')
    .eq('user_id', userId)
    .gte('created_at', new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString());

  const avgCalls = Math.ceil((history?.length || 0) / 7);

  // Create challenges
  const challenges = selected.map(c => ({
    date: today,
    challenge_type: 'individual',
    title: c.title,
    description: c.description || `Complete to earn ${c.xp} XP`,
    target_value: c.type === 'beat_average' ? avgCalls + 1 : c.target,
    bonus_xp: c.xp,
    user_id: userId,
  }));

  const { data: created } = await supabase
    .from('daily_challenges')
    .insert(challenges)
    .select();

  return created;
}

/**
 * Generate team challenge for today
 */
async function generateTeamChallenge() {
  const today = new Date().toISOString().split('T')[0];

  const { data: existing } = await supabase
    .from('daily_challenges')
    .select('*')
    .eq('date', today)
    .eq('challenge_type', 'team')
    .is('user_id', null);

  if (existing?.length > 0) {
    return existing[0];
  }

  // Pick random team challenge
  const challenge = TEAM_CHALLENGES[Math.floor(Math.random() * TEAM_CHALLENGES.length)];

  const { data: created } = await supabase
    .from('daily_challenges')
    .insert({
      date: today,
      challenge_type: 'team',
      title: challenge.title,
      description: challenge.description,
      target_value: challenge.target,
      bonus_xp: challenge.xp,
      user_id: null,
    })
    .select()
    .single();

  return created;
}

/**
 * Update challenge progress
 */
async function updateChallengeProgress(userId, actionType, count = 1) {
  const today = new Date().toISOString().split('T')[0];

  // Get user's challenges
  const { data: challenges } = await supabase
    .from('daily_challenges')
    .select('*')
    .eq('date', today)
    .or(`user_id.eq.${userId},user_id.is.null`)
    .eq('completed', false);

  const completed = [];

  for (const challenge of challenges || []) {
    let shouldUpdate = false;
    let newValue = challenge.current_value;

    // Map action types to challenge types
    if (actionType === 'call' && challenge.title.includes('call')) {
      shouldUpdate = true;
      newValue += count;
    }
    if (actionType === 'conversion' && challenge.title.includes('win')) {
      shouldUpdate = true;
      newValue += count;
    }
    // Add more mappings...

    if (shouldUpdate) {
      const isComplete = newValue >= challenge.target_value;

      await supabase
        .from('daily_challenges')
        .update({
          current_value: newValue,
          completed: isComplete,
          completed_at: isComplete ? new Date().toISOString() : null,
        })
        .eq('id', challenge.id);

      if (isComplete) {
        completed.push(challenge);

        // Award XP
        const xpService = require('./xpService');
        await xpService.awardXP(userId, 'challenge_complete', {
          customAmount: challenge.bonus_xp,
          reason: `Challenge: ${challenge.title}`,
        });

        // Pulse event
        const { data: user } = await supabase
          .from('user_xp')
          .select('display_name')
          .eq('user_id', userId)
          .single();

        await createPulseEvent({
          event_type: 'challenge_complete',
          user_id: userId,
          user_name: user?.display_name || 'Someone',
          title: `${user?.display_name} completed: ${challenge.title}`,
          subtitle: `+${challenge.bonus_xp} XP`,
          emoji: '✅',
        });
      }
    }
  }

  return { updated: true, completed };
}

/**
 * Get today's challenges for a user
 */
async function getTodayChallenges(userId) {
  const today = new Date().toISOString().split('T')[0];

  // Ensure challenges exist
  await generateDailyChallenges(userId);
  await generateTeamChallenge();

  const { data } = await supabase
    .from('daily_challenges')
    .select('*')
    .eq('date', today)
    .or(`user_id.eq.${userId},user_id.is.null`);

  const individual = (data || []).filter(c => c.challenge_type === 'individual');
  const team = (data || []).find(c => c.challenge_type === 'team');

  return { individual, team };
}

module.exports = {
  generateDailyChallenges,
  generateTeamChallenge,
  updateChallengeProgress,
  getTodayChallenges,
};
```

---

### Step 4: Backend - Badges Service

**File:** `src/services/badgesService.js` (CREATE NEW)

```javascript
/**
 * Badges Service - Weekly badge awards
 */
const { supabase } = require('../config/database');
const { WEEKLY_BADGES, createPulseEvent } = require('./leaderboardService');

/**
 * Calculate and award weekly badges (run every Friday 3pm)
 */
async function awardWeeklyBadges() {
  const now = new Date();
  const weekStart = getWeekStart(now);
  const weekEnd = new Date(weekStart);
  weekEnd.setDate(weekEnd.getDate() + 6);

  // Get all user stats for the week
  const { data: users } = await supabase
    .from('user_xp')
    .select('user_id, display_name');

  const stats = await Promise.all(
    (users || []).map(async (user) => {
      const userId = user.user_id;

      // Get call stats
      const { data: calls } = await supabase
        .from('call_log')
        .select('id, outcome, created_at')
        .eq('user_id', userId)
        .gte('created_at', weekStart.toISOString())
        .lte('created_at', weekEnd.toISOString());

      const totalCalls = calls?.length || 0;
      const conversions = calls?.filter(c => c.outcome === 'converted')?.length || 0;
      const conversionRate = totalCalls > 0 ? (conversions / totalCalls) * 100 : 0;

      // Count early/late calls
      const earlyBirdCalls = calls?.filter(c => {
        const hour = new Date(c.created_at).getHours();
        return hour < 9;
      })?.length || 0;

      const nightOwlCalls = calls?.filter(c => {
        const hour = new Date(c.created_at).getHours();
        return hour >= 17;
      })?.length || 0;

      // Days active
      const activeDays = new Set(
        calls?.map(c => new Date(c.created_at).toDateString())
      ).size;

      return {
        userId,
        name: user.display_name || 'Anonymous',
        calls: totalCalls,
        conversions,
        conversionRate,
        earlyBirdCalls,
        nightOwlCalls,
        daysActive: activeDays,
      };
    })
  );

  // Award badges
  const badges = [];

  // Closer - Most conversions
  const closer = stats.sort((a, b) => b.conversions - a.conversions)[0];
  if (closer?.conversions > 0) {
    badges.push({ ...WEEKLY_BADGES.CLOSER, winner: closer });
  }

  // Dialer - Most calls
  const dialer = stats.sort((a, b) => b.calls - a.calls)[0];
  if (dialer?.calls > 0) {
    badges.push({ ...WEEKLY_BADGES.DIALER, winner: dialer });
  }

  // Sniper - Highest conversion rate (min 5 calls)
  const sniper = stats
    .filter(s => s.calls >= 5)
    .sort((a, b) => b.conversionRate - a.conversionRate)[0];
  if (sniper) {
    badges.push({ ...WEEKLY_BADGES.SNIPER, winner: sniper });
  }

  // Consistent - Didn't miss a day (5+ days)
  const consistent = stats.filter(s => s.daysActive >= 5)[0];
  if (consistent) {
    badges.push({ ...WEEKLY_BADGES.CONSISTENT, winner: consistent });
  }

  // Early Bird
  const earlyBird = stats.sort((a, b) => b.earlyBirdCalls - a.earlyBirdCalls)[0];
  if (earlyBird?.earlyBirdCalls > 0) {
    badges.push({ ...WEEKLY_BADGES.EARLY_BIRD, winner: earlyBird });
  }

  // Night Owl
  const nightOwl = stats.sort((a, b) => b.nightOwlCalls - a.nightOwlCalls)[0];
  if (nightOwl?.nightOwlCalls > 0) {
    badges.push({ ...WEEKLY_BADGES.NIGHT_OWL, winner: nightOwl });
  }

  // Save badges to database
  for (const badge of badges) {
    await supabase
      .from('weekly_badges')
      .upsert({
        user_id: badge.winner.userId,
        badge_type: badge.id,
        week_start: weekStart.toISOString().split('T')[0],
        week_end: weekEnd.toISOString().split('T')[0],
        value: badge.winner[badge.stat] || 0,
      });

    // Update user's current badges
    const { data: userData } = await supabase
      .from('user_xp')
      .select('current_badges')
      .eq('user_id', badge.winner.userId)
      .single();

    const currentBadges = userData?.current_badges || [];
    if (!currentBadges.includes(badge.id)) {
      currentBadges.push(badge.id);
      await supabase
        .from('user_xp')
        .update({ current_badges: currentBadges })
        .eq('user_id', badge.winner.userId);
    }

    // Pulse event
    await createPulseEvent({
      event_type: 'badge_awarded',
      user_id: badge.winner.userId,
      user_name: badge.winner.name,
      title: `${badge.emoji} ${badge.winner.name} earned "${badge.name}" badge!`,
      subtitle: `This week's ${badge.name}`,
      emoji: badge.emoji,
    });
  }

  // Announce Player of the Week (highest XP)
  const { data: weeklyXP } = await supabase
    .from('xp_history')
    .select('user_id, amount')
    .gte('created_at', weekStart.toISOString());

  const xpByUser = {};
  (weeklyXP || []).forEach(row => {
    xpByUser[row.user_id] = (xpByUser[row.user_id] || 0) + row.amount;
  });

  const potw = Object.entries(xpByUser)
    .sort(([, a], [, b]) => b - a)[0];

  if (potw) {
    const { data: potwUser } = await supabase
      .from('user_xp')
      .select('display_name')
      .eq('user_id', potw[0])
      .single();

    await createPulseEvent({
      event_type: 'player_of_week',
      user_id: potw[0],
      user_name: potwUser?.display_name || 'Anonymous',
      title: `👑 PLAYER OF THE WEEK: ${potwUser?.display_name}!`,
      subtitle: `${potw[1]} XP earned this week`,
      emoji: '👑',
    });

    // Notify Slack
    const slackService = require('./slack');
    await slackService.sendNotification(
      `👑 *PLAYER OF THE WEEK*\n\n*${potwUser?.display_name}* crushed it with ${potw[1]} XP!`,
      '#sales-wins'
    );
  }

  return { badges, playerOfWeek: potw };
}

function getWeekStart(date) {
  const d = new Date(date);
  const day = d.getDay();
  const diff = d.getDate() - day + (day === 0 ? -6 : 1);
  return new Date(d.setDate(diff));
}

/**
 * Get user's badges
 */
async function getUserBadges(userId) {
  const { data } = await supabase
    .from('weekly_badges')
    .select('*')
    .eq('user_id', userId)
    .order('week_start', { ascending: false });

  return data || [];
}

module.exports = {
  awardWeeklyBadges,
  getUserBadges,
  getWeekStart,
};
```

---

### Step 5: Backend - API Routes

**File:** `src/routes/battleground.js` (CREATE NEW)

```javascript
const express = require('express');
const router = express.Router();
const leaderboardService = require('../services/leaderboardService');
const challengesService = require('../services/challengesService');
const badgesService = require('../services/badgesService');

// ============================================
// LEADERBOARD
// ============================================

// GET /api/battleground/leaderboard?period=today|week|month|all
router.get('/leaderboard', async (req, res) => {
  try {
    const { period = 'today' } = req.query;
    const result = await leaderboardService.getLeaderboard(period, 10);
    res.json({ success: true, ...result });
  } catch (error) {
    console.error('Leaderboard error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// GET /api/battleground/leaderboard/:userId - Get specific user stats
router.get('/leaderboard/:userId', async (req, res) => {
  try {
    const { userId } = req.params;
    const { supabase } = require('../config/database');

    const { data: user } = await supabase
      .from('user_xp')
      .select('*')
      .eq('user_id', userId)
      .single();

    const { data: calls } = await supabase
      .from('call_log')
      .select('id, outcome')
      .eq('user_id', userId);

    const conversions = calls?.filter(c => c.outcome === 'converted')?.length || 0;
    const badges = await badgesService.getUserBadges(userId);

    res.json({
      success: true,
      user: {
        ...user,
        totalCalls: calls?.length || 0,
        totalConversions: conversions,
        badges,
      },
    });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// ============================================
// HALL OF FAME
// ============================================

// GET /api/battleground/hall-of-fame
router.get('/hall-of-fame', async (req, res) => {
  try {
    const records = await leaderboardService.getHallOfFame();
    res.json({ success: true, records });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// ============================================
// CHALLENGES
// ============================================

// GET /api/battleground/challenges
router.get('/challenges', async (req, res) => {
  try {
    const userId = req.headers['x-user-id'] || 'default-user';
    const challenges = await challengesService.getTodayChallenges(userId);
    res.json({ success: true, ...challenges });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// POST /api/battleground/challenges/progress
router.post('/challenges/progress', async (req, res) => {
  try {
    const userId = req.headers['x-user-id'] || 'default-user';
    const { actionType, count = 1 } = req.body;
    const result = await challengesService.updateChallengeProgress(userId, actionType, count);
    res.json({ success: true, ...result });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// ============================================
// TEAM PULSE
// ============================================

// GET /api/battleground/pulse
router.get('/pulse', async (req, res) => {
  try {
    const { limit = 20 } = req.query;
    const events = await leaderboardService.getTeamPulse(parseInt(limit));
    res.json({ success: true, events });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// ============================================
// BADGES
// ============================================

// GET /api/battleground/badges/:userId
router.get('/badges/:userId', async (req, res) => {
  try {
    const badges = await badgesService.getUserBadges(req.params.userId);
    res.json({ success: true, badges });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

// POST /api/battleground/badges/award-weekly (called by cron)
router.post('/badges/award-weekly', async (req, res) => {
  try {
    const result = await badgesService.awardWeeklyBadges();
    res.json({ success: true, ...result });
  } catch (error) {
    res.status(500).json({ success: false, error: error.message });
  }
});

module.exports = router;
```

---

### Step 6: Register Routes in index.js

**File:** `src/index.js` (MODIFY)

Add these lines:

```javascript
// Add near other route imports
const battlegroundRoutes = require('./routes/battleground');

// Add near other route registrations
app.use('/api/battleground', battlegroundRoutes);
```

---

### Step 7: Frontend - API Client

**File:** `admin/src/api/client.js` (MODIFY)

Add to exports:

```javascript
// Battleground API
export const battlegroundApi = {
  getLeaderboard: (period = 'today') =>
    request(`/api/battleground/leaderboard?period=${period}`),
  getUserStats: (userId) =>
    request(`/api/battleground/leaderboard/${userId}`),
  getHallOfFame: () =>
    request('/api/battleground/hall-of-fame'),
  getChallenges: () =>
    request('/api/battleground/challenges'),
  updateChallengeProgress: (actionType, count = 1) =>
    request('/api/battleground/challenges/progress', {
      method: 'POST',
      body: JSON.stringify({ actionType, count }),
    }),
  getTeamPulse: (limit = 20) =>
    request(`/api/battleground/pulse?limit=${limit}`),
  getUserBadges: (userId) =>
    request(`/api/battleground/badges/${userId}`),
};
```

---

### Step 8: Frontend - Live Leaderboard Component

**File:** `admin/src/components/gamification/LiveLeaderboard.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { Crown, TrendingUp, TrendingDown, Minus, Flame, Phone, Trophy, ChevronRight } from 'lucide-react';
import { battlegroundApi } from '../../api/client';

const PERIOD_TABS = [
  { id: 'today', label: 'Today' },
  { id: 'week', label: 'This Week' },
  { id: 'month', label: 'Month' },
  { id: 'all', label: 'All Time' },
];

const BADGE_EMOJIS = {
  closer: '🦁',
  dialer: '📞',
  speed_demon: '⚡',
  sniper: '🎯',
  consistent: '🛡️',
  early_bird: '🌅',
  night_owl: '🦉',
};

export default function LiveLeaderboard({ onSelectUser }) {
  const [period, setPeriod] = useState('today');
  const [leaderboard, setLeaderboard] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadLeaderboard();
    const interval = setInterval(loadLeaderboard, 30000); // Refresh every 30s
    return () => clearInterval(interval);
  }, [period]);

  const loadLeaderboard = async () => {
    try {
      const data = await battlegroundApi.getLeaderboard(period);
      setLeaderboard(data.leaderboard || []);
    } catch (err) {
      console.error('Failed to load leaderboard:', err);
    } finally {
      setLoading(false);
    }
  };

  const getPositionIcon = (change) => {
    if (change > 0) return <TrendingUp className="w-4 h-4 text-green-500" />;
    if (change < 0) return <TrendingDown className="w-4 h-4 text-red-500" />;
    return <Minus className="w-4 h-4 text-slate-400" />;
  };

  const getPositionBg = (position) => {
    if (position === 1) return 'bg-gradient-to-r from-amber-100 to-yellow-50 border-amber-300';
    if (position === 2) return 'bg-gradient-to-r from-slate-100 to-slate-50 border-slate-300';
    if (position === 3) return 'bg-gradient-to-r from-orange-100 to-amber-50 border-orange-300';
    return 'bg-white border-slate-200';
  };

  return (
    <div className="bg-white rounded-2xl border border-slate-200 overflow-hidden">
      {/* Header */}
      <div className="bg-gradient-to-r from-purple-600 to-violet-600 px-4 py-3">
        <div className="flex items-center justify-between">
          <h3 className="font-bold text-white flex items-center gap-2">
            <Trophy className="w-5 h-5" />
            Leaderboard
          </h3>
          <span className="text-purple-200 text-xs">Live</span>
        </div>
      </div>

      {/* Period Tabs */}
      <div className="flex border-b border-slate-200">
        {PERIOD_TABS.map((tab) => (
          <button
            key={tab.id}
            onClick={() => setPeriod(tab.id)}
            className={`flex-1 py-2 text-sm font-medium transition-colors ${
              period === tab.id
                ? 'text-purple-600 border-b-2 border-purple-600'
                : 'text-slate-500 hover:text-slate-700'
            }`}
          >
            {tab.label}
          </button>
        ))}
      </div>

      {/* Leaderboard */}
      <div className="divide-y divide-slate-100">
        {loading ? (
          <div className="p-8 text-center text-slate-400">Loading...</div>
        ) : leaderboard.length === 0 ? (
          <div className="p-8 text-center text-slate-400">No activity yet</div>
        ) : (
          leaderboard.map((user, index) => (
            <button
              key={user.userId}
              onClick={() => onSelectUser?.(user.userId)}
              className={`w-full flex items-center gap-3 p-3 hover:bg-slate-50 transition-colors ${getPositionBg(user.position)}`}
            >
              {/* Position */}
              <div className="w-8 h-8 rounded-full flex items-center justify-center font-bold text-sm">
                {user.position === 1 ? (
                  <Crown className="w-6 h-6 text-amber-500" />
                ) : (
                  <span className={user.position <= 3 ? 'text-slate-700' : 'text-slate-400'}>
                    {user.position}
                  </span>
                )}
              </div>

              {/* Position Change */}
              <div className="w-6 flex items-center justify-center">
                {getPositionIcon(user.positionChange)}
              </div>

              {/* User Info */}
              <div className="flex-1 text-left">
                <div className="flex items-center gap-2">
                  <span className="font-semibold text-slate-900">{user.name}</span>
                  {/* Weekly Badges */}
                  {user.badges?.map((badge) => (
                    <span key={badge} className="text-sm" title={badge}>
                      {BADGE_EMOJIS[badge]}
                    </span>
                  ))}
                </div>
                <div className="flex items-center gap-3 text-xs text-slate-500">
                  <span className="flex items-center gap-1">
                    <Phone className="w-3 h-3" />
                    {user.calls}
                  </span>
                  <span className="flex items-center gap-1">
                    <Trophy className="w-3 h-3" />
                    {user.conversions}
                  </span>
                  {user.streak > 0 && (
                    <span className="flex items-center gap-1 text-orange-500">
                      <Flame className="w-3 h-3" />
                      {user.streak}
                    </span>
                  )}
                </div>
              </div>

              {/* XP */}
              <div className="text-right">
                <p className="font-bold text-purple-600">{user.xp.toLocaleString()}</p>
                <p className="text-xs text-slate-400">XP</p>
              </div>

              <ChevronRight className="w-4 h-4 text-slate-300" />
            </button>
          ))
        )}
      </div>
    </div>
  );
}
```

---

### Step 9: Frontend - Hall of Fame Component

**File:** `admin/src/components/gamification/HallOfFame.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { Trophy, Phone, DollarSign, Flame, Zap, Star } from 'lucide-react';
import { battlegroundApi } from '../../api/client';

const RECORD_CONFIG = {
  most_calls_day: { icon: Phone, color: 'blue', label: 'Most Calls (Day)' },
  biggest_deal: { icon: DollarSign, color: 'green', label: 'Biggest Deal' },
  longest_streak: { icon: Flame, color: 'orange', label: 'Longest Streak' },
  fastest_close: { icon: Zap, color: 'yellow', label: 'Fastest Close' },
  most_wins_week: { icon: Star, color: 'purple', label: 'Most Wins (Week)' },
};

export default function HallOfFame() {
  const [records, setRecords] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadRecords();
  }, []);

  const loadRecords = async () => {
    try {
      const data = await battlegroundApi.getHallOfFame();
      setRecords(data.records || []);
    } catch (err) {
      console.error('Failed to load hall of fame:', err);
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return <div className="p-4 text-center text-slate-400">Loading legends...</div>;
  }

  return (
    <div className="bg-gradient-to-br from-amber-50 to-yellow-50 rounded-2xl border-2 border-amber-200 overflow-hidden">
      {/* Header */}
      <div className="bg-gradient-to-r from-amber-500 to-yellow-500 px-4 py-3">
        <h3 className="font-bold text-white flex items-center gap-2">
          <Trophy className="w-5 h-5" />
          Hall of Fame
        </h3>
        <p className="text-amber-100 text-xs">Records that stand forever</p>
      </div>

      {/* Records */}
      <div className="p-4 space-y-3">
        {Object.entries(RECORD_CONFIG).map(([type, config]) => {
          const record = records.find((r) => r.record_type === type);
          const Icon = config.icon;

          return (
            <div
              key={type}
              className="flex items-center gap-3 bg-white rounded-xl p-3 border border-amber-100"
            >
              <div className={`w-10 h-10 rounded-lg bg-${config.color}-100 flex items-center justify-center`}>
                <Icon className={`w-5 h-5 text-${config.color}-600`} />
              </div>
              <div className="flex-1">
                <p className="text-xs text-slate-500">{config.label}</p>
                {record ? (
                  <>
                    <p className="font-bold text-slate-900">{record.user_name}</p>
                    <p className="text-lg font-black text-amber-600">{record.value}</p>
                  </>
                ) : (
                  <p className="text-slate-400 italic">Unclaimed - be the first!</p>
                )}
              </div>
              {record && <Trophy className="w-5 h-5 text-amber-400" />}
            </div>
          );
        })}
      </div>
    </div>
  );
}
```

---

### Step 10: Frontend - Daily Challenges Component

**File:** `admin/src/components/gamification/DailyChallenges.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { Target, Users, Zap, CheckCircle2 } from 'lucide-react';
import { battlegroundApi } from '../../api/client';

export default function DailyChallenges() {
  const [individual, setIndividual] = useState([]);
  const [team, setTeam] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadChallenges();
    const interval = setInterval(loadChallenges, 60000); // Refresh every minute
    return () => clearInterval(interval);
  }, []);

  const loadChallenges = async () => {
    try {
      const data = await battlegroundApi.getChallenges();
      setIndividual(data.individual || []);
      setTeam(data.team);
    } catch (err) {
      console.error('Failed to load challenges:', err);
    } finally {
      setLoading(false);
    }
  };

  const getProgress = (current, target) => {
    return Math.min(100, Math.round((current / target) * 100));
  };

  if (loading) {
    return <div className="p-4 text-center text-slate-400">Loading challenges...</div>;
  }

  return (
    <div className="space-y-4">
      {/* Individual Challenges */}
      <div className="bg-white rounded-2xl border border-slate-200 overflow-hidden">
        <div className="bg-gradient-to-r from-blue-600 to-cyan-600 px-4 py-3">
          <h3 className="font-bold text-white flex items-center gap-2">
            <Target className="w-5 h-5" />
            Today's Missions
          </h3>
        </div>
        <div className="p-4 space-y-3">
          {individual.map((challenge) => {
            const progress = getProgress(challenge.current_value, challenge.target_value);
            const isComplete = challenge.completed;

            return (
              <div
                key={challenge.id}
                className={`p-3 rounded-xl border ${
                  isComplete
                    ? 'bg-green-50 border-green-200'
                    : 'bg-slate-50 border-slate-200'
                }`}
              >
                <div className="flex items-center justify-between mb-2">
                  <span className={`font-medium ${isComplete ? 'text-green-700' : 'text-slate-900'}`}>
                    {challenge.title}
                  </span>
                  {isComplete ? (
                    <CheckCircle2 className="w-5 h-5 text-green-500" />
                  ) : (
                    <span className="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
                      +{challenge.bonus_xp} XP
                    </span>
                  )}
                </div>
                <div className="h-2 bg-slate-200 rounded-full overflow-hidden">
                  <div
                    className={`h-full transition-all duration-500 ${
                      isComplete ? 'bg-green-500' : 'bg-blue-500'
                    }`}
                    style={{ width: `${progress}%` }}
                  />
                </div>
                <p className="text-xs text-slate-500 mt-1">
                  {challenge.current_value} / {challenge.target_value}
                </p>
              </div>
            );
          })}
        </div>
      </div>

      {/* Team Challenge */}
      {team && (
        <div className="bg-gradient-to-br from-purple-50 to-violet-50 rounded-2xl border-2 border-purple-200 overflow-hidden">
          <div className="bg-gradient-to-r from-purple-600 to-violet-600 px-4 py-3">
            <h3 className="font-bold text-white flex items-center gap-2">
              <Users className="w-5 h-5" />
              Team Challenge
            </h3>
          </div>
          <div className="p-4">
            <div className="flex items-center justify-between mb-2">
              <span className="font-bold text-purple-900">{team.title}</span>
              <span className="text-sm bg-purple-100 text-purple-700 px-2 py-1 rounded-full flex items-center gap-1">
                <Zap className="w-3 h-3" />
                +{team.bonus_xp} XP each
              </span>
            </div>
            <p className="text-sm text-purple-600 mb-3">{team.description}</p>
            <div className="h-4 bg-purple-200 rounded-full overflow-hidden">
              <div
                className={`h-full transition-all duration-500 ${
                  team.completed ? 'bg-green-500' : 'bg-purple-500'
                }`}
                style={{ width: `${getProgress(team.current_value, team.target_value)}%` }}
              />
            </div>
            <p className="text-center text-sm font-bold text-purple-700 mt-2">
              {team.current_value} / {team.target_value}
            </p>
          </div>
        </div>
      )}
    </div>
  );
}
```

---

### Step 11: Frontend - Team Pulse Feed Component

**File:** `admin/src/components/gamification/TeamPulseFeed.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Activity, Trophy, TrendingUp, Star, Users, MessageCircle } from 'lucide-react';
import { battlegroundApi } from '../../api/client';
import { useRealtime } from '../../context/RealtimeContext';

const EVENT_ICONS = {
  conversion: Trophy,
  record_broken: Star,
  overtake: TrendingUp,
  badge_awarded: Star,
  challenge_complete: Activity,
  player_of_week: Trophy,
  default: Activity,
};

const EVENT_COLORS = {
  conversion: 'bg-green-100 text-green-600',
  record_broken: 'bg-amber-100 text-amber-600',
  overtake: 'bg-blue-100 text-blue-600',
  badge_awarded: 'bg-purple-100 text-purple-600',
  challenge_complete: 'bg-cyan-100 text-cyan-600',
  player_of_week: 'bg-amber-100 text-amber-600',
  default: 'bg-slate-100 text-slate-600',
};

export default function TeamPulseFeed({ limit = 10 }) {
  const navigate = useNavigate();
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);

  // Subscribe to realtime team_pulse inserts
  const { supabase } = useRealtime();

  useEffect(() => {
    loadEvents();

    // Realtime subscription
    const channel = supabase
      ?.channel('team-pulse')
      .on(
        'postgres_changes',
        { event: 'INSERT', schema: 'public', table: 'team_pulse' },
        (payload) => {
          setEvents((prev) => [payload.new, ...prev].slice(0, limit));
        }
      )
      .subscribe();

    return () => {
      channel?.unsubscribe();
    };
  }, [limit]);

  const loadEvents = async () => {
    try {
      const data = await battlegroundApi.getTeamPulse(limit);
      setEvents(data.events || []);
    } catch (err) {
      console.error('Failed to load pulse:', err);
    } finally {
      setLoading(false);
    }
  };

  const handleEventClick = (event) => {
    if (event.link_type === 'lead' && event.link_id) {
      navigate(`/leads/${event.link_id}`);
    } else if (event.link_type === 'leaderboard') {
      navigate('/battleground');
    }
  };

  const timeAgo = (date) => {
    const seconds = Math.floor((new Date() - new Date(date)) / 1000);
    if (seconds < 60) return 'just now';
    if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
    if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
    return `${Math.floor(seconds / 86400)}d ago`;
  };

  return (
    <div className="bg-white rounded-2xl border border-slate-200 overflow-hidden">
      {/* Header */}
      <div className="bg-gradient-to-r from-green-600 to-emerald-600 px-4 py-3">
        <h3 className="font-bold text-white flex items-center gap-2">
          <Activity className="w-5 h-5" />
          Team Pulse
        </h3>
        <p className="text-green-100 text-xs">Live activity feed</p>
      </div>

      {/* Events */}
      <div className="max-h-80 overflow-y-auto divide-y divide-slate-100">
        {loading ? (
          <div className="p-4 text-center text-slate-400">Loading...</div>
        ) : events.length === 0 ? (
          <div className="p-8 text-center">
            <Users className="w-12 h-12 text-slate-300 mx-auto mb-2" />
            <p className="text-slate-400">No activity yet today</p>
            <p className="text-xs text-slate-300">Be the first to make some noise!</p>
          </div>
        ) : (
          events.map((event) => {
            const Icon = EVENT_ICONS[event.event_type] || EVENT_ICONS.default;
            const colorClass = EVENT_COLORS[event.event_type] || EVENT_COLORS.default;

            return (
              <button
                key={event.id}
                onClick={() => handleEventClick(event)}
                className="w-full flex items-start gap-3 p-3 hover:bg-slate-50 transition-colors text-left"
              >
                <div className={`w-8 h-8 rounded-lg flex items-center justify-center ${colorClass}`}>
                  {event.emoji || <Icon className="w-4 h-4" />}
                </div>
                <div className="flex-1 min-w-0">
                  <p className="text-sm font-medium text-slate-900 truncate">
                    {event.title}
                  </p>
                  {event.subtitle && (
                    <p className="text-xs text-slate-500 truncate">{event.subtitle}</p>
                  )}
                </div>
                <span className="text-xs text-slate-400 whitespace-nowrap">
                  {timeAgo(event.created_at)}
                </span>
              </button>
            );
          })
        )}
      </div>
    </div>
  );
}
```

---

### Step 12: Frontend - Streak Wars Display

**File:** `admin/src/components/gamification/StreakWars.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { Flame, Shield, Crown } from 'lucide-react';
import { battlegroundApi } from '../../api/client';

const STREAK_TIERS = {
  none: { flames: 0, color: 'slate', label: 'No streak' },
  bronze: { flames: 1, color: 'orange', label: 'Bronze Flame' },
  silver: { flames: 2, color: 'slate', label: 'Silver Blaze' },
  gold: { flames: 3, color: 'amber', label: 'Gold Inferno' },
  diamond: { flames: 4, color: 'cyan', label: 'Diamond Legend' },
};

export default function StreakWars({ currentUserId }) {
  const [streakData, setStreakData] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadStreaks();
  }, []);

  const loadStreaks = async () => {
    try {
      const data = await battlegroundApi.getLeaderboard('all', 10);
      // Sort by streak
      const sorted = (data.leaderboard || [])
        .filter((u) => u.streak > 0)
        .sort((a, b) => b.streak - a.streak);
      setStreakData(sorted);
    } catch (err) {
      console.error('Failed to load streaks:', err);
    } finally {
      setLoading(false);
    }
  };

  const getTier = (streak) => {
    if (streak >= 30) return STREAK_TIERS.diamond;
    if (streak >= 14) return STREAK_TIERS.gold;
    if (streak >= 7) return STREAK_TIERS.silver;
    if (streak >= 3) return STREAK_TIERS.bronze;
    return STREAK_TIERS.none;
  };

  const renderFlames = (count) => {
    return Array.from({ length: count }, (_, i) => (
      <Flame key={i} className="w-5 h-5 text-orange-500 animate-pulse" style={{ animationDelay: `${i * 0.1}s` }} />
    ));
  };

  if (loading) {
    return <div className="p-4 text-center text-slate-400">Loading streaks...</div>;
  }

  return (
    <div className="bg-gradient-to-br from-orange-50 to-red-50 rounded-2xl border-2 border-orange-200 overflow-hidden">
      {/* Header */}
      <div className="bg-gradient-to-r from-orange-500 to-red-500 px-4 py-3">
        <h3 className="font-bold text-white flex items-center gap-2">
          <Flame className="w-5 h-5" />
          Streak Wars
        </h3>
        <p className="text-orange-100 text-xs">Who's got the longest flame?</p>
      </div>

      {/* Streak List */}
      <div className="p-4 space-y-2">
        {streakData.length === 0 ? (
          <p className="text-center text-slate-400 py-4">No active streaks</p>
        ) : (
          streakData.slice(0, 5).map((user, index) => {
            const tier = getTier(user.streak);
            const isCurrentUser = user.userId === currentUserId;

            return (
              <div
                key={user.userId}
                className={`flex items-center gap-3 p-3 rounded-xl ${
                  isCurrentUser
                    ? 'bg-orange-100 border-2 border-orange-300'
                    : 'bg-white border border-orange-100'
                }`}
              >
                {/* Rank */}
                <div className="w-6 text-center font-bold text-slate-400">
                  {index === 0 ? <Crown className="w-5 h-5 text-amber-500" /> : index + 1}
                </div>

                {/* Flames */}
                <div className="flex items-center">{renderFlames(tier.flames || 1)}</div>

                {/* Name */}
                <div className="flex-1">
                  <span className={`font-semibold ${isCurrentUser ? 'text-orange-700' : 'text-slate-900'}`}>
                    {user.name}
                    {isCurrentUser && ' (You)'}
                  </span>
                  <p className="text-xs text-slate-500">{tier.label}</p>
                </div>

                {/* Streak Count */}
                <div className="text-right">
                  <p className="text-2xl font-black text-orange-600">{user.streak}</p>
                  <p className="text-xs text-slate-400">days</p>
                </div>
              </div>
            );
          })
        )}
      </div>

      {/* Milestones Legend */}
      <div className="px-4 pb-4">
        <p className="text-xs text-slate-500 mb-2">Milestones:</p>
        <div className="flex flex-wrap gap-2">
          <span className="text-xs bg-orange-100 text-orange-700 px-2 py-1 rounded-full">🔥 3d Bronze</span>
          <span className="text-xs bg-slate-100 text-slate-700 px-2 py-1 rounded-full">🔥🔥 7d Silver</span>
          <span className="text-xs bg-amber-100 text-amber-700 px-2 py-1 rounded-full">🔥🔥🔥 14d Gold</span>
          <span className="text-xs bg-cyan-100 text-cyan-700 px-2 py-1 rounded-full">💎🔥 30d Diamond</span>
        </div>
      </div>
    </div>
  );
}
```

---

### Step 13: Frontend - Battleground Page

**File:** `admin/src/pages/Battleground.jsx` (CREATE NEW)

```jsx
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { ArrowLeft, Trophy, Swords, Users } from 'lucide-react';

import LiveLeaderboard from '../components/gamification/LiveLeaderboard';
import HallOfFame from '../components/gamification/HallOfFame';
import DailyChallenges from '../components/gamification/DailyChallenges';
import TeamPulseFeed from '../components/gamification/TeamPulseFeed';
import StreakWars from '../components/gamification/StreakWars';
import UserStatsModal from '../components/gamification/UserStatsModal';

export default function Battleground() {
  const navigate = useNavigate();
  const [selectedUserId, setSelectedUserId] = useState(null);

  return (
    <div className="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 pb-24">
      {/* Epic Header */}
      <div className="relative overflow-hidden">
        <div className="absolute inset-0 bg-[url('/grid.svg')] opacity-20" />
        <div className="relative px-4 py-6">
          <button
            onClick={() => navigate(-1)}
            className="p-2 -ml-2 mb-4 text-white/60 hover:text-white transition-colors"
          >
            <ArrowLeft className="w-5 h-5" />
          </button>
          <div className="flex items-center gap-3">
            <div className="w-14 h-14 bg-gradient-to-br from-amber-400 to-orange-500 rounded-2xl flex items-center justify-center shadow-lg shadow-orange-500/30">
              <Swords className="w-8 h-8 text-white" />
            </div>
            <div>
              <h1 className="text-2xl font-black text-white">Battleground</h1>
              <p className="text-purple-300">Compete. Dominate. Win.</p>
            </div>
          </div>
        </div>
      </div>

      {/* Content */}
      <div className="px-4 space-y-6">
        {/* Challenges */}
        <DailyChallenges />

        {/* Leaderboard */}
        <LiveLeaderboard onSelectUser={setSelectedUserId} />

        {/* Streak Wars */}
        <StreakWars />

        {/* Hall of Fame */}
        <HallOfFame />

        {/* Team Pulse */}
        <TeamPulseFeed limit={15} />
      </div>

      {/* User Stats Modal */}
      {selectedUserId && (
        <UserStatsModal
          userId={selectedUserId}
          onClose={() => setSelectedUserId(null)}
        />
      )}
    </div>
  );
}
```

---

### Step 14: Frontend - User Stats Modal

**File:** `admin/src/components/gamification/UserStatsModal.jsx` (CREATE NEW)

```jsx
import { useState, useEffect } from 'react';
import { X, Phone, Trophy, Flame, Star, Zap } from 'lucide-react';
import { battlegroundApi } from '../../api/client';

const BADGE_INFO = {
  closer: { emoji: '🦁', name: 'Closer', desc: 'Most conversions' },
  dialer: { emoji: '📞', name: 'Dialer', desc: 'Most calls' },
  speed_demon: { emoji: '⚡', name: 'Speed Demon', desc: 'Fastest response' },
  sniper: { emoji: '🎯', name: 'Sniper', desc: 'Best conversion rate' },
  consistent: { emoji: '🛡️', name: 'Consistent', desc: 'Active every day' },
  early_bird: { emoji: '🌅', name: 'Early Bird', desc: 'Morning warrior' },
  night_owl: { emoji: '🦉', name: 'Night Owl', desc: 'Evening closer' },
};

export default function UserStatsModal({ userId, onClose }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    loadUser();
  }, [userId]);

  const loadUser = async () => {
    try {
      const data = await battlegroundApi.getUserStats(userId);
      setUser(data.user);
    } catch (err) {
      console.error('Failed to load user stats:', err);
    } finally {
      setLoading(false);
    }
  };

  if (loading) {
    return (
      <div className="fixed inset-0 z-50 bg-black/50 flex items-center justify-center">
        <div className="bg-white rounded-2xl p-8">
          <p className="text-slate-500">Loading...</p>
        </div>
      </div>
    );
  }

  if (!user) return null;

  return (
    <div className="fixed inset-0 z-50 bg-black/50 flex items-end sm:items-center justify-center">
      <div className="w-full max-w-md bg-white rounded-t-3xl sm:rounded-2xl overflow-hidden">
        {/* Header */}
        <div className="bg-gradient-to-r from-purple-600 to-violet-600 px-4 py-4 relative">
          <button
            onClick={onClose}
            className="absolute top-4 right-4 p-2 text-white/60 hover:text-white"
          >
            <X className="w-5 h-5" />
          </button>
          <h2 className="text-xl font-bold text-white">{user.display_name || 'Anonymous'}</h2>
          <p className="text-purple-200 text-sm">Level {user.level}</p>
        </div>

        {/* Stats Grid */}
        <div className="p-4 grid grid-cols-2 gap-3">
          <div className="bg-slate-50 rounded-xl p-3 text-center">
            <Phone className="w-6 h-6 text-blue-500 mx-auto mb-1" />
            <p className="text-2xl font-bold text-slate-900">{user.totalCalls}</p>
            <p className="text-xs text-slate-500">Total Calls</p>
          </div>
          <div className="bg-slate-50 rounded-xl p-3 text-center">
            <Trophy className="w-6 h-6 text-green-500 mx-auto mb-1" />
            <p className="text-2xl font-bold text-slate-900">{user.totalConversions}</p>
            <p className="text-xs text-slate-500">Conversions</p>
          </div>
          <div className="bg-slate-50 rounded-xl p-3 text-center">
            <Flame className="w-6 h-6 text-orange-500 mx-auto mb-1" />
            <p className="text-2xl font-bold text-slate-900">{user.current_streak}</p>
            <p className="text-xs text-slate-500">Day Streak</p>
          </div>
          <div className="bg-slate-50 rounded-xl p-3 text-center">
            <Zap className="w-6 h-6 text-yellow-500 mx-auto mb-1" />
            <p className="text-2xl font-bold text-slate-900">{user.total_xp?.toLocaleString()}</p>
            <p className="text-xs text-slate-500">Total XP</p>
          </div>
        </div>

        {/* Badges */}
        {user.badges?.length > 0 && (
          <div className="px-4 pb-4">
            <h3 className="font-semibold text-slate-900 mb-2">Weekly Badges</h3>
            <div className="flex flex-wrap gap-2">
              {user.badges.map((badge) => {
                const info = BADGE_INFO[badge.badge_type] || {};
                return (
                  <div
                    key={badge.id}
                    className="flex items-center gap-2 bg-purple-50 rounded-full px-3 py-1"
                  >
                    <span className="text-lg">{info.emoji}</span>
                    <span className="text-sm font-medium text-purple-700">{info.name}</span>
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* Close Button */}
        <div className="p-4 pt-0">
          <button
            onClick={onClose}
            className="w-full py-3 bg-slate-100 text-slate-700 font-semibold rounded-xl hover:bg-slate-200 transition-colors"
          >
            Close
          </button>
        </div>
      </div>
    </div>
  );
}
```

---

### Step 15: Add Route to App.jsx

**File:** `admin/src/App.jsx` (MODIFY)

Add import and route:

```jsx
// Add import
import Battleground from './pages/Battleground';

// Add route inside Routes (near other routes)
<Route path="/battleground" element={<ProtectedRoute><Battleground /></ProtectedRoute>} />
```

---

### Step 16: Add Navigation Link

**File:** `admin/src/components/BottomNav.jsx` (MODIFY)

Add battleground link:

```jsx
// Add to NAV_ITEMS array
{ path: '/battleground', icon: Swords, label: 'Battle' },

// Add import
import { Swords } from 'lucide-react';
```

---

### Step 17: Integration Hooks

**File:** `src/routes/calls.js` (MODIFY)

After logging a call, trigger record checks:

```javascript
// Add near top
const { checkRecord, RECORD_TYPES, createPulseEvent } = require('../services/leaderboardService');
const { updateChallengeProgress } = require('../services/challengesService');

// After successful call log (inside the route handler):
// Check for daily calls record
const todaysCalls = await getTodaysCallCount(userId);
await checkRecord(RECORD_TYPES.MOST_CALLS_DAY, userId, userName, todaysCalls);

// Update challenge progress
await updateChallengeProgress(userId, 'call', 1);

// If conversion, check for record
if (outcome === 'converted') {
  await createPulseEvent({
    event_type: 'conversion',
    user_id: userId,
    user_name: userName,
    title: `🎉 ${userName} just closed a deal!`,
    subtitle: leadName,
    emoji: '🎉',
    link_type: 'lead',
    link_id: leadId,
  });
}
```

---

### Step 18: Weekly Badge Cron Job

**File:** `src/jobs/index.js` (MODIFY)

Add Friday 3pm AEST job:

```javascript
// Add import
const { awardWeeklyBadges } = require('../services/badgesService');

// Add cron job (Friday 3pm AEST = Friday 4am UTC)
cron.schedule('0 4 * * 5', async () => {
  console.log('[CRON] Running weekly badge awards...');
  try {
    const result = await awardWeeklyBadges();
    console.log('[CRON] Weekly badges awarded:', result);
  } catch (error) {
    console.error('[CRON] Weekly badge error:', error);
  }
}, {
  timezone: 'Australia/Sydney'
});
```

---

## Files Summary

### CREATE NEW (Backend)
1. `supabase/gamification-battleground-migration.sql`
2. `src/services/leaderboardService.js`
3. `src/services/challengesService.js`
4. `src/services/badgesService.js`
5. `src/routes/battleground.js`

### MODIFY (Backend)
6. `src/index.js` - Register battleground routes
7. `src/routes/calls.js` - Add record/pulse triggers
8. `src/jobs/index.js` - Add weekly badge cron

### CREATE NEW (Frontend)
9. `admin/src/components/gamification/LiveLeaderboard.jsx`
10. `admin/src/components/gamification/HallOfFame.jsx`
11. `admin/src/components/gamification/DailyChallenges.jsx`
12. `admin/src/components/gamification/TeamPulseFeed.jsx`
13. `admin/src/components/gamification/StreakWars.jsx`
14. `admin/src/components/gamification/UserStatsModal.jsx`
15. `admin/src/pages/Battleground.jsx`

### MODIFY (Frontend)
16. `admin/src/api/client.js` - Add battlegroundApi
17. `admin/src/App.jsx` - Add Battleground route
18. `admin/src/components/BottomNav.jsx` - Add Battle nav item

---

## Build Order for CC2

1. ⬜ Create migration file, add to PENDING_MIGRATIONS.md
2. ⬜ Create `leaderboardService.js`
3. ⬜ Create `challengesService.js`
4. ⬜ Create `badgesService.js`
5. ⬜ Create `battleground.js` routes
6. ⬜ Modify `src/index.js` - register routes
7. ⬜ Modify `admin/src/api/client.js` - add API
8. ⬜ Create `LiveLeaderboard.jsx`
9. ⬜ Create `HallOfFame.jsx`
10. ⬜ Create `DailyChallenges.jsx`
11. ⬜ Create `TeamPulseFeed.jsx`
12. ⬜ Create `StreakWars.jsx`
13. ⬜ Create `UserStatsModal.jsx`
14. ⬜ Create `Battleground.jsx` page
15. ⬜ Modify `App.jsx` - add route
16. ⬜ Modify `BottomNav.jsx` - add nav item
17. ⬜ Modify `calls.js` - add triggers
18. ⬜ Modify `jobs/index.js` - add cron
19. ⬜ Build frontend: `cd admin && npm run build`
20. ⬜ Copy to public: `cp -r admin/dist/* public/`
21. ⬜ Commit and push

---

## What to Reuse

| Existing | Reuse For |
|----------|-----------|
| `CelebrationOverlay.jsx` | Record broken celebrations |
| `XPContext.jsx` | XP award handling |
| `RealtimeContext.jsx` | Team pulse live updates |
| `user_xp` table | Streak, level, XP data |
| `xp_history` table | Period leaderboard calculation |
| `src/services/slack.js` | Record/badge Slack notifications |

---

*CC1 Investigation Complete - Jan 17, 2026*
