# Speaker Sessions Manager — Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a Speaker Sessions manager to career fair events — admin creates/manages session schedules, employers view and update their speaker details via portal, admin can email the full schedule to all participants.

**Architecture:** Two new `career_center` tables (`event_sessions`, `event_session_speakers`) following existing event data patterns. New "Sessions" tab in `event_sponsor_manager.cgi` (admin) and `portal.cgi` (employer). Shared CRUD functions in `events.pl`. Email via `smtp_notify` using the standard HBCU email template.

**Tech Stack:** Perl 5 CGI, MySQL (career_center on newdb.hbcuconnect.com), Tailwind CSS (CDN), vanilla JavaScript (AJAX/fetch), ImageMagick (headshot processing)

**Design doc:** `docs/plans/2026-03-04-speaker-sessions-manager-design.md`

---

### Task 1: Create Database Tables

**Files:**
- Create: `events/sql/create_session_tables.sql` (reference script, not auto-run)

**Step 1: Create the SQL file with both table definitions**

```sql
-- Speaker Sessions Manager tables
-- Run against career_center database on newdb.hbcuconnect.com

CREATE TABLE IF NOT EXISTS event_sessions (
    session_id    INT AUTO_INCREMENT PRIMARY KEY,
    company_id    INT NOT NULL COMMENT 'FK to hbcu_central.resume_recommendation_clients',
    session_title VARCHAR(255) NOT NULL,
    start_time    TIME NOT NULL,
    end_time      TIME NOT NULL,
    session_url   VARCHAR(500) DEFAULT NULL COMMENT 'Zoom/meeting link',
    description   TEXT DEFAULT NULL,
    sort_order    INT DEFAULT 0,
    active        TINYINT(1) DEFAULT 1,
    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
    created_by    VARCHAR(50) DEFAULT NULL,
    INDEX idx_company (company_id),
    INDEX idx_active (company_id, active)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS event_session_speakers (
    speaker_id    INT AUTO_INCREMENT PRIMARY KEY,
    session_id    INT NOT NULL COMMENT 'FK to event_sessions',
    employer_id   INT NOT NULL COMMENT 'FK to career_center.employer_data',
    speaker_name  VARCHAR(255) NOT NULL,
    speaker_email VARCHAR(255) DEFAULT NULL,
    speaker_title VARCHAR(255) DEFAULT NULL COMMENT 'Job title',
    speaker_bio   TEXT DEFAULT NULL,
    headshot_url  VARCHAR(500) DEFAULT NULL,
    headshot_path VARCHAR(500) DEFAULT NULL,
    confirmed     TINYINT(1) DEFAULT 0 COMMENT '0=pending, 1=confirmed, 2=declined',
    confirmed_date DATETIME DEFAULT NULL,
    active        TINYINT(1) DEFAULT 1,
    created_at    DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_session (session_id),
    INDEX idx_employer (employer_id),
    INDEX idx_session_active (session_id, active)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
```

**Step 2: Run the SQL against career_center**

```bash
mysql -h newdb.hbcuconnect.com -u root -p career_center < events/sql/create_session_tables.sql
```

**Step 3: Verify tables were created**

```bash
mysql -h newdb.hbcuconnect.com -u root -p career_center -e "DESCRIBE event_sessions; DESCRIBE event_session_speakers;"
```

**Step 4: Fix file ownership**

```bash
chown -R hbcuconnect:psacln events/sql/
```

**Step 5: Commit**

```bash
git add events/sql/create_session_tables.sql
git commit -m "feat: add event_sessions and event_session_speakers tables for career fair session management"
```

---

### Task 2: Add CRUD Functions to events.pl

**Files:**
- Modify: `events/events.pl` (add new functions after the existing `deleteEventDocument` at ~line 732)

**Step 1: Add `getEventSessions($company_id)`**

Add after the `deleteEventDocument` function (around line 732). This function retrieves all active sessions for an event, with their speakers joined in.

```perl
###############################################################################################################
#### Speaker Session Functions
###############################################################################################################

sub getEventSessions {
    my ($company_id) = @_;

    my $ccdbh = getDBHandle('career_center');

    my $sth = $ccdbh->prepare(qq~
        SELECT s.*, sp.speaker_id, sp.employer_id, sp.speaker_name, sp.speaker_email,
               sp.speaker_title, sp.speaker_bio, sp.headshot_url, sp.headshot_path,
               sp.confirmed, sp.confirmed_date,
               ed.company_name
        FROM event_sessions s
        LEFT JOIN event_session_speakers sp ON s.session_id = sp.session_id AND sp.active = 1
        LEFT JOIN employer_data ed ON sp.employer_id = ed.employer_id
        WHERE s.company_id = ? AND s.active = 1
        ORDER BY s.start_time, s.sort_order, s.session_id, sp.speaker_id
    ~);
    $sth->execute($company_id);

    # Group speakers under their sessions
    my @sessions;
    my %session_map;
    while (my $row = $sth->fetchrow_hashref) {
        my $sid = $row->{session_id};
        unless ($session_map{$sid}) {
            $session_map{$sid} = {
                session_id    => $row->{session_id},
                company_id    => $row->{company_id},
                session_title => $row->{session_title},
                start_time    => $row->{start_time},
                end_time      => $row->{end_time},
                session_url   => $row->{session_url},
                description   => $row->{description},
                sort_order    => $row->{sort_order},
                created_at    => $row->{created_at},
                created_by    => $row->{created_by},
                speakers      => [],
            };
            push @sessions, $session_map{$sid};
        }
        if ($row->{speaker_id}) {
            push @{$session_map{$sid}{speakers}}, {
                speaker_id    => $row->{speaker_id},
                employer_id   => $row->{employer_id},
                company_name  => $row->{company_name},
                speaker_name  => $row->{speaker_name},
                speaker_email => $row->{speaker_email},
                speaker_title => $row->{speaker_title},
                speaker_bio   => $row->{speaker_bio},
                headshot_url  => $row->{headshot_url},
                headshot_path => $row->{headshot_path},
                confirmed     => $row->{confirmed},
                confirmed_date => $row->{confirmed_date},
            };
        }
    }

    return @sessions;
}
```

**Step 2: Add `getEmployerSessions($company_id, $employer_id)`**

```perl
###############################################################################################################
sub getEmployerSessions {
    my ($company_id, $employer_id) = @_;

    my $ccdbh = getDBHandle('career_center');

    my $sth = $ccdbh->prepare(qq~
        SELECT s.*, sp.speaker_id, sp.employer_id, sp.speaker_name, sp.speaker_email,
               sp.speaker_title, sp.speaker_bio, sp.headshot_url, sp.headshot_path,
               sp.confirmed, sp.confirmed_date,
               ed.company_name
        FROM event_sessions s
        JOIN event_session_speakers sp ON s.session_id = sp.session_id AND sp.active = 1
        LEFT JOIN employer_data ed ON sp.employer_id = ed.employer_id
        WHERE s.company_id = ? AND sp.employer_id = ? AND s.active = 1
        ORDER BY s.start_time, s.sort_order
    ~);
    $sth->execute($company_id, $employer_id);

    my @sessions;
    my %session_map;
    while (my $row = $sth->fetchrow_hashref) {
        my $sid = $row->{session_id};
        unless ($session_map{$sid}) {
            $session_map{$sid} = {
                session_id    => $row->{session_id},
                session_title => $row->{session_title},
                start_time    => $row->{start_time},
                end_time      => $row->{end_time},
                session_url   => $row->{session_url},
                description   => $row->{description},
                speakers      => [],
            };
            push @sessions, $session_map{$sid};
        }
        push @{$session_map{$sid}{speakers}}, {
            speaker_id    => $row->{speaker_id},
            employer_id   => $row->{employer_id},
            company_name  => $row->{company_name},
            speaker_name  => $row->{speaker_name},
            speaker_email => $row->{speaker_email},
            speaker_title => $row->{speaker_title},
            speaker_bio   => $row->{speaker_bio},
            headshot_url  => $row->{headshot_url},
            confirmed     => $row->{confirmed},
        };
    }

    return @sessions;
}
```

**Step 3: Add `saveEventSession($data)` and `deleteEventSession($session_id)`**

```perl
###############################################################################################################
sub saveEventSession {
    my ($data) = @_;

    my $ccdbh = getDBHandle('career_center');

    if ($data->{session_id}) {
        my $sth = $ccdbh->prepare(qq~
            UPDATE event_sessions
            SET session_title=?, start_time=?, end_time=?, session_url=?, description=?, sort_order=?
            WHERE session_id=?
        ~);
        $sth->execute(
            $data->{session_title}, $data->{start_time}, $data->{end_time},
            $data->{session_url}, $data->{description}, $data->{sort_order} || 0,
            $data->{session_id}
        );
        return $data->{session_id};
    } else {
        my $sth = $ccdbh->prepare(qq~
            INSERT INTO event_sessions (company_id, session_title, start_time, end_time, session_url, description, sort_order, created_by)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?)
        ~);
        $sth->execute(
            $data->{company_id}, $data->{session_title}, $data->{start_time}, $data->{end_time},
            $data->{session_url}, $data->{description}, $data->{sort_order} || 0, $data->{created_by}
        );
        return $ccdbh->{mysql_insertid};
    }
}

###############################################################################################################
sub deleteEventSession {
    my ($session_id) = @_;

    my $ccdbh = getDBHandle('career_center');
    # Soft delete session and its speakers
    $ccdbh->do('UPDATE event_sessions SET active=0 WHERE session_id=?', undef, $session_id);
    $ccdbh->do('UPDATE event_session_speakers SET active=0 WHERE session_id=?', undef, $session_id);

    return 1;
}
```

**Step 4: Add `saveSessionSpeaker($data)`, `deleteSessionSpeaker($speaker_id)`, `updateSpeakerHeadshot($speaker_id, $url, $path)`**

```perl
###############################################################################################################
sub saveSessionSpeaker {
    my ($data) = @_;

    my $ccdbh = getDBHandle('career_center');

    if ($data->{speaker_id}) {
        my $sql = qq~
            UPDATE event_session_speakers
            SET speaker_name=?, speaker_email=?, speaker_title=?, speaker_bio=?, employer_id=?
        ~;
        my @bind = (
            $data->{speaker_name}, $data->{speaker_email}, $data->{speaker_title},
            $data->{speaker_bio}, $data->{employer_id},
        );
        if (exists $data->{headshot_url}) {
            $sql .= ', headshot_url=?, headshot_path=?';
            push @bind, $data->{headshot_url}, $data->{headshot_path};
        }
        if (exists $data->{confirmed}) {
            $sql .= ', confirmed=?, confirmed_date=NOW()';
            push @bind, $data->{confirmed};
        }
        $sql .= ' WHERE speaker_id=?';
        push @bind, $data->{speaker_id};

        my $sth = $ccdbh->prepare($sql);
        $sth->execute(@bind);
        return $data->{speaker_id};
    } else {
        my $sth = $ccdbh->prepare(qq~
            INSERT INTO event_session_speakers (session_id, employer_id, speaker_name, speaker_email, speaker_title, speaker_bio)
            VALUES (?, ?, ?, ?, ?, ?)
        ~);
        $sth->execute(
            $data->{session_id}, $data->{employer_id},
            $data->{speaker_name}, $data->{speaker_email}, $data->{speaker_title}, $data->{speaker_bio}
        );
        return $ccdbh->{mysql_insertid};
    }
}

###############################################################################################################
sub deleteSessionSpeaker {
    my ($speaker_id) = @_;

    my $ccdbh = getDBHandle('career_center');
    $ccdbh->do('UPDATE event_session_speakers SET active=0 WHERE speaker_id=?', undef, $speaker_id);

    return 1;
}

###############################################################################################################
sub updateSpeakerHeadshot {
    my ($speaker_id, $url, $path) = @_;

    my $ccdbh = getDBHandle('career_center');
    $ccdbh->do('UPDATE event_session_speakers SET headshot_url=?, headshot_path=? WHERE speaker_id=?',
               undef, $url, $path, $speaker_id);

    return 1;
}
```

**Step 5: Add `formatSessionTime($time_value)` helper**

```perl
###############################################################################################################
sub formatSessionTime {
    my ($raw) = @_;
    return '' unless $raw && $raw =~ /^(\d{2}):(\d{2})/;
    my ($h, $m) = ($1, $2);
    my $ampm = $h >= 12 ? 'PM' : 'AM';
    $h = $h % 12 || 12;
    return "$h:$m $ampm";
}
```

**Step 6: Syntax check**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c events.pl
```

**Step 7: Fix file ownership**

```bash
chown hbcuconnect:psacln events/events.pl
```

**Step 8: Commit**

```bash
git add events/events.pl
git commit -m "feat: add session and speaker CRUD functions to events.pl"
```

---

### Task 3: Add "Sessions" Tab to Admin Sub-Navigation

**Files:**
- Modify: `events/events.pl` — `printEventSubNav()` function (line ~1276)

**Step 1: Add sessions tab to the `@tabs` array**

In `printEventSubNav()`, add the sessions tab between `booth_reps` and `email_blast` in the `@tabs` array:

```perl
{ id => 'sessions',  href => "event_sponsor_manager.cgi?show_sessions=1&cid=$company_id", label => 'Sessions',    icon => 'M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z' },
```

The calendar icon (`M8 7V3m8 4V3...`) represents a schedule/calendar which fits sessions.

**Step 2: Syntax check and fix ownership**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c events.pl
chown hbcuconnect:psacln events/events.pl
```

**Step 3: Commit**

```bash
git add events/events.pl
git commit -m "feat: add Sessions tab to event sub-navigation"
```

---

### Task 4: Add Sessions Tab Route + AJAX Actions to event_sponsor_manager.cgi

**Files:**
- Modify: `events/event_sponsor_manager.cgi`

**Step 1: Add `show_sessions` route to the main routing block (around line 39-52)**

Add after the `elsif ($q->param('show_attendees'))` line (around line 46):

```perl
} elsif ($q->param('show_sessions')) {
    showSessionsManager($q->param('cid'));
```

**Step 2: Add AJAX action handlers to `handleAjaxAction()` (add before the final closing brace)**

Add these new `elsif` branches to the `handleAjaxAction()` function:

```perl
    } elsif ($action eq 'save_session') {
        my $data = {
            session_id    => $q->param('session_id') || undef,
            company_id    => $q->param('company_id'),
            session_title => $q->param('session_title'),
            start_time    => $q->param('start_time'),
            end_time      => $q->param('end_time'),
            session_url   => $q->param('session_url'),
            description   => $q->param('description'),
            sort_order    => $q->param('sort_order') || 0,
            created_by    => $ENV{REMOTE_USER} || 'admin',
        };
        my $session_id = saveEventSession($data);
        # Save primary speaker if provided
        if ($q->param('speaker_name')) {
            my $sp_data = {
                session_id    => $session_id,
                speaker_id    => $q->param('speaker_id') || undef,
                employer_id   => $q->param('employer_id'),
                speaker_name  => $q->param('speaker_name'),
                speaker_email => $q->param('speaker_email'),
                speaker_title => $q->param('speaker_title'),
                speaker_bio   => $q->param('speaker_bio'),
            };
            saveSessionSpeaker($sp_data);
        }
        my $action_word = $q->param('session_id') ? 'Updated' : 'Added';
        &logEvent($q->param('employer_id') || 0, $ENV{REMOTE_USER} || 'admin', "Event: Session", "Event: $action_word session: " . $q->param('session_title'), $ccdbh, 0);
        print qq~{"success":true,"session_id":$session_id}~;

    } elsif ($action eq 'delete_session') {
        my $session_id = $q->param('session_id');
        deleteEventSession($session_id);
        &logEvent(0, $ENV{REMOTE_USER} || 'admin', "Event: Session", "Event: Deleted session #$session_id", $ccdbh, 0);
        print '{"success":true}';

    } elsif ($action eq 'add_speaker') {
        my $data = {
            session_id    => $q->param('session_id'),
            employer_id   => $q->param('employer_id'),
            speaker_name  => $q->param('speaker_name'),
            speaker_email => $q->param('speaker_email'),
            speaker_title => $q->param('speaker_title'),
            speaker_bio   => $q->param('speaker_bio'),
        };
        my $speaker_id = saveSessionSpeaker($data);

        # Process headshot upload if provided
        my $upload_fh = $q->upload('speaker_headshot');
        if ($upload_fh && $speaker_id) {
            _processSpeakerHeadshot($speaker_id, $q->param('company_id'), $q->param('employer_id'), $upload_fh);
        }

        &logEvent($q->param('employer_id') || 0, $ENV{REMOTE_USER} || 'admin', "Event: Speaker", "Event: Added speaker " . $q->param('speaker_name'), $ccdbh, 0);
        print qq~{"success":true,"speaker_id":$speaker_id}~;

    } elsif ($action eq 'update_speaker') {
        my $data = {
            speaker_id    => $q->param('speaker_id'),
            employer_id   => $q->param('employer_id'),
            speaker_name  => $q->param('speaker_name'),
            speaker_email => $q->param('speaker_email'),
            speaker_title => $q->param('speaker_title'),
            speaker_bio   => $q->param('speaker_bio'),
        };
        if (defined $q->param('confirmed')) {
            $data->{confirmed} = $q->param('confirmed');
        }
        saveSessionSpeaker($data);

        # Process headshot upload if provided
        my $upload_fh = $q->upload('speaker_headshot');
        if ($upload_fh) {
            _processSpeakerHeadshot($q->param('speaker_id'), $q->param('company_id'), $q->param('employer_id'), $upload_fh);
        }

        &logEvent($q->param('employer_id') || 0, $ENV{REMOTE_USER} || 'admin', "Event: Speaker", "Event: Updated speaker " . $q->param('speaker_name'), $ccdbh, 0);
        print '{"success":true}';

    } elsif ($action eq 'delete_speaker') {
        my $speaker_id = $q->param('speaker_id');
        deleteSessionSpeaker($speaker_id);
        &logEvent(0, $ENV{REMOTE_USER} || 'admin', "Event: Speaker", "Event: Deleted speaker #$speaker_id", $ccdbh, 0);
        print '{"success":true}';

    } elsif ($action eq 'confirm_speaker') {
        my $speaker_id = $q->param('speaker_id');
        my $status     = $q->param('status') || 1;  # 1=confirmed, 2=declined
        my $ccdbh = getDBHandle('career_center');
        $ccdbh->do('UPDATE event_session_speakers SET confirmed=?, confirmed_date=NOW() WHERE speaker_id=?', undef, $status, $speaker_id);
        my @statuses = ('Pending', 'Confirmed', 'Declined');
        &logEvent(0, $ENV{REMOTE_USER} || 'admin', "Event: Speaker", "Event: Marked speaker #$speaker_id as $statuses[$status]", $ccdbh, 0);
        print '{"success":true}';

    } elsif ($action eq 'email_schedule') {
        emailScheduleToEmployers();

    } elsif ($action eq 'send_speaker_reminder') {
        sendSpeakerReminder();
```

**Step 3: Add `_processSpeakerHeadshot()` helper function**

```perl
sub _processSpeakerHeadshot {
    my ($speaker_id, $company_id, $employer_id, $upload_fh) = @_;

    my $tmpfile  = $q->tmpFileName($upload_fh);
    my $original = "$upload_fh";
    my ($ext) = ($original =~ /\.(jpe?g|png)$/i);
    return unless $ext && -s $tmpfile && -s $tmpfile < 5 * 1024 * 1024;

    $ext = lc($ext);
    $ext = 'jpg' if $ext eq 'jpeg';

    my $dir = "$CONF->{WWW_ROOT}/events/assets/$company_id/speakers";
    system("mkdir", "-p", $dir) unless -d $dir;

    my $fname = "headshot_${speaker_id}.$ext";
    my $thumb = "headshot_${speaker_id}_thumb.$ext";
    my $fpath = "$dir/$fname";
    my $tpath = "$dir/$thumb";

    if (File::Copy::copy($tmpfile, $fpath)) {
        chmod 0644, $fpath;
        system($CONF->{CONVERT}, '-resize', '600x600>', '-quality', '90', $fpath, $fpath);
        system($CONF->{CONVERT}, '-thumbnail', '150x150^', '-gravity', 'center', '-extent', '150x150', $fpath, $tpath);
        chmod 0644, $tpath if -f $tpath;
        my $headshot_url = "https://hbcuconnect.com/events/assets/$company_id/speakers/$fname";
        updateSpeakerHeadshot($speaker_id, $headshot_url, $fpath);
    }
}
```

**Step 4: Syntax check and fix ownership**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c event_sponsor_manager.cgi
chown hbcuconnect:psacln event_sponsor_manager.cgi
```

**Step 5: Commit**

```bash
git add events/event_sponsor_manager.cgi
git commit -m "feat: add session/speaker AJAX actions and routing to event_sponsor_manager.cgi"
```

---

### Task 5: Build Admin Sessions UI (showSessionsManager)

**Files:**
- Modify: `events/event_sponsor_manager.cgi` — add `showSessionsManager()` function

**Step 1: Add the full `showSessionsManager()` function**

This renders the Sessions tab in the admin interface. It shows a table of sessions with speakers, and provides an Add/Edit modal with company dropdown. The UI follows the same Tailwind + adminUI pattern as the rest of event_sponsor_manager.cgi.

Key elements:
- Session table with columns: Time, Title, Company, Speaker(s), Status, Zoom Link, Actions
- "Add Session" button opens a modal form
- Company dropdown populated from `getParticipatingEmployers()`
- Each session row has Edit/Delete buttons
- Each speaker has Confirm/Decline/Remind buttons
- "Add Speaker" button on each session for multi-speaker support
- "Email Schedule to All" bulk action button
- Modal form fields: session_title, start_time, end_time, session_url, description, employer_id (dropdown), speaker_name, speaker_email, speaker_title, speaker_bio, headshot upload

The function should:
1. Call `getEvent($company_id)` and `getEventSessions($company_id)`
2. Call `getParticipatingEmployers($company_id)` for the company dropdown
3. Print admin header via `printAdminHeader()` / `printAdminNav()` / `printEventSubNav()`
4. Render the sessions table with inline Tailwind styles
5. Include JavaScript for AJAX save/delete/confirm operations
6. Include the Add/Edit Session modal HTML

Follow the exact same pattern as `showParticipatingEmployers()` and `manageEmployerDetails()` for layout, modal structure, and AJAX fetch patterns.

**Important implementation notes:**
- Time inputs: use `<input type="time">` for start_time/end_time
- Company dropdown: `<select>` with `<option value="$emp->{employer_id}">$emp->{company_name}</option>` for each participating employer
- Confirmed badge: green "Confirmed" / yellow "Pending" / red "Declined" using Tailwind badge classes
- Session URL: show as clickable link icon if present
- The modal should have an "Add Another Speaker" section that appears after saving the primary speaker (second AJAX call)
- Escape `@` signs and `$` in JS regex within `qq~` strings

**Step 2: Syntax check and fix ownership**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c event_sponsor_manager.cgi
chown hbcuconnect:psacln event_sponsor_manager.cgi
```

**Step 3: Commit**

```bash
git add events/event_sponsor_manager.cgi
git commit -m "feat: add admin Sessions tab UI with session/speaker management"
```

---

### Task 6: Build Email Schedule Feature

**Files:**
- Modify: `events/event_sponsor_manager.cgi` — add `emailScheduleToEmployers()` and `sendSpeakerReminder()` functions
- Reference: `memory/email_template.md` for the email template skeleton

**Step 1: Add `emailScheduleToEmployers()` function**

This handles the `email_schedule` AJAX action. It:
1. Gets the event and all sessions via `getEventSessions($company_id)`
2. If `$q->param('employer_id')` is set, emails just that employer; otherwise emails all participating employers
3. Builds a schedule HTML email using the standard HBCU email template (600px card, inline CSS, no `<style>` blocks)
4. Email content: event name, date, then a table of sessions with Time, Title, Company, Speaker(s), Zoom Link
5. CTA button: "View Event Portal" linking to the portal URL
6. Sends via `smtp_notify()` from `niceMail.pl`

**Important email template rules** (from `memory/email_template.md`):
- ALL CSS must be inlined — no `<style>` blocks
- Use `q{}` quoting to avoid interpolation issues, or escape `@` as `\@` in `qq~`
- Use HTML entities for special chars (`&#8217;` for apostrophe, etc.)
- Never use `linear-gradient` or `padding` on `<a>` tags
- 600px fixed-width centered card layout
- Dark navy header (#1a1a2e) with logo, maroon accent bar (#993300)

```perl
sub emailScheduleToEmployers {
    my $company_id  = $q->param('company_id');
    my $employer_id = $q->param('employer_id');  # optional — send to one vs all

    my $event    = getEvent($company_id);
    my @sessions = getEventSessions($company_id);
    my @employers = getParticipatingEmployers($company_id);

    # Filter to single employer if specified
    if ($employer_id) {
        @employers = grep { $_->{employer_id} == $employer_id } @employers;
    }

    my $event_name = $event->{company_name} || "Career Fair";
    my $event_date = formatEventDate($event->{event_date}) || 'TBD';

    # Build session rows HTML
    my $session_rows = '';
    for my $sess (@sessions) {
        my $start = formatSessionTime($sess->{start_time});
        my $end   = formatSessionTime($sess->{end_time});
        my $speakers_html = join(', ', map { "$_->{speaker_name} ($_->{company_name})" } @{$sess->{speakers}});
        $speakers_html ||= '<em>TBD</em>';
        my $zoom = $sess->{session_url} ? qq~<a href="$sess->{session_url}" style="color:#993300;">Join</a>~ : '';

        $session_rows .= qq~
        <tr>
            <td style="padding:10px 12px; border-bottom:1px solid #eee; font-size:14px; white-space:nowrap;">$start - $end</td>
            <td style="padding:10px 12px; border-bottom:1px solid #eee; font-size:14px; font-weight:600;">$sess->{session_title}</td>
            <td style="padding:10px 12px; border-bottom:1px solid #eee; font-size:14px;">$speakers_html</td>
            <td style="padding:10px 12px; border-bottom:1px solid #eee; font-size:14px; text-align:center;">$zoom</td>
        </tr>~;
    }

    # Build full email HTML using template skeleton
    # (Use the standard template from memory/email_template.md with session table in the body)

    my $sent = 0;
    for my $emp (@employers) {
        next unless $emp->{email};
        my $subject = "Session Schedule: $event_name - $event_date";
        # Build personalized HTML with greeting
        # Send via smtp_notify
        smtp_notify('','','',$emp->{email},'events\@hbcuconnect.com',$subject,$html_body,"HBCU Connect Events",'',$html_body);
        $sent++;
    }

    print qq~{"success":true,"message":"Schedule emailed to $sent employer(s)"}~;
}
```

**Step 2: Add `sendSpeakerReminder()` function**

Sends a confirmation reminder email to a single speaker.

```perl
sub sendSpeakerReminder {
    my $speaker_id = $q->param('speaker_id');
    my $company_id = $q->param('company_id');

    my $ccdbh = getDBHandle('career_center');
    my $speaker = $ccdbh->selectrow_hashref('SELECT * FROM event_session_speakers WHERE speaker_id=?', undef, $speaker_id);
    unless ($speaker && $speaker->{speaker_email}) {
        print '{"success":false,"message":"Speaker has no email address"}';
        return;
    }

    my $session = $ccdbh->selectrow_hashref('SELECT * FROM event_sessions WHERE session_id=?', undef, $speaker->{session_id});
    my $event = getEvent($company_id);

    my $event_name = $event->{company_name} || "Career Fair";
    my $event_date = formatEventDate($event->{event_date}) || 'TBD';
    my $start = formatSessionTime($session->{start_time});
    my $end   = formatSessionTime($session->{end_time});

    # Build reminder email using standard template
    my $subject = "Reminder: Please Confirm Your Session - $event_name";
    # Body includes: event name, date, session title, time, and a request to confirm

    smtp_notify('','','',$speaker->{speaker_email},'events\@hbcuconnect.com',$subject,$html,'HBCU Connect Events','',$html);

    &logEvent(0, $ENV{REMOTE_USER} || 'admin', "Event: Speaker Reminder", "Event: Sent reminder to $speaker->{speaker_name} ($speaker->{speaker_email})", $ccdbh, 0);
    print '{"success":true,"message":"Reminder sent to ' . $speaker->{speaker_name} . '"}';
}
```

**Step 3: Syntax check and fix ownership**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c event_sponsor_manager.cgi
chown hbcuconnect:psacln event_sponsor_manager.cgi
```

**Step 4: Commit**

```bash
git add events/event_sponsor_manager.cgi
git commit -m "feat: add email schedule and speaker reminder functions"
```

---

### Task 7: Add Sessions Tab to Employer Portal

**Files:**
- Modify: `events/portalUI.pm` — add `sessions` tab to `printPortalNav()` (line ~129-135)
- Modify: `events/portal.cgi` — add `sessions` tab route and handler

**Step 1: Add sessions tab to portal navigation**

In `portalUI.pm` `printPortalNav()`, add to the `@tabs` array after `team`:

```perl
{ id => 'sessions', label => 'Sessions' },
```

**Step 2: Add route in portal.cgi `showEventWorkspace()` (around line 306-310)**

Add before the `else` (overview) branch:

```perl
elsif ($tab eq 'sessions') { showSessionsTab($event, $employer, $config, $is_past); }
```

**Step 3: Add AJAX action handlers in portal.cgi `handleAction()` (around line 1614+)**

```perl
elsif ($action eq 'update_portal_speaker') {
    actionUpdatePortalSpeaker($cid, $employer);
}
elsif ($action eq 'upload_speaker_headshot') {
    actionUploadSpeakerHeadshot($cid, $employer);
}
elsif ($action eq 'confirm_portal_speaker') {
    actionConfirmPortalSpeaker($cid, $employer);
}
elsif ($action eq 'get_sessions') {
    actionGetSessions($cid, $employer);
}
```

**Step 4: Add `showSessionsTab()` function to portal.cgi**

This renders the Sessions tab for employers. Shows:
- Full event schedule (all sessions, read-only for sessions not theirs)
- Their sessions highlighted with an edit pencil icon
- For their speakers: editable bio field, headshot upload, confirmation toggle
- Clean Tailwind card layout consistent with other portal tabs

Key behavior:
- `getEventSessions($cid)` for full schedule
- `getEmployerSessions($cid, $employer->{employer_id})` to identify their sessions
- Their sessions get a highlighted border and edit controls
- Other sessions displayed read-only
- If `$is_past` is true, all fields are read-only (no edit controls)

**Step 5: Add AJAX handler functions**

- `actionUpdatePortalSpeaker()` — employer updates their speaker's bio/title via `saveSessionSpeaker()`
- `actionUploadSpeakerHeadshot()` — employer uploads headshot, processed via `_processPortalSpeakerHeadshot()` (same logic as admin but verifies employer ownership)
- `actionConfirmPortalSpeaker()` — employer confirms their speaker
- `actionGetSessions()` — returns sessions JSON for AJAX refresh

**Important security check:** For every portal speaker action, verify that the speaker belongs to the employer:
```perl
my $ccdbh = getDBHandle('career_center');
my $speaker = $ccdbh->selectrow_hashref(
    'SELECT * FROM event_session_speakers WHERE speaker_id=? AND employer_id=? AND active=1',
    undef, $speaker_id, $employer->{employer_id}
);
unless ($speaker) {
    print encode_json({ success => 0, message => 'Not authorized' });
    return;
}
```

**Step 6: Add `_processPortalSpeakerHeadshot()` helper**

Same logic as `_processSpeakerHeadshot()` in event_sponsor_manager.cgi but runs in portal.cgi context.

**Step 7: Syntax check and fix ownership**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events && perl -c portal.cgi && perl -c portalUI.pm
chown hbcuconnect:psacln portal.cgi portalUI.pm
```

**Step 8: Commit**

```bash
git add events/portal.cgi events/portalUI.pm
git commit -m "feat: add Sessions tab to employer portal with speaker self-management"
```

---

### Task 8: End-to-End Testing

**Files:**
- All files from previous tasks

**Step 1: Verify syntax on all modified files**

```bash
cd /var/www/vhosts/hbcuconnect.com/httpdocs/events
perl -c events.pl
perl -c event_sponsor_manager.cgi
perl -c portal.cgi
perl -c portalUI.pm
```

**Step 2: Verify database tables exist**

```bash
mysql -h newdb.hbcuconnect.com -u root -p career_center -e "SELECT COUNT(*) FROM event_sessions; SELECT COUNT(*) FROM event_session_speakers;"
```

**Step 3: Test admin sessions tab renders**

Load in browser: `https://hbcuconnect.com/events/event_sponsor_manager.cgi?show_sessions=1&cid=<valid_company_id>`

Verify:
- Sessions tab is active in sub-nav
- Empty state shows "No sessions yet" with Add Session button
- Company dropdown is populated with participating employers

**Step 4: Test session CRUD via admin**

1. Click "Add Session" — fill in title, time, company, speaker — save
2. Verify session appears in table
3. Click edit — modify title — save — verify update
4. Add a second speaker to the session — verify it appears
5. Confirm a speaker — verify badge changes to green "Confirmed"
6. Delete a speaker — verify it disappears
7. Delete a session — verify it disappears

**Step 5: Test email schedule**

1. Click "Email Schedule to All" with at least one session created
2. Verify email arrives with correct schedule table
3. Verify email uses the standard HBCU template (dark header, maroon accent)

**Step 6: Test portal sessions tab**

1. Log in as an employer who has a speaker assigned
2. Navigate to Sessions tab
3. Verify full schedule is visible
4. Verify their session is highlighted with edit controls
5. Update speaker bio — verify save works
6. Upload headshot — verify thumbnail appears
7. Toggle confirmation — verify status updates

**Step 7: Test speaker reminder email**

1. In admin, click "Send Reminder" on an unconfirmed speaker
2. Verify reminder email arrives at speaker's email

**Step 8: Final commit**

```bash
git add -A events/
git commit -m "feat: Speaker Sessions manager — complete implementation with admin UI, portal integration, and email features"
```

---

### Task Summary

| Task | Description | Files |
|------|-------------|-------|
| 1 | Create database tables | `events/sql/create_session_tables.sql` |
| 2 | Add CRUD functions to events.pl | `events/events.pl` |
| 3 | Add Sessions tab to admin sub-nav | `events/events.pl` |
| 4 | Add routes + AJAX actions | `events/event_sponsor_manager.cgi` |
| 5 | Build admin Sessions UI | `events/event_sponsor_manager.cgi` |
| 6 | Build email schedule feature | `events/event_sponsor_manager.cgi` |
| 7 | Add Sessions tab to portal | `events/portal.cgi`, `events/portalUI.pm` |
| 8 | End-to-end testing | All files |
