# Response Wall Implementation Guide

## Overview
The Response Wall is a community-driven feature that allows campaign participants to share and vote on responses they receive from elected representatives. This creates transparency, accountability, and demonstrates which representatives are actually engaging with constituents.

## User Story
**As a campaign participant**, I want to share the response I received from my representative so that:
- Other community members can see which representatives are responsive
- Good/helpful responses get visibility through upvoting
- We can hold representatives accountable publicly
- Future participants know what to expect

## Feature Requirements

### Core Functionality
1. **Post Responses**: Users can submit responses they received from representatives
2. **Upvote System**: Community can upvote helpful/interesting responses
3. **Moderation**: Admin can verify, edit, or remove responses
4. **Campaign Context**: Responses are tied to specific campaigns
5. **Representative Tracking**: Track which representatives respond most/least
6. **Anonymous Option**: Users can choose to post anonymously

### Display Features
- Sort by: Most Recent, Most Upvoted, Verified Responses
- Filter by: Representative Level (Federal/Provincial/Municipal), Representative Name
- Response cards show: Representative info, response excerpt, upvote count, date, verified badge
- Pagination or infinite scroll for large volumes

---

## Implementation Plan

### Phase 1: Database Schema (NocoDB)

#### New Table: `representative_responses`

**Run this NocoDB setup script:**

```bash
# Add to scripts/build-nocodb.sh

# Create representative_responses table
curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "table_name": "representative_responses",
    "title": "Representative Responses",
    "columns": [
      {
        "column_name": "id",
        "title": "ID",
        "uidt": "ID",
        "pk": true,
        "ai": true
      },
      {
        "column_name": "campaign_id",
        "title": "Campaign ID",
        "uidt": "Number",
        "rqd": true
      },
      {
        "column_name": "user_id",
        "title": "User ID",
        "uidt": "Number",
        "rqd": false
      },
      {
        "column_name": "representative_name",
        "title": "Representative Name",
        "uidt": "SingleLineText",
        "rqd": true
      },
      {
        "column_name": "representative_level",
        "title": "Representative Level",
        "uidt": "SingleSelect",
        "dtxp": "Federal,Provincial,Municipal",
        "rqd": true
      },
      {
        "column_name": "representative_district",
        "title": "District/Riding",
        "uidt": "SingleLineText"
      },
      {
        "column_name": "response_type",
        "title": "Response Type",
        "uidt": "SingleSelect",
        "dtxp": "Email,Phone,Letter,Meeting,Other",
        "rqd": true
      },
      {
        "column_name": "response_text",
        "title": "Response Text",
        "uidt": "LongText",
        "rqd": true
      },
      {
        "column_name": "response_screenshot",
        "title": "Response Screenshot",
        "uidt": "Attachment"
      },
      {
        "column_name": "user_comment",
        "title": "User Comment",
        "uidt": "LongText"
      },
      {
        "column_name": "upvotes",
        "title": "Upvotes",
        "uidt": "Number",
        "cdf": "0"
      },
      {
        "column_name": "verified",
        "title": "Verified",
        "uidt": "Checkbox",
        "cdf": "false"
      },
      {
        "column_name": "is_anonymous",
        "title": "Post Anonymously",
        "uidt": "Checkbox",
        "cdf": "false"
      },
      {
        "column_name": "status",
        "title": "Status",
        "uidt": "SingleSelect",
        "dtxp": "pending,approved,rejected",
        "cdf": "pending"
      },
      {
        "column_name": "created_at",
        "title": "Created At",
        "uidt": "DateTime",
        "cdf": "CURRENT_TIMESTAMP"
      },
      {
        "column_name": "updated_at",
        "title": "Updated At",
        "uidt": "DateTime",
        "cdf": "CURRENT_TIMESTAMP"
      }
    ]
  }'
```

#### New Table: `response_upvotes`

**Track who upvoted what (prevent duplicate upvotes):**

```bash
# Create response_upvotes table
curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "table_name": "response_upvotes",
    "title": "Response Upvotes",
    "columns": [
      {
        "column_name": "id",
        "title": "ID",
        "uidt": "ID",
        "pk": true,
        "ai": true
      },
      {
        "column_name": "response_id",
        "title": "Response ID",
        "uidt": "Number",
        "rqd": true
      },
      {
        "column_name": "user_id",
        "title": "User ID",
        "uidt": "Number",
        "rqd": false
      },
      {
        "column_name": "ip_address",
        "title": "IP Address",
        "uidt": "SingleLineText"
      },
      {
        "column_name": "created_at",
        "title": "Created At",
        "uidt": "DateTime",
        "cdf": "CURRENT_TIMESTAMP"
      }
    ]
  }'

# Create unique constraint to prevent duplicate upvotes
# Note: This may need to be done via NocoDB UI or direct database access
```

---

### Phase 2: Backend API Implementation

#### File: `app/controllers/responses.js` (NEW FILE)

Create a new controller to handle response operations:

```javascript
const nocodbService = require('../services/nocodb');
const { validateResponse } = require('../utils/validators');

/**
 * Get all responses for a campaign
 * GET /api/campaigns/:slug/responses
 */
async function getCampaignResponses(req, res) {
  try {
    const { slug } = req.params;
    const { sort = 'recent', level, representative, limit = 20, offset = 0 } = req.query;

    // Get campaign ID from slug
    const campaign = await nocodbService.getCampaignBySlug(slug);
    if (!campaign) {
      return res.status(404).json({ error: 'Campaign not found' });
    }

    // Build filters
    let whereClause = `(campaign_id,eq,${campaign.Id})~and(status,eq,approved)`;
    
    if (level) {
      whereClause += `~and(representative_level,eq,${level})`;
    }
    
    if (representative) {
      whereClause += `~and(representative_name,like,%${representative}%)`;
    }

    // Determine sort order
    let sortParam = '-created_at'; // Default: newest first
    if (sort === 'popular') {
      sortParam = '-upvotes,-created_at';
    } else if (sort === 'verified') {
      sortParam = '-verified,-upvotes,-created_at';
    }

    // Fetch responses
    const responses = await nocodbService.getRecords('representative_responses', {
      where: whereClause,
      sort: sortParam,
      limit,
      offset
    });

    // For logged-in users, check which responses they've upvoted
    let userUpvotes = [];
    if (req.user) {
      userUpvotes = await nocodbService.getRecords('response_upvotes', {
        where: `(user_id,eq,${req.user.id})`,
        fields: 'response_id'
      });
    }

    const userUpvotedIds = userUpvotes.map(u => u.response_id);

    // Enrich responses with upvote status
    const enrichedResponses = responses.map(response => ({
      ...response,
      user_has_upvoted: userUpvotedIds.includes(response.Id)
    }));

    res.json({
      success: true,
      responses: enrichedResponses,
      pagination: {
        limit: parseInt(limit),
        offset: parseInt(offset),
        hasMore: responses.length === parseInt(limit)
      }
    });

  } catch (error) {
    console.error('Error fetching campaign responses:', error);
    res.status(500).json({ error: 'Failed to fetch responses' });
  }
}

/**
 * Submit a new response
 * POST /api/campaigns/:slug/responses
 */
async function submitResponse(req, res) {
  try {
    const { slug } = req.params;
    const {
      representative_name,
      representative_level,
      representative_district,
      response_type,
      response_text,
      user_comment,
      is_anonymous = false
    } = req.body;

    // Validate input
    const validation = validateResponse(req.body);
    if (!validation.valid) {
      return res.status(400).json({ error: validation.error });
    }

    // Get campaign
    const campaign = await nocodbService.getCampaignBySlug(slug);
    if (!campaign) {
      return res.status(404).json({ error: 'Campaign not found' });
    }

    // Require authentication for non-anonymous posts
    if (!is_anonymous && !req.user) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    // Handle file upload (screenshot) if present
    let screenshot_url = null;
    if (req.file) {
      screenshot_url = `/uploads/${req.file.filename}`;
    }

    // Create response record
    const responseData = {
      campaign_id: campaign.Id,
      user_id: is_anonymous ? null : req.user?.id,
      representative_name,
      representative_level,
      representative_district,
      response_type,
      response_text,
      response_screenshot: screenshot_url,
      user_comment,
      is_anonymous,
      upvotes: 0,
      verified: false,
      status: 'pending', // Requires admin approval
      created_at: new Date().toISOString()
    };

    const newResponse = await nocodbService.createRecord('representative_responses', responseData);

    res.status(201).json({
      success: true,
      message: 'Response submitted successfully. It will appear after admin approval.',
      response: newResponse
    });

  } catch (error) {
    console.error('Error submitting response:', error);
    res.status(500).json({ error: 'Failed to submit response' });
  }
}

/**
 * Upvote a response
 * POST /api/responses/:id/upvote
 */
async function upvoteResponse(req, res) {
  try {
    const { id } = req.params;
    const userId = req.user?.id;
    const ipAddress = req.ip || req.connection.remoteAddress;

    // Check if response exists
    const response = await nocodbService.getRecord('representative_responses', id);
    if (!response) {
      return res.status(404).json({ error: 'Response not found' });
    }

    // Check if user/IP already upvoted
    let whereClause = `(response_id,eq,${id})`;
    if (userId) {
      whereClause += `~and(user_id,eq,${userId})`;
    } else {
      whereClause += `~and(ip_address,eq,${ipAddress})`;
    }

    const existingUpvote = await nocodbService.getRecords('response_upvotes', {
      where: whereClause,
      limit: 1
    });

    if (existingUpvote.length > 0) {
      return res.status(400).json({ error: 'You have already upvoted this response' });
    }

    // Create upvote record
    await nocodbService.createRecord('response_upvotes', {
      response_id: parseInt(id),
      user_id: userId || null,
      ip_address: ipAddress,
      created_at: new Date().toISOString()
    });

    // Increment upvote count
    const newUpvoteCount = (response.upvotes || 0) + 1;
    await nocodbService.updateRecord('representative_responses', id, {
      upvotes: newUpvoteCount
    });

    res.json({
      success: true,
      upvotes: newUpvoteCount,
      message: 'Response upvoted successfully'
    });

  } catch (error) {
    console.error('Error upvoting response:', error);
    res.status(500).json({ error: 'Failed to upvote response' });
  }
}

/**
 * Remove upvote from a response
 * DELETE /api/responses/:id/upvote
 */
async function removeUpvote(req, res) {
  try {
    const { id } = req.params;
    const userId = req.user?.id;
    const ipAddress = req.ip || req.connection.remoteAddress;

    // Find upvote record
    let whereClause = `(response_id,eq,${id})`;
    if (userId) {
      whereClause += `~and(user_id,eq,${userId})`;
    } else {
      whereClause += `~and(ip_address,eq,${ipAddress})`;
    }

    const upvotes = await nocodbService.getRecords('response_upvotes', {
      where: whereClause,
      limit: 1
    });

    if (upvotes.length === 0) {
      return res.status(400).json({ error: 'You have not upvoted this response' });
    }

    // Delete upvote record
    await nocodbService.deleteRecord('response_upvotes', upvotes[0].Id);

    // Decrement upvote count
    const response = await nocodbService.getRecord('representative_responses', id);
    const newUpvoteCount = Math.max(0, (response.upvotes || 0) - 1);
    await nocodbService.updateRecord('representative_responses', id, {
      upvotes: newUpvoteCount
    });

    res.json({
      success: true,
      upvotes: newUpvoteCount,
      message: 'Upvote removed successfully'
    });

  } catch (error) {
    console.error('Error removing upvote:', error);
    res.status(500).json({ error: 'Failed to remove upvote' });
  }
}

/**
 * Get response statistics for a campaign
 * GET /api/campaigns/:slug/response-stats
 */
async function getResponseStats(req, res) {
  try {
    const { slug } = req.params;

    const campaign = await nocodbService.getCampaignBySlug(slug);
    if (!campaign) {
      return res.status(404).json({ error: 'Campaign not found' });
    }

    const responses = await nocodbService.getRecords('representative_responses', {
      where: `(campaign_id,eq,${campaign.Id})~and(status,eq,approved)`,
      fields: 'representative_name,representative_level,verified'
    });

    // Calculate stats
    const stats = {
      total_responses: responses.length,
      verified_responses: responses.filter(r => r.verified).length,
      by_level: {
        Federal: responses.filter(r => r.representative_level === 'Federal').length,
        Provincial: responses.filter(r => r.representative_level === 'Provincial').length,
        Municipal: responses.filter(r => r.representative_level === 'Municipal').length
      },
      unique_representatives: new Set(responses.map(r => r.representative_name)).size
    };

    res.json({ success: true, stats });

  } catch (error) {
    console.error('Error fetching response stats:', error);
    res.status(500).json({ error: 'Failed to fetch stats' });
  }
}

module.exports = {
  getCampaignResponses,
  submitResponse,
  upvoteResponse,
  removeUpvote,
  getResponseStats
};
```

#### File: `app/routes/api.js` (UPDATE)

Add response routes to existing API routes:

```javascript
// Add at top with other requires
const responsesController = require('../controllers/responses');
const upload = require('../middleware/upload'); // Need to create this for file uploads

// Add these routes (around line 40-50, after campaign routes)
// Response Wall Routes
router.get('/campaigns/:slug/responses', responsesController.getCampaignResponses);
router.get('/campaigns/:slug/response-stats', responsesController.getResponseStats);
router.post('/campaigns/:slug/responses', 
  optionalAuth, // Allows anonymous submissions
  upload.single('screenshot'), 
  responsesController.submitResponse
);
router.post('/responses/:id/upvote', optionalAuth, responsesController.upvoteResponse);
router.delete('/responses/:id/upvote', optionalAuth, responsesController.removeUpvote);
```

#### File: `app/middleware/upload.js` (NEW FILE)

Create file upload middleware for screenshots:

```javascript
const multer = require('multer');
const path = require('path');
const fs = require('fs');

// Ensure uploads directory exists
const uploadDir = path.join(__dirname, '../public/uploads/responses');
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir, { recursive: true });
}

// Configure storage
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, 'response-' + uniqueSuffix + path.extname(file.originalname));
  }
});

// File filter - only images
const fileFilter = (req, file, cb) => {
  const allowedTypes = /jpeg|jpg|png|gif|webp/;
  const extname = allowedTypes.test(path.extname(file.originalname).toLowerCase());
  const mimetype = allowedTypes.test(file.mimetype);

  if (mimetype && extname) {
    return cb(null, true);
  } else {
    cb(new Error('Only image files are allowed (jpeg, jpg, png, gif, webp)'));
  }
};

// Configure multer
const upload = multer({
  storage: storage,
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB max
  },
  fileFilter: fileFilter
});

module.exports = upload;
```

#### File: `app/middleware/auth.js` (UPDATE)

Add optional authentication middleware:

```javascript
// Add this new function to existing auth.js
async function optionalAuth(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1] || req.cookies?.token;
  
  if (!token) {
    // No token provided, but that's OK - continue without user
    req.user = null;
    return next();
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await nocodbService.getUserById(decoded.userId);
    
    if (user) {
      req.user = user;
    } else {
      req.user = null;
    }
    
    next();
  } catch (error) {
    // Invalid token, but continue anyway
    req.user = null;
    next();
  }
}

// Export it
module.exports = {
  requireAuth,
  requireAdmin,
  optionalAuth // Add this
};
```

#### File: `app/utils/validators.js` (UPDATE)

Add response validation:

```javascript
// Add this function to existing validators.js
function validateResponse(data) {
  const { representative_name, representative_level, response_type, response_text } = data;

  if (!representative_name || representative_name.trim().length < 2) {
    return { valid: false, error: 'Representative name is required (min 2 characters)' };
  }

  const validLevels = ['Federal', 'Provincial', 'Municipal'];
  if (!representative_level || !validLevels.includes(representative_level)) {
    return { valid: false, error: 'Valid representative level is required (Federal, Provincial, or Municipal)' };
  }

  const validTypes = ['Email', 'Phone', 'Letter', 'Meeting', 'Other'];
  if (!response_type || !validTypes.includes(response_type)) {
    return { valid: false, error: 'Valid response type is required' };
  }

  if (!response_text || response_text.trim().length < 10) {
    return { valid: false, error: 'Response text is required (min 10 characters)' };
  }

  if (response_text.length > 5000) {
    return { valid: false, error: 'Response text is too long (max 5000 characters)' };
  }

  return { valid: true };
}

// Export it
module.exports = {
  validateEmail,
  validatePostalCode,
  validateCampaign,
  validateResponse // Add this
};
```

#### File: `package.json` (UPDATE)

Add multer dependency:

```json
"dependencies": {
  // ... existing dependencies
  "multer": "^1.4.5-lts.1"
}
```

---

### Phase 3: Frontend Implementation

#### File: `app/public/response-wall.html` (NEW FILE)

Create dedicated response wall page (can be accessed standalone or embedded):

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Community Responses - BNKops Influence</title>
    <link rel="stylesheet" href="/css/styles.css">
    <link rel="stylesheet" href="/css/response-wall.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>Community Response Wall</h1>
            <p>See what responses community members are getting from their representatives</p>
        </header>

        <!-- Stats Banner -->
        <div id="response-stats" class="stats-banner">
            <div class="stat-item">
                <span class="stat-number" id="total-responses">0</span>
                <span class="stat-label">Total Responses</span>
            </div>
            <div class="stat-item">
                <span class="stat-number" id="verified-responses">0</span>
                <span class="stat-label">Verified</span>
            </div>
            <div class="stat-item">
                <span class="stat-number" id="unique-reps">0</span>
                <span class="stat-label">Representatives</span>
            </div>
        </div>

        <!-- Filters and Sort -->
        <div class="response-controls">
            <div class="filter-group">
                <label for="level-filter">Level:</label>
                <select id="level-filter">
                    <option value="">All Levels</option>
                    <option value="Federal">Federal</option>
                    <option value="Provincial">Provincial</option>
                    <option value="Municipal">Municipal</option>
                </select>
            </div>

            <div class="filter-group">
                <label for="sort-select">Sort by:</label>
                <select id="sort-select">
                    <option value="recent">Most Recent</option>
                    <option value="popular">Most Popular</option>
                    <option value="verified">Verified First</option>
                </select>
            </div>

            <button id="submit-response-btn" class="btn btn-primary">
                Share Your Response
            </button>
        </div>

        <!-- Loading Spinner -->
        <div id="loading" class="loading" style="display: none;">
            <div class="spinner"></div>
            <p>Loading responses...</p>
        </div>

        <!-- Responses Container -->
        <div id="responses-container"></div>

        <!-- Load More Button -->
        <div id="load-more-container" style="display: none; text-align: center; margin-top: 2rem;">
            <button id="load-more-btn" class="btn btn-secondary">Load More Responses</button>
        </div>

        <!-- Empty State -->
        <div id="empty-state" class="empty-state" style="display: none;">
            <p>No responses yet. Be the first to share your experience!</p>
            <button class="btn btn-primary" onclick="openSubmitModal()">Share Your Response</button>
        </div>
    </div>

    <!-- Submit Response Modal -->
    <div id="submit-modal" class="modal" style="display: none;">
        <div class="modal-content">
            <span class="close" onclick="closeSubmitModal()">&times;</span>
            <h2>Share Your Representative's Response</h2>
            
            <form id="submit-response-form">
                <div class="form-group">
                    <label for="rep-name">Representative Name *</label>
                    <input type="text" id="rep-name" required>
                </div>

                <div class="form-group">
                    <label for="rep-level">Government Level *</label>
                    <select id="rep-level" required>
                        <option value="">Select level</option>
                        <option value="Federal">Federal (MP)</option>
                        <option value="Provincial">Provincial (MLA)</option>
                        <option value="Municipal">Municipal (Councillor/Mayor)</option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="rep-district">District/Riding</label>
                    <input type="text" id="rep-district" placeholder="e.g., Edmonton Centre">
                </div>

                <div class="form-group">
                    <label for="response-type">Response Type *</label>
                    <select id="response-type" required>
                        <option value="">Select type</option>
                        <option value="Email">Email</option>
                        <option value="Phone">Phone Call</option>
                        <option value="Letter">Physical Letter</option>
                        <option value="Meeting">In-Person Meeting</option>
                        <option value="Other">Other</option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="response-text">Response Content *</label>
                    <textarea id="response-text" rows="6" required 
                              placeholder="Share what your representative said or wrote..."></textarea>
                    <small class="text-muted">Minimum 10 characters</small>
                </div>

                <div class="form-group">
                    <label for="user-comment">Your Thoughts (Optional)</label>
                    <textarea id="user-comment" rows="3" 
                              placeholder="What did you think of this response?"></textarea>
                </div>

                <div class="form-group">
                    <label for="screenshot">Upload Screenshot (Optional)</label>
                    <input type="file" id="screenshot" accept="image/*">
                    <small class="text-muted">Max 5MB. Helps verify authenticity.</small>
                </div>

                <div class="form-group">
                    <label>
                        <input type="checkbox" id="is-anonymous">
                        Post anonymously
                    </label>
                </div>

                <div class="form-actions">
                    <button type="submit" class="btn btn-primary">Submit Response</button>
                    <button type="button" class="btn btn-secondary" onclick="closeSubmitModal()">Cancel</button>
                </div>

                <p class="text-muted" style="margin-top: 1rem; font-size: 0.9rem;">
                    Your response will be reviewed by moderators before appearing publicly.
                </p>
            </form>
        </div>
    </div>

    <script src="/js/api-client.js"></script>
    <script src="/js/response-wall.js"></script>
</body>
</html>
```

#### File: `app/public/css/response-wall.css` (NEW FILE)

Styles for the response wall:

```css
/* Response Wall Styles */

.stats-banner {
    display: flex;
    justify-content: space-around;
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 2rem;
    border-radius: 8px;
    margin-bottom: 2rem;
}

.stat-item {
    text-align: center;
}

.stat-number {
    display: block;
    font-size: 2.5rem;
    font-weight: bold;
    margin-bottom: 0.5rem;
}

.stat-label {
    display: block;
    font-size: 1rem;
    opacity: 0.9;
}

.response-controls {
    display: flex;
    gap: 1rem;
    margin-bottom: 2rem;
    flex-wrap: wrap;
    align-items: center;
}

.filter-group {
    display: flex;
    align-items: center;
    gap: 0.5rem;
}

.filter-group label {
    font-weight: 600;
    margin-bottom: 0;
}

.filter-group select {
    padding: 0.5rem;
    border: 1px solid #ddd;
    border-radius: 4px;
    font-size: 1rem;
}

#submit-response-btn {
    margin-left: auto;
}

/* Response Card */
.response-card {
    background: white;
    border: 1px solid #e1e8ed;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1.5rem;
    transition: box-shadow 0.3s ease;
}

.response-card:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.response-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 1rem;
}

.response-rep-info {
    flex: 1;
}

.response-rep-info h3 {
    margin: 0 0 0.25rem 0;
    color: #2c3e50;
    font-size: 1.2rem;
}

.response-rep-info .rep-meta {
    color: #7f8c8d;
    font-size: 0.9rem;
}

.response-rep-info .rep-meta span {
    margin-right: 1rem;
}

.response-badges {
    display: flex;
    gap: 0.5rem;
}

.badge {
    padding: 0.25rem 0.75rem;
    border-radius: 12px;
    font-size: 0.85rem;
    font-weight: 600;
}

.badge-verified {
    background: #27ae60;
    color: white;
}

.badge-level {
    background: #3498db;
    color: white;
}

.badge-type {
    background: #95a5a6;
    color: white;
}

.response-content {
    margin-bottom: 1rem;
}

.response-text {
    background: #f8f9fa;
    padding: 1rem;
    border-left: 4px solid #3498db;
    border-radius: 4px;
    margin-bottom: 1rem;
    font-style: italic;
    color: #2c3e50;
    line-height: 1.6;
}

.user-comment {
    padding: 0.75rem;
    background: #fff9e6;
    border-left: 4px solid #f39c12;
    border-radius: 4px;
    margin-bottom: 1rem;
}

.user-comment-label {
    font-weight: 600;
    color: #f39c12;
    margin-bottom: 0.5rem;
    display: block;
}

.response-screenshot {
    margin-bottom: 1rem;
}

.response-screenshot img {
    max-width: 100%;
    border-radius: 4px;
    border: 1px solid #ddd;
    cursor: pointer;
}

.response-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 1rem;
    border-top: 1px solid #e1e8ed;
}

.response-meta {
    color: #7f8c8d;
    font-size: 0.9rem;
}

.response-actions {
    display: flex;
    gap: 1rem;
}

.upvote-btn {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    background: white;
    border: 2px solid #3498db;
    color: #3498db;
    padding: 0.5rem 1rem;
    border-radius: 20px;
    cursor: pointer;
    transition: all 0.3s ease;
    font-weight: 600;
}

.upvote-btn:hover {
    background: #3498db;
    color: white;
    transform: translateY(-2px);
}

.upvote-btn.upvoted {
    background: #3498db;
    color: white;
}

.upvote-btn .upvote-icon {
    font-size: 1.2rem;
}

.upvote-count {
    font-weight: bold;
}

/* Empty State */
.empty-state {
    text-align: center;
    padding: 4rem 2rem;
    color: #7f8c8d;
}

.empty-state p {
    font-size: 1.2rem;
    margin-bottom: 1.5rem;
}

/* Modal Styles */
.modal {
    position: fixed;
    z-index: 1000;
    left: 0;
    top: 0;
    width: 100%;
    height: 100%;
    overflow: auto;
    background-color: rgba(0, 0, 0, 0.5);
}

.modal-content {
    background-color: white;
    margin: 5% auto;
    padding: 2rem;
    border-radius: 8px;
    width: 90%;
    max-width: 600px;
    position: relative;
}

.close {
    color: #aaa;
    float: right;
    font-size: 28px;
    font-weight: bold;
    cursor: pointer;
}

.close:hover,
.close:focus {
    color: #000;
}

.form-actions {
    display: flex;
    gap: 1rem;
    margin-top: 1.5rem;
}

.form-actions .btn {
    flex: 1;
}

/* Responsive Design */
@media (max-width: 768px) {
    .stats-banner {
        flex-direction: column;
        gap: 1.5rem;
    }

    .response-controls {
        flex-direction: column;
        align-items: stretch;
    }

    #submit-response-btn {
        margin-left: 0;
        width: 100%;
    }

    .response-header {
        flex-direction: column;
        gap: 1rem;
    }

    .response-footer {
        flex-direction: column;
        align-items: flex-start;
        gap: 1rem;
    }

    .modal-content {
        margin: 10% auto;
        width: 95%;
        padding: 1.5rem;
    }
}
```

#### File: `app/public/js/response-wall.js` (NEW FILE)

JavaScript for response wall functionality:

```javascript
// Response Wall JavaScript

let currentCampaignSlug = null;
let currentOffset = 0;
let currentSort = 'recent';
let currentLevel = '';
const LIMIT = 20;

// Initialize
document.addEventListener('DOMContentLoaded', () => {
    // Get campaign slug from URL if present
    const urlParams = new URLSearchParams(window.location.search);
    currentCampaignSlug = urlParams.get('campaign');

    if (!currentCampaignSlug) {
        showError('No campaign specified');
        return;
    }

    loadResponseStats();
    loadResponses();

    // Event listeners
    document.getElementById('sort-select').addEventListener('change', (e) => {
        currentSort = e.target.value;
        currentOffset = 0;
        loadResponses(true);
    });

    document.getElementById('level-filter').addEventListener('change', (e) => {
        currentLevel = e.target.value;
        currentOffset = 0;
        loadResponses(true);
    });

    document.getElementById('submit-response-btn').addEventListener('click', openSubmitModal);
    document.getElementById('load-more-btn').addEventListener('click', loadMoreResponses);
    document.getElementById('submit-response-form').addEventListener('submit', handleSubmitResponse);
});

// Load response statistics
async function loadResponseStats() {
    try {
        const response = await fetch(`/api/campaigns/${currentCampaignSlug}/response-stats`);
        const data = await response.json();

        if (data.success) {
            document.getElementById('total-responses').textContent = data.stats.total_responses;
            document.getElementById('verified-responses').textContent = data.stats.verified_responses;
            document.getElementById('unique-reps').textContent = data.stats.unique_representatives;
        }
    } catch (error) {
        console.error('Error loading stats:', error);
    }
}

// Load responses
async function loadResponses(reset = false) {
    if (reset) {
        currentOffset = 0;
        document.getElementById('responses-container').innerHTML = '';
    }

    showLoading(true);

    try {
        let url = `/api/campaigns/${currentCampaignSlug}/responses?sort=${currentSort}&limit=${LIMIT}&offset=${currentOffset}`;
        
        if (currentLevel) {
            url += `&level=${currentLevel}`;
        }

        const response = await fetch(url);
        const data = await response.json();

        if (data.success) {
            if (data.responses.length === 0 && currentOffset === 0) {
                showEmptyState();
            } else {
                renderResponses(data.responses);
                
                // Show/hide load more button
                const loadMoreContainer = document.getElementById('load-more-container');
                if (data.pagination.hasMore) {
                    loadMoreContainer.style.display = 'block';
                } else {
                    loadMoreContainer.style.display = 'none';
                }
            }
        } else {
            showError('Failed to load responses');
        }
    } catch (error) {
        console.error('Error loading responses:', error);
        showError('Failed to load responses');
    } finally {
        showLoading(false);
    }
}

// Render responses
function renderResponses(responses) {
    const container = document.getElementById('responses-container');
    document.getElementById('empty-state').style.display = 'none';

    responses.forEach(response => {
        const card = createResponseCard(response);
        container.appendChild(card);
    });
}

// Create response card element
function createResponseCard(response) {
    const card = document.createElement('div');
    card.className = 'response-card';
    card.dataset.responseId = response.Id;

    // Format date
    const date = new Date(response.created_at);
    const formattedDate = date.toLocaleDateString('en-US', { 
        year: 'numeric', 
        month: 'short', 
        day: 'numeric' 
    });

    // Build badges
    let badges = `<span class="badge badge-level">${response.representative_level}</span>`;
    badges += `<span class="badge badge-type">${response.response_type}</span>`;
    if (response.verified) {
        badges = `<span class="badge badge-verified">✓ Verified</span>` + badges;
    }

    // Build screenshot if present
    let screenshotHTML = '';
    if (response.response_screenshot) {
        screenshotHTML = `
            <div class="response-screenshot">
                <img src="${response.response_screenshot}" alt="Response screenshot" 
                     onclick="viewImage('${response.response_screenshot}')">
            </div>
        `;
    }

    // Build user comment if present
    let commentHTML = '';
    if (response.user_comment) {
        commentHTML = `
            <div class="user-comment">
                <span class="user-comment-label">Community Member's Thoughts:</span>
                ${escapeHtml(response.user_comment)}
            </div>
        `;
    }

    // Build district info
    let districtInfo = '';
    if (response.representative_district) {
        districtInfo = `<span>📍 ${escapeHtml(response.representative_district)}</span>`;
    }

    card.innerHTML = `
        <div class="response-header">
            <div class="response-rep-info">
                <h3>${escapeHtml(response.representative_name)}</h3>
                <div class="rep-meta">
                    ${districtInfo}
                </div>
            </div>
            <div class="response-badges">
                ${badges}
            </div>
        </div>

        <div class="response-content">
            <div class="response-text">
                "${escapeHtml(response.response_text)}"
            </div>
            ${commentHTML}
            ${screenshotHTML}
        </div>

        <div class="response-footer">
            <div class="response-meta">
                Posted ${formattedDate}
                ${response.is_anonymous ? ' by Anonymous' : ''}
            </div>
            <div class="response-actions">
                <button class="upvote-btn ${response.user_has_upvoted ? 'upvoted' : ''}" 
                        onclick="toggleUpvote(${response.Id}, this)">
                    <span class="upvote-icon">👍</span>
                    <span class="upvote-count">${response.upvotes || 0}</span>
                </button>
            </div>
        </div>
    `;

    return card;
}

// Toggle upvote
async function toggleUpvote(responseId, button) {
    const isUpvoted = button.classList.contains('upvoted');
    const countElement = button.querySelector('.upvote-count');
    const currentCount = parseInt(countElement.textContent);

    try {
        const method = isUpvoted ? 'DELETE' : 'POST';
        const response = await fetch(`/api/responses/${responseId}/upvote`, {
            method: method,
            headers: {
                'Content-Type': 'application/json'
            }
        });

        const data = await response.json();

        if (data.success) {
            button.classList.toggle('upvoted');
            countElement.textContent = data.upvotes;
        } else {
            showError(data.error || 'Failed to update upvote');
        }
    } catch (error) {
        console.error('Error toggling upvote:', error);
        showError('Failed to update upvote');
    }
}

// Load more responses
function loadMoreResponses() {
    currentOffset += LIMIT;
    loadResponses(false);
}

// Open submit modal
function openSubmitModal() {
    document.getElementById('submit-modal').style.display = 'block';
}

// Close submit modal
function closeSubmitModal() {
    document.getElementById('submit-modal').style.display = 'none';
    document.getElementById('submit-response-form').reset();
}

// Handle response submission
async function handleSubmitResponse(e) {
    e.preventDefault();

    const formData = new FormData();
    formData.append('representative_name', document.getElementById('rep-name').value.trim());
    formData.append('representative_level', document.getElementById('rep-level').value);
    formData.append('representative_district', document.getElementById('rep-district').value.trim());
    formData.append('response_type', document.getElementById('response-type').value);
    formData.append('response_text', document.getElementById('response-text').value.trim());
    formData.append('user_comment', document.getElementById('user-comment').value.trim());
    formData.append('is_anonymous', document.getElementById('is-anonymous').checked);

    const screenshotFile = document.getElementById('screenshot').files[0];
    if (screenshotFile) {
        formData.append('screenshot', screenshotFile);
    }

    try {
        const submitButton = e.target.querySelector('button[type="submit"]');
        submitButton.disabled = true;
        submitButton.textContent = 'Submitting...';

        const response = await fetch(`/api/campaigns/${currentCampaignSlug}/responses`, {
            method: 'POST',
            body: formData
        });

        const data = await response.json();

        if (data.success) {
            showSuccess('Response submitted successfully! It will appear after admin approval.');
            closeSubmitModal();
        } else {
            showError(data.error || 'Failed to submit response');
        }
    } catch (error) {
        console.error('Error submitting response:', error);
        showError('Failed to submit response');
    } finally {
        const submitButton = e.target.querySelector('button[type="submit"]');
        submitButton.disabled = false;
        submitButton.textContent = 'Submit Response';
    }
}

// View image in modal/new tab
function viewImage(url) {
    window.open(url, '_blank');
}

// Utility functions
function showLoading(show) {
    document.getElementById('loading').style.display = show ? 'block' : 'none';
}

function showEmptyState() {
    document.getElementById('empty-state').style.display = 'block';
    document.getElementById('responses-container').innerHTML = '';
}

function showError(message) {
    // Could integrate with existing error display system
    alert(message);
}

function showSuccess(message) {
    // Could integrate with existing success display system
    alert(message);
}

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Close modal when clicking outside
window.onclick = function(event) {
    const modal = document.getElementById('submit-modal');
    if (event.target === modal) {
        closeSubmitModal();
    }
};
```

---

### Phase 4: Admin Panel Integration

#### File: `app/public/admin.html` (UPDATE)

Add response moderation section to existing admin panel:

```html
<!-- Add this new section after campaigns section -->
<section id="responses-section" style="display: none;">
    <h2>Response Moderation</h2>
    
    <div class="admin-filters">
        <select id="admin-response-status">
            <option value="pending">Pending Approval</option>
            <option value="approved">Approved</option>
            <option value="rejected">Rejected</option>
        </select>
        
        <button onclick="loadAdminResponses()" class="btn btn-secondary">Refresh</button>
    </div>

    <div id="admin-responses-container"></div>
</section>
```

#### File: `app/public/js/admin.js` (UPDATE)

Add response moderation functions:

```javascript
// Add these functions to existing admin.js

async function loadAdminResponses() {
    const status = document.getElementById('admin-response-status').value;
    
    try {
        const response = await fetch(`/api/admin/responses?status=${status}`, {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        const data = await response.json();
        
        if (data.success) {
            renderAdminResponses(data.responses);
        }
    } catch (error) {
        console.error('Error loading responses:', error);
    }
}

function renderAdminResponses(responses) {
    const container = document.getElementById('admin-responses-container');
    
    if (responses.length === 0) {
        container.innerHTML = '<p>No responses found.</p>';
        return;
    }
    
    container.innerHTML = responses.map(response => `
        <div class="admin-response-card">
            <h4>${response.representative_name} (${response.representative_level})</h4>
            <p><strong>Response:</strong> ${response.response_text.substring(0, 200)}...</p>
            ${response.user_comment ? `<p><strong>User Comment:</strong> ${response.user_comment}</p>` : ''}
            ${response.response_screenshot ? `<img src="${response.response_screenshot}" style="max-width: 300px;">` : ''}
            
            <div class="admin-actions">
                ${response.status === 'pending' ? `
                    <button onclick="approveResponse(${response.Id})" class="btn btn-success">Approve</button>
                    <button onclick="rejectResponse(${response.Id})" class="btn btn-danger">Reject</button>
                ` : ''}
                <button onclick="toggleVerified(${response.Id}, ${response.verified})" class="btn btn-secondary">
                    ${response.verified ? 'Unverify' : 'Verify'}
                </button>
                <button onclick="deleteResponse(${response.Id})" class="btn btn-danger">Delete</button>
            </div>
        </div>
    `).join('');
}

async function approveResponse(id) {
    await updateResponseStatus(id, 'approved');
}

async function rejectResponse(id) {
    await updateResponseStatus(id, 'rejected');
}

async function updateResponseStatus(id, status) {
    try {
        const response = await fetch(`/api/admin/responses/${id}/status`, {
            method: 'PATCH',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ status })
        });
        
        const data = await response.json();
        
        if (data.success) {
            showSuccess(`Response ${status}`);
            loadAdminResponses();
        }
    } catch (error) {
        console.error('Error updating response:', error);
        showError('Failed to update response');
    }
}

async function toggleVerified(id, currentlyVerified) {
    try {
        const response = await fetch(`/api/admin/responses/${id}`, {
            method: 'PATCH',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ verified: !currentlyVerified })
        });
        
        const data = await response.json();
        
        if (data.success) {
            showSuccess('Response updated');
            loadAdminResponses();
        }
    } catch (error) {
        console.error('Error toggling verified:', error);
    }
}

async function deleteResponse(id) {
    if (!confirm('Are you sure you want to delete this response?')) return;
    
    try {
        const response = await fetch(`/api/admin/responses/${id}`, {
            method: 'DELETE',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        const data = await response.json();
        
        if (data.success) {
            showSuccess('Response deleted');
            loadAdminResponses();
        }
    } catch (error) {
        console.error('Error deleting response:', error);
    }
}
```

#### File: `app/controllers/responses.js` (UPDATE)

Add admin endpoints:

```javascript
// Add these admin functions to responses controller

async function getAdminResponses(req, res) {
    try {
        const { status = 'pending' } = req.query;
        
        const responses = await nocodbService.getRecords('representative_responses', {
            where: `(status,eq,${status})`,
            sort: '-created_at',
            limit: 100
        });
        
        res.json({ success: true, responses });
    } catch (error) {
        console.error('Error fetching admin responses:', error);
        res.status(500).json({ error: 'Failed to fetch responses' });
    }
}

async function updateResponseStatus(req, res) {
    try {
        const { id } = req.params;
        const { status } = req.body;
        
        if (!['pending', 'approved', 'rejected'].includes(status)) {
            return res.status(400).json({ error: 'Invalid status' });
        }
        
        await nocodbService.updateRecord('representative_responses', id, {
            status,
            updated_at: new Date().toISOString()
        });
        
        res.json({ success: true, message: 'Response status updated' });
    } catch (error) {
        console.error('Error updating response status:', error);
        res.status(500).json({ error: 'Failed to update status' });
    }
}

async function updateResponse(req, res) {
    try {
        const { id } = req.params;
        const updates = req.body;
        
        updates.updated_at = new Date().toISOString();
        
        await nocodbService.updateRecord('representative_responses', id, updates);
        
        res.json({ success: true, message: 'Response updated' });
    } catch (error) {
        console.error('Error updating response:', error);
        res.status(500).json({ error: 'Failed to update response' });
    }
}

async function deleteResponse(req, res) {
    try {
        const { id } = req.params;
        
        await nocodbService.deleteRecord('representative_responses', id);
        
        res.json({ success: true, message: 'Response deleted' });
    } catch (error) {
        console.error('Error deleting response:', error);
        res.status(500).json({ error: 'Failed to delete response' });
    }
}

// Export these new functions
module.exports = {
    getCampaignResponses,
    submitResponse,
    upvoteResponse,
    removeUpvote,
    getResponseStats,
    // Admin functions
    getAdminResponses,
    updateResponseStatus,
    updateResponse,
    deleteResponse
};
```

#### File: `app/routes/api.js` (UPDATE)

Add admin routes:

```javascript
// Add these admin routes (after other admin routes)
router.get('/admin/responses', requireAdmin, responsesController.getAdminResponses);
router.patch('/admin/responses/:id/status', requireAdmin, responsesController.updateResponseStatus);
router.patch('/admin/responses/:id', requireAdmin, responsesController.updateResponse);
router.delete('/admin/responses/:id', requireAdmin, responsesController.deleteResponse);
```

---

### Phase 5: Integration with Campaign Page

#### File: `app/public/campaign.html` (UPDATE)

Add response wall section to existing campaign page:

```html
<!-- Add this section before the footer, after email composer -->
<section id="response-wall-section" style="margin-top: 3rem;">
    <h2>Community Responses</h2>
    <p>See what responses community members are getting from their representatives</p>
    
    <div id="campaign-response-preview">
        <!-- Shows top 3 responses -->
    </div>
    
    <button onclick="viewAllResponses()" class="btn btn-secondary" style="margin-top: 1rem;">
        View All Responses
    </button>
</section>
```

#### File: `app/public/js/campaign.js` (UPDATE)

Add response preview loading:

```javascript
// Add this function to existing campaign.js

async function loadResponsePreview() {
    try {
        const response = await fetch(`/api/campaigns/${campaignSlug}/responses?limit=3&sort=popular`);
        const data = await response.json();
        
        if (data.success && data.responses.length > 0) {
            renderResponsePreview(data.responses);
            document.getElementById('response-wall-section').style.display = 'block';
        }
    } catch (error) {
        console.error('Error loading response preview:', error);
    }
}

function renderResponsePreview(responses) {
    const container = document.getElementById('campaign-response-preview');
    
    container.innerHTML = responses.map(response => `
        <div class="response-preview-card">
            <strong>${response.representative_name}</strong> (${response.representative_level})
            <p>${response.response_text.substring(0, 150)}...</p>
            <span>👍 ${response.upvotes || 0}</span>
        </div>
    `).join('');
}

function viewAllResponses() {
    window.location.href = `/response-wall.html?campaign=${campaignSlug}`;
}

// Call this in your campaign init function
loadResponsePreview();
```

---

## Testing Checklist

### Backend Testing
- [ ] Database tables created successfully
- [ ] POST response submission works (with/without auth)
- [ ] GET responses returns correct data
- [ ] Upvote/downvote works correctly
- [ ] Duplicate upvote prevention works
- [ ] File upload works for screenshots
- [ ] Admin endpoints require authentication
- [ ] Status updates work (pending/approved/rejected)

### Frontend Testing
- [ ] Response wall page loads correctly
- [ ] Responses display with correct formatting
- [ ] Sorting works (recent, popular, verified)
- [ ] Filtering by level works
- [ ] Upvote button updates correctly
- [ ] Submit modal opens and closes
- [ ] Form validation works
- [ ] File upload works
- [ ] Anonymous posting works
- [ ] Admin moderation panel works

### Integration Testing
- [ ] Campaign page shows response preview
- [ ] Stats update correctly
- [ ] Pagination/load more works
- [ ] Mobile responsive design
- [ ] Error handling shows appropriate messages

---

## Deployment Steps

1. **Update NocoDB Schema:**
   ```bash
   cd /path/to/influence
   chmod +x scripts/build-nocodb.sh
   ./scripts/build-nocodb.sh
   ```

2. **Install Dependencies:**
   ```bash
   cd app
   npm install multer
   ```

3. **Create Upload Directory:**
   ```bash
   mkdir -p app/public/uploads/responses
   ```

4. **Rebuild Docker Container:**
   ```bash
   docker compose down
   docker compose build
   docker compose up -d
   ```

5. **Test Endpoints:**
   - Visit campaign page
   - Try submitting a response
   - Check admin panel for moderation

---

---

## Phase 6: Representative Accounts & Direct Response System

### Overview
Allow elected representatives to create verified accounts and respond directly to campaign emails through the platform. These responses get a special "Verified by Representative" badge, creating authentic two-way dialogue.

### Benefits
- **Authenticity**: Representatives control their own responses
- **Transparency**: Public record of representative positions
- **Engagement**: Encourages reps to participate directly
- **Accountability**: Official statements are permanently visible
- **Credibility**: Community trusts verified responses more

---

### Database Schema Updates

#### Update Table: `users` (Add Representative Fields)

Add these columns to the existing `users` table:

```bash
# Add to scripts/build-nocodb.sh

# Add representative-specific columns to users table
curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "user_type",
    "title": "User Type",
    "uidt": "SingleSelect",
    "dtxp": "regular,representative,admin",
    "cdf": "regular"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_name",
    "title": "Representative Name",
    "uidt": "SingleLineText"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_level",
    "title": "Representative Level",
    "uidt": "SingleSelect",
    "dtxp": "Federal,Provincial,Municipal"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_district",
    "title": "District/Riding",
    "uidt": "SingleLineText"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_verified",
    "title": "Representative Verified",
    "uidt": "Checkbox",
    "cdf": "false"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_bio",
    "title": "Representative Bio",
    "uidt": "LongText"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_photo",
    "title": "Representative Photo",
    "uidt": "Attachment"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{users_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "verification_document",
    "title": "Verification Document",
    "uidt": "Attachment"
  }'
```

#### Update Table: `representative_responses` (Add Representative Fields)

Add these columns:

```bash
curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{responses_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "posted_by_representative",
    "title": "Posted by Representative",
    "uidt": "Checkbox",
    "cdf": "false"
  }'

curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables/{responses_table_id}/columns" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "column_name": "representative_user_id",
    "title": "Representative User ID",
    "uidt": "Number"
  }'
```

#### New Table: `representative_inbox`

Track emails sent to representatives through campaigns:

```bash
curl -X POST "$NOCODB_API_URL/api/v1/db/meta/tables" \
  -H "xc-token: $NOCODB_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "table_name": "representative_inbox",
    "title": "Representative Inbox",
    "columns": [
      {
        "column_name": "id",
        "title": "ID",
        "uidt": "ID",
        "pk": true,
        "ai": true
      },
      {
        "column_name": "campaign_id",
        "title": "Campaign ID",
        "uidt": "Number",
        "rqd": true
      },
      {
        "column_name": "email_log_id",
        "title": "Email Log ID",
        "uidt": "Number"
      },
      {
        "column_name": "representative_name",
        "title": "Representative Name",
        "uidt": "SingleLineText",
        "rqd": true
      },
      {
        "column_name": "representative_email",
        "title": "Representative Email",
        "uidt": "SingleLineText",
        "rqd": true
      },
      {
        "column_name": "representative_level",
        "title": "Representative Level",
        "uidt": "SingleSelect",
        "dtxp": "Federal,Provincial,Municipal",
        "rqd": true
      },
      {
        "column_name": "email_subject",
        "title": "Email Subject",
        "uidt": "SingleLineText"
      },
      {
        "column_name": "email_body",
        "title": "Email Body",
        "uidt": "LongText"
      },
      {
        "column_name": "sender_postal_code",
        "title": "Sender Postal Code",
        "uidt": "SingleLineText"
      },
      {
        "column_name": "has_response",
        "title": "Has Response",
        "uidt": "Checkbox",
        "cdf": "false"
      },
      {
        "column_name": "response_id",
        "title": "Response ID",
        "uidt": "Number"
      },
      {
        "column_name": "created_at",
        "title": "Created At",
        "uidt": "DateTime",
        "cdf": "CURRENT_TIMESTAMP"
      }
    ]
  }'
```

---

### Backend Implementation

#### File: `app/controllers/representativeController.js` (NEW FILE)

```javascript
const nocodbService = require('../services/nocodb');
const emailService = require('../services/email');
const { validateResponse } = require('../utils/validators');

/**
 * Get representative's inbox - emails sent to them through campaigns
 * GET /api/representative/inbox
 */
async function getRepresentativeInbox(req, res) {
  try {
    if (!req.user || req.user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    if (!req.user.representative_verified) {
      return res.status(403).json({ error: 'Your representative account is pending verification' });
    }

    const { limit = 20, offset = 0, has_response } = req.query;

    // Build filters
    let whereClause = `(representative_email,eq,${req.user.email})`;
    
    if (has_response !== undefined) {
      whereClause += `~and(has_response,eq,${has_response})`;
    }

    const emails = await nocodbService.getRecords('representative_inbox', {
      where: whereClause,
      sort: '-created_at',
      limit,
      offset
    });

    // Get campaign details for each email
    const enrichedEmails = await Promise.all(emails.map(async (email) => {
      const campaign = await nocodbService.getRecord('campaigns', email.campaign_id);
      return {
        ...email,
        campaign_title: campaign?.title || 'Unknown Campaign',
        campaign_slug: campaign?.slug
      };
    }));

    res.json({
      success: true,
      emails: enrichedEmails,
      pagination: {
        limit: parseInt(limit),
        offset: parseInt(offset),
        hasMore: emails.length === parseInt(limit)
      }
    });

  } catch (error) {
    console.error('Error fetching representative inbox:', error);
    res.status(500).json({ error: 'Failed to fetch inbox' });
  }
}

/**
 * Get inbox statistics
 * GET /api/representative/inbox/stats
 */
async function getInboxStats(req, res) {
  try {
    if (!req.user || req.user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    const allEmails = await nocodbService.getRecords('representative_inbox', {
      where: `(representative_email,eq,${req.user.email})`,
      fields: 'has_response,campaign_id'
    });

    const stats = {
      total_emails: allEmails.length,
      unanswered: allEmails.filter(e => !e.has_response).length,
      answered: allEmails.filter(e => e.has_response).length,
      unique_campaigns: new Set(allEmails.map(e => e.campaign_id)).size
    };

    res.json({ success: true, stats });

  } catch (error) {
    console.error('Error fetching inbox stats:', error);
    res.status(500).json({ error: 'Failed to fetch stats' });
  }
}

/**
 * Submit official response from representative
 * POST /api/representative/respond/:inboxId
 */
async function submitOfficialResponse(req, res) {
  try {
    if (!req.user || req.user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    if (!req.user.representative_verified) {
      return res.status(403).json({ error: 'Your representative account is pending verification' });
    }

    const { inboxId } = req.params;
    const { response_text, user_comment, response_type = 'Email' } = req.body;

    // Validate
    if (!response_text || response_text.trim().length < 10) {
      return res.status(400).json({ error: 'Response text is required (min 10 characters)' });
    }

    // Get inbox item
    const inboxItem = await nocodbService.getRecord('representative_inbox', inboxId);
    if (!inboxItem) {
      return res.status(404).json({ error: 'Email not found' });
    }

    // Verify this email was sent to this representative
    if (inboxItem.representative_email !== req.user.email) {
      return res.status(403).json({ error: 'You can only respond to emails sent to you' });
    }

    // Handle file upload (screenshot) if present
    let screenshot_url = null;
    if (req.file) {
      screenshot_url = `/uploads/responses/${req.file.filename}`;
    }

    // Create verified response record
    const responseData = {
      campaign_id: inboxItem.campaign_id,
      user_id: req.user.id,
      representative_user_id: req.user.id,
      representative_name: req.user.representative_name,
      representative_level: req.user.representative_level,
      representative_district: req.user.representative_district,
      response_type,
      response_text,
      response_screenshot: screenshot_url,
      user_comment,
      is_anonymous: false,
      posted_by_representative: true,
      upvotes: 0,
      verified: true, // Auto-verified since it's from the rep
      status: 'approved', // Auto-approved
      created_at: new Date().toISOString()
    };

    const newResponse = await nocodbService.createRecord('representative_responses', responseData);

    // Update inbox item
    await nocodbService.updateRecord('representative_inbox', inboxId, {
      has_response: true,
      response_id: newResponse.Id
    });

    res.status(201).json({
      success: true,
      message: 'Response published successfully',
      response: newResponse
    });

  } catch (error) {
    console.error('Error submitting official response:', error);
    res.status(500).json({ error: 'Failed to submit response' });
  }
}

/**
 * Get representative profile (public)
 * GET /api/representative/profile/:id
 */
async function getRepresentativeProfile(req, res) {
  try {
    const { id } = req.params;

    const user = await nocodbService.getRecord('users', id);
    
    if (!user || user.user_type !== 'representative') {
      return res.status(404).json({ error: 'Representative not found' });
    }

    // Get response stats
    const responses = await nocodbService.getRecords('representative_responses', {
      where: `(representative_user_id,eq,${id})~and(status,eq,approved)`,
      fields: 'Id,created_at,campaign_id,upvotes'
    });

    const profile = {
      id: user.Id,
      name: user.representative_name,
      level: user.representative_level,
      district: user.representative_district,
      bio: user.representative_bio,
      photo: user.representative_photo,
      verified: user.representative_verified,
      stats: {
        total_responses: responses.length,
        total_upvotes: responses.reduce((sum, r) => sum + (r.upvotes || 0), 0),
        campaigns_responded: new Set(responses.map(r => r.campaign_id)).size,
        member_since: user.created_at
      }
    };

    res.json({ success: true, profile });

  } catch (error) {
    console.error('Error fetching representative profile:', error);
    res.status(500).json({ error: 'Failed to fetch profile' });
  }
}

/**
 * Update representative profile
 * PATCH /api/representative/profile
 */
async function updateRepresentativeProfile(req, res) {
  try {
    if (!req.user || req.user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    const { representative_bio } = req.body;
    const updates = {};

    if (representative_bio !== undefined) {
      updates.representative_bio = representative_bio;
    }

    if (req.file) {
      updates.representative_photo = `/uploads/representatives/${req.file.filename}`;
    }

    if (Object.keys(updates).length > 0) {
      await nocodbService.updateRecord('users', req.user.id, updates);
    }

    res.json({ success: true, message: 'Profile updated successfully' });

  } catch (error) {
    console.error('Error updating profile:', error);
    res.status(500).json({ error: 'Failed to update profile' });
  }
}

/**
 * Request representative verification
 * POST /api/representative/request-verification
 */
async function requestVerification(req, res) {
  try {
    if (!req.user || req.user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    const { representative_name, representative_level, representative_district } = req.body;

    if (!representative_name || !representative_level || !representative_district) {
      return res.status(400).json({ error: 'All representative details are required' });
    }

    const updates = {
      representative_name,
      representative_level,
      representative_district
    };

    if (req.file) {
      updates.verification_document = `/uploads/verification/${req.file.filename}`;
    }

    await nocodbService.updateRecord('users', req.user.id, updates);

    // Notify admins (could send email here)
    console.log(`Verification requested by ${representative_name}`);

    res.json({ 
      success: true, 
      message: 'Verification request submitted. An administrator will review your request.' 
    });

  } catch (error) {
    console.error('Error requesting verification:', error);
    res.status(500).json({ error: 'Failed to submit verification request' });
  }
}

module.exports = {
  getRepresentativeInbox,
  getInboxStats,
  submitOfficialResponse,
  getRepresentativeProfile,
  updateRepresentativeProfile,
  requestVerification
};
```

#### File: `app/middleware/auth.js` (UPDATE)

Add representative check middleware:

```javascript
// Add this new middleware function
async function requireRepresentative(req, res, next) {
  try {
    const token = req.headers.authorization?.split(' ')[1] || req.cookies?.token;
    
    if (!token) {
      return res.status(401).json({ error: 'Authentication required' });
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await nocodbService.getUserById(decoded.userId);
    
    if (!user) {
      return res.status(401).json({ error: 'Invalid token' });
    }

    if (user.user_type !== 'representative') {
      return res.status(403).json({ error: 'Representative access required' });
    }

    req.user = user;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid or expired token' });
  }
}

// Export it
module.exports = {
  requireAuth,
  requireAdmin,
  optionalAuth,
  requireRepresentative // Add this
};
```

#### File: `app/routes/api.js` (UPDATE)

Add representative routes:

```javascript
// Add at top with other requires
const representativeController = require('../controllers/representativeController');

// Add these routes (around line 60, after response routes)
// Representative Routes
router.get('/representative/inbox', requireRepresentative, representativeController.getRepresentativeInbox);
router.get('/representative/inbox/stats', requireRepresentative, representativeController.getInboxStats);
router.post('/representative/respond/:inboxId', 
  requireRepresentative, 
  upload.single('screenshot'), 
  representativeController.submitOfficialResponse
);
router.get('/representative/profile/:id', representativeController.getRepresentativeProfile); // Public
router.patch('/representative/profile', 
  requireRepresentative, 
  upload.single('photo'), 
  representativeController.updateRepresentativeProfile
);
router.post('/representative/request-verification', 
  requireRepresentative, 
  upload.single('verification_document'), 
  representativeController.requestVerification
);
```

#### File: `app/controllers/emails.js` (UPDATE)

Log emails sent to representatives:

```javascript
// Add this function call in your existing sendEmail function
// After successfully sending email to a representative

async function logEmailToRepresentativeInbox(campaignId, representative, emailSubject, emailBody, senderPostalCode) {
  try {
    await nocodbService.createRecord('representative_inbox', {
      campaign_id: campaignId,
      representative_name: representative.name,
      representative_email: representative.email,
      representative_level: representative.elected_office, // Adjust based on your data structure
      email_subject: emailSubject,
      email_body: emailBody,
      sender_postal_code: senderPostalCode,
      has_response: false,
      created_at: new Date().toISOString()
    });
  } catch (error) {
    console.error('Error logging email to representative inbox:', error);
    // Don't fail the email send if logging fails
  }
}

// Call this function in your sendCampaignEmail function:
// logEmailToRepresentativeInbox(campaign.Id, representative, subject, body, postalCode);
```

---

### Frontend Implementation

#### File: `app/public/representative-dashboard.html` (NEW FILE)

Representative inbox and response interface:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Representative Dashboard - BNKops Influence</title>
    <link rel="stylesheet" href="/css/styles.css">
    <link rel="stylesheet" href="/css/representative.css">
</head>
<body>
    <div class="container">
        <header>
            <h1>Representative Dashboard</h1>
            <p>Respond to your constituents directly</p>
            <div class="header-actions">
                <button onclick="window.location.href='/'" class="btn btn-secondary">View Public Site</button>
                <button onclick="logout()" class="btn btn-secondary">Logout</button>
            </div>
        </header>

        <!-- Stats Dashboard -->
        <div class="rep-stats-grid">
            <div class="stat-card">
                <div class="stat-icon">📧</div>
                <div class="stat-content">
                    <span class="stat-number" id="total-emails">0</span>
                    <span class="stat-label">Total Emails Received</span>
                </div>
            </div>
            <div class="stat-card">
                <div class="stat-icon">⏳</div>
                <div class="stat-content">
                    <span class="stat-number" id="unanswered-emails">0</span>
                    <span class="stat-label">Awaiting Response</span>
                </div>
            </div>
            <div class="stat-card">
                <div class="stat-icon">✅</div>
                <div class="stat-content">
                    <span class="stat-number" id="answered-emails">0</span>
                    <span class="stat-label">Responded</span>
                </div>
            </div>
            <div class="stat-card">
                <div class="stat-icon">📊</div>
                <div class="stat-content">
                    <span class="stat-number" id="unique-campaigns">0</span>
                    <span class="stat-label">Active Campaigns</span>
                </div>
            </div>
        </div>

        <!-- Verification Notice -->
        <div id="verification-notice" class="notice notice-warning" style="display: none;">
            <strong>⚠️ Verification Pending</strong>
            <p>Your representative account is awaiting administrator verification. You'll be able to respond to emails once verified.</p>
        </div>

        <!-- Filters -->
        <div class="inbox-controls">
            <div class="filter-group">
                <label for="status-filter">Show:</label>
                <select id="status-filter">
                    <option value="">All Emails</option>
                    <option value="false" selected>Unanswered</option>
                    <option value="true">Answered</option>
                </select>
            </div>
            <button onclick="loadInbox()" class="btn btn-secondary">Refresh</button>
        </div>

        <!-- Loading -->
        <div id="loading" class="loading" style="display: none;">
            <div class="spinner"></div>
            <p>Loading inbox...</p>
        </div>

        <!-- Inbox Container -->
        <div id="inbox-container"></div>

        <!-- Load More -->
        <div id="load-more-container" style="display: none; text-align: center; margin-top: 2rem;">
            <button id="load-more-btn" class="btn btn-secondary">Load More</button>
        </div>

        <!-- Empty State -->
        <div id="empty-state" class="empty-state" style="display: none;">
            <p>No emails found. Constituents haven't contacted you through any campaigns yet.</p>
        </div>
    </div>

    <!-- Response Modal -->
    <div id="response-modal" class="modal" style="display: none;">
        <div class="modal-content modal-large">
            <span class="close" onclick="closeResponseModal()">&times;</span>
            <h2>Respond to Constituent Email</h2>
            
            <div id="original-email" class="original-email-box"></div>

            <form id="response-form">
                <input type="hidden" id="inbox-id">
                
                <div class="form-group">
                    <label for="response-type">Response Type</label>
                    <select id="response-type">
                        <option value="Email">Email Response</option>
                        <option value="Phone">Phone Call</option>
                        <option value="Meeting">In-Person Meeting</option>
                        <option value="Letter">Written Letter</option>
                    </select>
                </div>

                <div class="form-group">
                    <label for="response-text">Your Official Response *</label>
                    <textarea id="response-text" rows="8" required 
                              placeholder="Write your official response here..."></textarea>
                    <small class="text-muted">This will be publicly visible on the Response Wall with a "Verified by Representative" badge.</small>
                </div>

                <div class="form-group">
                    <label for="response-comment">Additional Context (Optional)</label>
                    <textarea id="response-comment" rows="3" 
                              placeholder="Add any additional context or notes about this response..."></textarea>
                </div>

                <div class="form-group">
                    <label for="response-screenshot">Attach Screenshot (Optional)</label>
                    <input type="file" id="response-screenshot" accept="image/*">
                    <small class="text-muted">Max 5MB. Attach proof of response if applicable.</small>
                </div>

                <div class="form-actions">
                    <button type="submit" class="btn btn-primary">Publish Response</button>
                    <button type="button" class="btn btn-secondary" onclick="closeResponseModal()">Cancel</button>
                </div>

                <p class="text-muted" style="margin-top: 1rem;">
                    ✓ This response will be automatically verified and published to the campaign's Response Wall.
                </p>
            </form>
        </div>
    </div>

    <script src="/js/api-client.js"></script>
    <script src="/js/representative-dashboard.js"></script>
</body>
</html>
```

#### File: `app/public/css/representative.css` (NEW FILE)

```css
/* Representative Dashboard Styles */

.header-actions {
    display: flex;
    gap: 1rem;
    margin-top: 1rem;
}

.rep-stats-grid {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
    gap: 1.5rem;
    margin-bottom: 2rem;
}

.stat-card {
    background: white;
    border: 1px solid #e1e8ed;
    border-radius: 8px;
    padding: 1.5rem;
    display: flex;
    align-items: center;
    gap: 1rem;
    transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.stat-card:hover {
    transform: translateY(-4px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.stat-icon {
    font-size: 2.5rem;
    opacity: 0.8;
}

.stat-content {
    display: flex;
    flex-direction: column;
}

.stat-number {
    font-size: 2rem;
    font-weight: bold;
    color: #2c3e50;
}

.stat-label {
    font-size: 0.9rem;
    color: #7f8c8d;
    margin-top: 0.25rem;
}

.notice {
    padding: 1rem 1.5rem;
    border-radius: 8px;
    margin-bottom: 2rem;
}

.notice-warning {
    background: #fff9e6;
    border: 1px solid #f39c12;
    color: #856404;
}

.inbox-controls {
    display: flex;
    gap: 1rem;
    margin-bottom: 2rem;
    align-items: center;
}

.inbox-item {
    background: white;
    border: 1px solid #e1e8ed;
    border-radius: 8px;
    padding: 1.5rem;
    margin-bottom: 1rem;
    transition: box-shadow 0.3s ease;
}

.inbox-item:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}

.inbox-item.unread {
    border-left: 4px solid #3498db;
    background: #f8f9ff;
}

.inbox-item.answered {
    opacity: 0.7;
}

.inbox-header {
    display: flex;
    justify-content: space-between;
    align-items: flex-start;
    margin-bottom: 1rem;
}

.inbox-campaign-info h3 {
    margin: 0 0 0.5rem 0;
    color: #2c3e50;
}

.inbox-campaign-info .campaign-meta {
    color: #7f8c8d;
    font-size: 0.9rem;
}

.inbox-badges {
    display: flex;
    gap: 0.5rem;
}

.inbox-body {
    background: #f8f9fa;
    padding: 1rem;
    border-radius: 4px;
    margin-bottom: 1rem;
    max-height: 150px;
    overflow: hidden;
    position: relative;
}

.inbox-body.expanded {
    max-height: none;
}

.inbox-body-fade {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    height: 50px;
    background: linear-gradient(transparent, #f8f9fa);
}

.inbox-footer {
    display: flex;
    justify-content: space-between;
    align-items: center;
}

.inbox-meta {
    color: #7f8c8d;
    font-size: 0.9rem;
}

.inbox-actions {
    display: flex;
    gap: 0.5rem;
}

.btn-respond {
    background: #27ae60;
    color: white;
}

.btn-respond:hover {
    background: #229954;
}

.btn-expand {
    background: transparent;
    border: 1px solid #3498db;
    color: #3498db;
}

.original-email-box {
    background: #f8f9fa;
    padding: 1.5rem;
    border-left: 4px solid #3498db;
    border-radius: 4px;
    margin-bottom: 2rem;
}

.original-email-box h4 {
    margin: 0 0 0.5rem 0;
    color: #2c3e50;
}

.original-email-box .email-meta {
    color: #7f8c8d;
    font-size: 0.9rem;
    margin-bottom: 1rem;
}

.original-email-box .email-body {
    color: #2c3e50;
    line-height: 1.6;
}

.modal-large {
    max-width: 800px;
}

.badge-answered {
    background: #27ae60;
    color: white;
}

/* Responsive */
@media (max-width: 768px) {
    .rep-stats-grid {
        grid-template-columns: 1fr;
    }

    .inbox-header {
        flex-direction: column;
        gap: 1rem;
    }

    .inbox-footer {
        flex-direction: column;
        align-items: flex-start;
        gap: 1rem;
    }

    .inbox-actions {
        width: 100%;
    }

    .inbox-actions button {
        flex: 1;
    }
}
```

#### File: `app/public/js/representative-dashboard.js` (NEW FILE)

```javascript
// Representative Dashboard JavaScript

let currentOffset = 0;
let currentFilter = 'false'; // Show unanswered by default
const LIMIT = 20;
let isVerified = false;

// Initialize
document.addEventListener('DOMContentLoaded', async () => {
    await checkVerificationStatus();
    loadStats();
    loadInbox();

    // Event listeners
    document.getElementById('status-filter').addEventListener('change', (e) => {
        currentFilter = e.target.value;
        currentOffset = 0;
        loadInbox(true);
    });

    document.getElementById('load-more-btn').addEventListener('click', loadMoreEmails);
    document.getElementById('response-form').addEventListener('submit', handleSubmitResponse);
});

// Check if representative is verified
async function checkVerificationStatus() {
    try {
        const response = await fetch('/api/auth/me', {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });

        const data = await response.json();

        if (data.success && data.user.user_type === 'representative') {
            isVerified = data.user.representative_verified;

            if (!isVerified) {
                document.getElementById('verification-notice').style.display = 'block';
            }
        } else {
            window.location.href = '/login.html';
        }
    } catch (error) {
        console.error('Error checking verification:', error);
        window.location.href = '/login.html';
    }
}

// Load inbox statistics
async function loadStats() {
    try {
        const response = await fetch('/api/representative/inbox/stats', {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });

        const data = await response.json();

        if (data.success) {
            document.getElementById('total-emails').textContent = data.stats.total_emails;
            document.getElementById('unanswered-emails').textContent = data.stats.unanswered;
            document.getElementById('answered-emails').textContent = data.stats.answered;
            document.getElementById('unique-campaigns').textContent = data.stats.unique_campaigns;
        }
    } catch (error) {
        console.error('Error loading stats:', error);
    }
}

// Load inbox emails
async function loadInbox(reset = false) {
    if (reset) {
        currentOffset = 0;
        document.getElementById('inbox-container').innerHTML = '';
    }

    showLoading(true);

    try {
        let url = `/api/representative/inbox?limit=${LIMIT}&offset=${currentOffset}`;

        if (currentFilter !== '') {
            url += `&has_response=${currentFilter}`;
        }

        const response = await fetch(url, {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });

        const data = await response.json();

        if (data.success) {
            if (data.emails.length === 0 && currentOffset === 0) {
                showEmptyState();
            } else {
                renderEmails(data.emails);

                const loadMoreContainer = document.getElementById('load-more-container');
                if (data.pagination.hasMore) {
                    loadMoreContainer.style.display = 'block';
                } else {
                    loadMoreContainer.style.display = 'none';
                }
            }
        } else {
            showError('Failed to load inbox');
        }
    } catch (error) {
        console.error('Error loading inbox:', error);
        showError('Failed to load inbox');
    } finally {
        showLoading(false);
    }
}

// Render inbox emails
function renderEmails(emails) {
    const container = document.getElementById('inbox-container');
    document.getElementById('empty-state').style.display = 'none';

    emails.forEach(email => {
        const item = createInboxItem(email);
        container.appendChild(item);
    });
}

// Create inbox item element
function createInboxItem(email) {
    const item = document.createElement('div');
    item.className = `inbox-item ${!email.has_response ? 'unread' : 'answered'}`;
    item.dataset.emailId = email.Id;

    const date = new Date(email.created_at);
    const formattedDate = date.toLocaleDateString('en-US', {
        year: 'numeric',
        month: 'short',
        day: 'numeric',
        hour: '2-digit',
        minute: '2-digit'
    });

    let badges = '';
    if (email.has_response) {
        badges = `<span class="badge badge-answered">✓ Answered</span>`;
    }

    // Truncate email body for preview
    const bodyPreview = email.email_body.length > 200 
        ? email.email_body.substring(0, 200) + '...' 
        : email.email_body;

    item.innerHTML = `
        <div class="inbox-header">
            <div class="inbox-campaign-info">
                <h3>${escapeHtml(email.campaign_title)}</h3>
                <div class="campaign-meta">
                    <span>From constituent in ${escapeHtml(email.sender_postal_code)}</span>
                </div>
            </div>
            <div class="inbox-badges">
                ${badges}
            </div>
        </div>

        <div class="inbox-body" id="body-${email.Id}">
            <strong>Subject:</strong> ${escapeHtml(email.email_subject)}<br><br>
            ${escapeHtml(bodyPreview)}
            ${email.email_body.length > 200 ? '<div class="inbox-body-fade"></div>' : ''}
        </div>

        <div class="inbox-footer">
            <div class="inbox-meta">
                Received ${formattedDate}
            </div>
            <div class="inbox-actions">
                ${email.email_body.length > 200 ? `
                    <button class="btn btn-expand" onclick="toggleEmailBody(${email.Id})">
                        Read Full Email
                    </button>
                ` : ''}
                ${!email.has_response && isVerified ? `
                    <button class="btn btn-respond" onclick="openResponseModal(${email.Id})">
                        Respond Publicly
                    </button>
                ` : ''}
                ${email.has_response ? `
                    <button class="btn btn-secondary" onclick="viewResponse(${email.response_id})">
                        View Response
                    </button>
                ` : ''}
            </div>
        </div>
    `;

    // Store full email data for later use
    item.dataset.emailData = JSON.stringify(email);

    return item;
}

// Toggle email body expansion
function toggleEmailBody(emailId) {
    const bodyElement = document.getElementById(`body-${emailId}`);
    const item = document.querySelector(`[data-email-id="${emailId}"]`);
    const email = JSON.parse(item.dataset.emailData);

    if (bodyElement.classList.contains('expanded')) {
        bodyElement.classList.remove('expanded');
        bodyElement.innerHTML = `
            <strong>Subject:</strong> ${escapeHtml(email.email_subject)}<br><br>
            ${escapeHtml(email.email_body.substring(0, 200))}...
            <div class="inbox-body-fade"></div>
        `;
    } else {
        bodyElement.classList.add('expanded');
        bodyElement.innerHTML = `
            <strong>Subject:</strong> ${escapeHtml(email.email_subject)}<br><br>
            ${escapeHtml(email.email_body)}
        `;
    }
}

// Open response modal
function openResponseModal(emailId) {
    const item = document.querySelector(`[data-email-id="${emailId}"]`);
    const email = JSON.parse(item.dataset.emailData);

    document.getElementById('inbox-id').value = email.Id;

    // Show original email in modal
    document.getElementById('original-email').innerHTML = `
        <h4>Original Email from Constituent</h4>
        <div class="email-meta">
            <strong>Campaign:</strong> ${escapeHtml(email.campaign_title)}<br>
            <strong>From:</strong> Constituent in ${escapeHtml(email.sender_postal_code)}<br>
            <strong>Subject:</strong> ${escapeHtml(email.email_subject)}<br>
            <strong>Date:</strong> ${new Date(email.created_at).toLocaleDateString()}
        </div>
        <div class="email-body">
            ${escapeHtml(email.email_body)}
        </div>
    `;

    document.getElementById('response-modal').style.display = 'block';
}

// Close response modal
function closeResponseModal() {
    document.getElementById('response-modal').style.display = 'none';
    document.getElementById('response-form').reset();
}

// Handle response submission
async function handleSubmitResponse(e) {
    e.preventDefault();

    const inboxId = document.getElementById('inbox-id').value;
    const formData = new FormData();

    formData.append('response_type', document.getElementById('response-type').value);
    formData.append('response_text', document.getElementById('response-text').value.trim());
    formData.append('user_comment', document.getElementById('response-comment').value.trim());

    const screenshotFile = document.getElementById('response-screenshot').files[0];
    if (screenshotFile) {
        formData.append('screenshot', screenshotFile);
    }

    try {
        const submitButton = e.target.querySelector('button[type="submit"]');
        submitButton.disabled = true;
        submitButton.textContent = 'Publishing...';

        const response = await fetch(`/api/representative/respond/${inboxId}`, {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            },
            body: formData
        });

        const data = await response.json();

        if (data.success) {
            showSuccess('Response published successfully! It now appears on the Response Wall.');
            closeResponseModal();
            loadStats();
            currentOffset = 0;
            loadInbox(true);
        } else {
            showError(data.error || 'Failed to publish response');
        }
    } catch (error) {
        console.error('Error submitting response:', error);
        showError('Failed to publish response');
    } finally {
        const submitButton = e.target.querySelector('button[type="submit"]');
        submitButton.disabled = false;
        submitButton.textContent = 'Publish Response';
    }
}

// View existing response
function viewResponse(responseId) {
    // Could open response in modal or redirect to response wall
    window.open(`/response-wall.html?response=${responseId}`, '_blank');
}

// Load more emails
function loadMoreEmails() {
    currentOffset += LIMIT;
    loadInbox(false);
}

// Utility functions
function showLoading(show) {
    document.getElementById('loading').style.display = show ? 'block' : 'none';
}

function showEmptyState() {
    document.getElementById('empty-state').style.display = 'block';
    document.getElementById('inbox-container').innerHTML = '';
}

function showError(message) {
    alert(message); // Could be improved with toast notifications
}

function showSuccess(message) {
    alert(message); // Could be improved with toast notifications
}

function escapeHtml(text) {
    const div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

function logout() {
    localStorage.removeItem('token');
    window.location.href = '/login.html';
}

// Close modal when clicking outside
window.onclick = function(event) {
    const modal = document.getElementById('response-modal');
    if (event.target === modal) {
        closeResponseModal();
    }
};
```

#### File: `app/public/css/response-wall.css` (UPDATE)

Add representative badge styling:

```css
/* Add to existing response-wall.css */

.badge-verified-rep {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    padding: 0.25rem 0.75rem;
    border-radius: 12px;
    font-size: 0.85rem;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.badge-verified-rep::before {
    content: "✓";
    font-weight: bold;
}

.response-card.verified-by-rep {
    border: 2px solid #667eea;
    background: linear-gradient(to bottom, #f8f9ff 0%, white 100%);
}

.response-card.verified-by-rep .response-header {
    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    color: white;
    margin: -1.5rem -1.5rem 1rem -1.5rem;
    padding: 1.5rem;
    border-radius: 8px 8px 0 0;
}

.response-card.verified-by-rep .response-header h3,
.response-card.verified-by-rep .response-header .rep-meta {
    color: white;
}

.response-card.verified-by-rep .response-header .rep-meta {
    opacity: 0.9;
}

.rep-profile-link {
    color: #667eea;
    text-decoration: none;
    font-weight: 600;
    display: inline-flex;
    align-items: center;
    gap: 0.25rem;
}

.rep-profile-link:hover {
    text-decoration: underline;
}
```

#### File: `app/public/js/response-wall.js` (UPDATE)

Update response card rendering to show representative badge:

```javascript
// Update the createResponseCard function to detect representative responses

function createResponseCard(response) {
    const card = document.createElement('div');
    
    // Add special class for representative responses
    card.className = response.posted_by_representative 
        ? 'response-card verified-by-rep' 
        : 'response-card';
    
    card.dataset.responseId = response.Id;

    // ... existing code ...

    // Build badges - add representative badge first if applicable
    let badges = '';
    if (response.posted_by_representative) {
        badges = `<span class="badge badge-verified-rep">Official Response</span>`;
    }
    if (response.verified && !response.posted_by_representative) {
        badges += `<span class="badge badge-verified">✓ Verified</span>`;
    }
    badges += `<span class="badge badge-level">${response.representative_level}</span>`;
    badges += `<span class="badge badge-type">${response.response_type}</span>`;

    // ... rest of existing code ...
}
```

---

### Admin Panel Updates

#### File: `app/public/admin.html` (UPDATE)

Add representative verification section:

```html
<!-- Add this new section after responses section -->
<section id="rep-verification-section" style="display: none;">
    <h2>Representative Verification</h2>
    
    <div class="admin-filters">
        <select id="rep-verification-status">
            <option value="pending">Pending Verification</option>
            <option value="verified">Verified</option>
            <option value="all">All Representatives</option>
        </select>
        
        <button onclick="loadRepresentatives()" class="btn btn-secondary">Refresh</button>
    </div>

    <div id="admin-representatives-container"></div>
</section>
```

#### File: `app/public/js/admin.js` (UPDATE)

Add representative verification functions:

```javascript
// Add these functions to existing admin.js

async function loadRepresentatives() {
    const status = document.getElementById('rep-verification-status').value;
    
    try {
        const response = await fetch(`/api/admin/representatives?status=${status}`, {
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`
            }
        });
        
        const data = await response.json();
        
        if (data.success) {
            renderRepresentatives(data.representatives);
        }
    } catch (error) {
        console.error('Error loading representatives:', error);
    }
}

function renderRepresentatives(reps) {
    const container = document.getElementById('admin-representatives-container');
    
    if (reps.length === 0) {
        container.innerHTML = '<p>No representatives found.</p>';
        return;
    }
    
    container.innerHTML = reps.map(rep => `
        <div class="admin-rep-card">
            <h4>${rep.representative_name} (${rep.representative_level})</h4>
            <p><strong>District:</strong> ${rep.representative_district}</p>
            <p><strong>Email:</strong> ${rep.email}</p>
            <p><strong>Status:</strong> ${rep.representative_verified ? 'Verified' : 'Pending Verification'}</p>
            ${rep.verification_document ? `
                <p><a href="${rep.verification_document}" target="_blank">View Verification Document</a></p>
            ` : ''}
            
            <div class="admin-actions">
                ${!rep.representative_verified ? `
                    <button onclick="verifyRepresentative(${rep.Id})" class="btn btn-success">
                        Verify Representative
                    </button>
                ` : `
                    <button onclick="unverifyRepresentative(${rep.Id})" class="btn btn-danger">
                        Revoke Verification
                    </button>
                `}
            </div>
        </div>
    `).join('');
}

async function verifyRepresentative(id) {
    if (!confirm('Verify this representative account? They will be able to post official responses.')) {
        return;
    }
    
    try {
        const response = await fetch(`/api/admin/representatives/${id}/verify`, {
            method: 'PATCH',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ verified: true })
        });
        
        const data = await response.json();
        
        if (data.success) {
            showSuccess('Representative verified successfully');
            loadRepresentatives();
        }
    } catch (error) {
        console.error('Error verifying representative:', error);
    }
}

async function unverifyRepresentative(id) {
    if (!confirm('Revoke verification for this representative?')) {
        return;
    }
    
    try {
        const response = await fetch(`/api/admin/representatives/${id}/verify`, {
            method: 'PATCH',
            headers: {
                'Authorization': `Bearer ${localStorage.getItem('token')}`,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify({ verified: false })
        });
        
        const data = await response.json();
        
        if (data.success) {
            showSuccess('Verification revoked');
            loadRepresentatives();
        }
    } catch (error) {
        console.error('Error revoking verification:', error);
    }
}
```

#### File: `app/controllers/representativeController.js` (UPDATE)

Add admin endpoints:

```javascript
// Add these admin functions to representative controller

async function getRepresentativesForAdmin(req, res) {
    try {
        const { status = 'pending' } = req.query;
        
        let whereClause = `(user_type,eq,representative)`;
        
        if (status === 'pending') {
            whereClause += `~and(representative_verified,eq,false)`;
        } else if (status === 'verified') {
            whereClause += `~and(representative_verified,eq,true)`;
        }
        
        const reps = await nocodbService.getRecords('users', {
            where: whereClause,
            sort: '-created_at'
        });
        
        res.json({ success: true, representatives: reps });
    } catch (error) {
        console.error('Error fetching representatives:', error);
        res.status(500).json({ error: 'Failed to fetch representatives' });
    }
}

async function verifyRepresentative(req, res) {
    try {
        const { id } = req.params;
        const { verified } = req.body;
        
        await nocodbService.updateRecord('users', id, {
            representative_verified: verified
        });
        
        // Could send email notification here
        
        res.json({ success: true, message: `Representative ${verified ? 'verified' : 'unverified'} successfully` });
    } catch (error) {
        console.error('Error updating representative verification:', error);
        res.status(500).json({ error: 'Failed to update verification' });
    }
}

// Export these
module.exports = {
    // ... existing exports
    getRepresentativesForAdmin,
    verifyRepresentative
};
```

#### File: `app/routes/api.js` (UPDATE)

Add admin representative routes:

```javascript
// Add these admin routes
router.get('/admin/representatives', requireAdmin, representativeController.getRepresentativesForAdmin);
router.patch('/admin/representatives/:id/verify', requireAdmin, representativeController.verifyRepresentative);
```

---

### Testing Checklist

#### Representative Features
- [ ] Representative can register account
- [ ] Representative can request verification
- [ ] Admin can verify/unverify representatives
- [ ] Representative dashboard loads correctly
- [ ] Inbox shows emails sent to representative
- [ ] Representative can respond to emails
- [ ] Responses get "Official Response" badge
- [ ] Responses are auto-verified and auto-approved
- [ ] Inbox tracking works (has_response flag updates)
- [ ] Stats display correctly

#### Integration
- [ ] Regular users see representative responses with special badge
- [ ] Representative responses appear at top when sorted
- [ ] Profile links work
- [ ] Representative bio displays
- [ ] Verification status prevents unverified reps from responding

---

## Future Enhancements (Post-MVP)

1. **Representative Scorecards**: Aggregate response data to show which reps are most responsive
2. **Email Notifications**: Notify users when their response is approved
3. **Response Templates**: Let users use parts of responses as email templates
4. **Search Functionality**: Search responses by keyword
5. **Response Analytics**: Track response sentiment, common themes
6. **API Rate Limiting**: Add specific rate limits for response submission
7. **Spam Detection**: Automated spam/abuse detection
8. **Response Categories**: Tag responses (Positive, Negative, Non-Answer, etc.)
9. **Representative Notifications**: Email reps when they receive new constituent emails
10. **Response Drafts**: Let representatives save draft responses
11. **Bulk Response**: Allow representatives to respond to multiple similar emails at once
12. **Representative Analytics**: Show reps their response rate and engagement metrics

---

## Notes for Implementation Agent

- Follow existing code style and patterns from the project
- Use existing error handling and validation patterns
- Integrate with existing authentication system
- Match existing CSS design patterns
- Test thoroughly before marking complete
- Update README.md with new feature documentation
- Add comments for complex logic

Good luck! 🚀
