Traffic Intensity = (Calls per interval × AHT) / Interval duration in seconds
Traffic Intensity = (100 × 180) / 1800 = 10 Erlangs
Traffic Intensity = (Calls per interval × AHT) / Interval duration in seconds
Traffic Intensity = (100 × 180) / 1800 = 10 Erlangs
Traffic Intensity = (Calls per interval × AHT) / Interval duration in seconds
Traffic Intensity = (100 × 180) / 1800 = 10 Erlangs
Agents needed = Raw agents / (1 - Shrinkage rate)
Agents needed = 13 / (1 - 0.30) = 13 / 0.70 = 18.6 ≈ 19 agents
Agents needed = Raw agents / (1 - Shrinkage rate)
Agents needed = 13 / (1 - 0.30) = 13 / 0.70 = 18.6 ≈ 19 agents
Agents needed = Raw agents / (1 - Shrinkage rate)
Agents needed = 13 / (1 - 0.30) = 13 / 0.70 = 18.6 ≈ 19 agents
#!/usr/bin/env python3
"""erlang_c.py - Call center staffing calculator""" import math
from functools import lru_cache @lru_cache(maxsize=1024)
def erlang_c(agents, traffic): """Calculate Erlang C probability of waiting.""" if agents <= traffic: return 1.0 # system is overloaded # Erlang B (probability of blocking) inv_b = 1.0 for i in range(1, agents + 1): inv_b = 1.0 + inv_b * i / traffic erlang_b = 1.0 / inv_b # Erlang C = Erlang B / (1 - rho * (1 - Erlang B)) rho = traffic / agents ec = erlang_b / (1.0 - rho * (1.0 - erlang_b)) return min(ec, 1.0) def calculate_service_level(agents, traffic, target_time, aht): """Calculate service level for given parameters.""" pw = erlang_c(agents, traffic) rho = traffic / agents sl = 1.0 - pw * math.exp(-(agents - traffic) * target_time / aht) return max(0.0, min(1.0, sl)) def find_agents_needed(calls_per_interval, aht_seconds, interval_seconds, target_sl, target_time, shrinkage=0.30, max_occupancy=0.85): """Find minimum agents to meet service level and occupancy targets.""" traffic = (calls_per_interval * aht_seconds) / interval_seconds for agents in range(int(traffic) + 1, int(traffic) + 100): sl = calculate_service_level(agents, traffic, target_time, aht_seconds) occupancy = traffic / agents if sl >= target_sl and occupancy <= max_occupancy: raw_agents = agents scheduled = math.ceil(raw_agents / (1 - shrinkage)) return { "traffic_erlangs": round(traffic, 1), "raw_agents": raw_agents, "service_level": round(sl * 100, 1), "occupancy": round(occupancy * 100, 1), "prob_waiting": round(erlang_c(agents, traffic) * 100, 1), "shrinkage": shrinkage, "scheduled_agents": scheduled } return None # Example: 100 calls per 30 min, 3 min AHT, 80/20 service level
result = find_agents_needed( calls_per_interval=100, aht_seconds=180, interval_seconds=1800, target_sl=0.80, target_time=20, shrinkage=0.30
) if result: print(f"Traffic intensity: {result['traffic_erlangs']} Erlangs") print(f"Raw agents needed: {result['raw_agents']}") print(f"Service level: {result['service_level']}%") print(f"Occupancy: {result['occupancy']}%") print(f"Prob of waiting: {result['prob_waiting']}%") print(f"With {result['shrinkage']*100:.0f}% shrinkage: {result['scheduled_agents']} scheduled agents")
#!/usr/bin/env python3
"""erlang_c.py - Call center staffing calculator""" import math
from functools import lru_cache @lru_cache(maxsize=1024)
def erlang_c(agents, traffic): """Calculate Erlang C probability of waiting.""" if agents <= traffic: return 1.0 # system is overloaded # Erlang B (probability of blocking) inv_b = 1.0 for i in range(1, agents + 1): inv_b = 1.0 + inv_b * i / traffic erlang_b = 1.0 / inv_b # Erlang C = Erlang B / (1 - rho * (1 - Erlang B)) rho = traffic / agents ec = erlang_b / (1.0 - rho * (1.0 - erlang_b)) return min(ec, 1.0) def calculate_service_level(agents, traffic, target_time, aht): """Calculate service level for given parameters.""" pw = erlang_c(agents, traffic) rho = traffic / agents sl = 1.0 - pw * math.exp(-(agents - traffic) * target_time / aht) return max(0.0, min(1.0, sl)) def find_agents_needed(calls_per_interval, aht_seconds, interval_seconds, target_sl, target_time, shrinkage=0.30, max_occupancy=0.85): """Find minimum agents to meet service level and occupancy targets.""" traffic = (calls_per_interval * aht_seconds) / interval_seconds for agents in range(int(traffic) + 1, int(traffic) + 100): sl = calculate_service_level(agents, traffic, target_time, aht_seconds) occupancy = traffic / agents if sl >= target_sl and occupancy <= max_occupancy: raw_agents = agents scheduled = math.ceil(raw_agents / (1 - shrinkage)) return { "traffic_erlangs": round(traffic, 1), "raw_agents": raw_agents, "service_level": round(sl * 100, 1), "occupancy": round(occupancy * 100, 1), "prob_waiting": round(erlang_c(agents, traffic) * 100, 1), "shrinkage": shrinkage, "scheduled_agents": scheduled } return None # Example: 100 calls per 30 min, 3 min AHT, 80/20 service level
result = find_agents_needed( calls_per_interval=100, aht_seconds=180, interval_seconds=1800, target_sl=0.80, target_time=20, shrinkage=0.30
) if result: print(f"Traffic intensity: {result['traffic_erlangs']} Erlangs") print(f"Raw agents needed: {result['raw_agents']}") print(f"Service level: {result['service_level']}%") print(f"Occupancy: {result['occupancy']}%") print(f"Prob of waiting: {result['prob_waiting']}%") print(f"With {result['shrinkage']*100:.0f}% shrinkage: {result['scheduled_agents']} scheduled agents")
#!/usr/bin/env python3
"""erlang_c.py - Call center staffing calculator""" import math
from functools import lru_cache @lru_cache(maxsize=1024)
def erlang_c(agents, traffic): """Calculate Erlang C probability of waiting.""" if agents <= traffic: return 1.0 # system is overloaded # Erlang B (probability of blocking) inv_b = 1.0 for i in range(1, agents + 1): inv_b = 1.0 + inv_b * i / traffic erlang_b = 1.0 / inv_b # Erlang C = Erlang B / (1 - rho * (1 - Erlang B)) rho = traffic / agents ec = erlang_b / (1.0 - rho * (1.0 - erlang_b)) return min(ec, 1.0) def calculate_service_level(agents, traffic, target_time, aht): """Calculate service level for given parameters.""" pw = erlang_c(agents, traffic) rho = traffic / agents sl = 1.0 - pw * math.exp(-(agents - traffic) * target_time / aht) return max(0.0, min(1.0, sl)) def find_agents_needed(calls_per_interval, aht_seconds, interval_seconds, target_sl, target_time, shrinkage=0.30, max_occupancy=0.85): """Find minimum agents to meet service level and occupancy targets.""" traffic = (calls_per_interval * aht_seconds) / interval_seconds for agents in range(int(traffic) + 1, int(traffic) + 100): sl = calculate_service_level(agents, traffic, target_time, aht_seconds) occupancy = traffic / agents if sl >= target_sl and occupancy <= max_occupancy: raw_agents = agents scheduled = math.ceil(raw_agents / (1 - shrinkage)) return { "traffic_erlangs": round(traffic, 1), "raw_agents": raw_agents, "service_level": round(sl * 100, 1), "occupancy": round(occupancy * 100, 1), "prob_waiting": round(erlang_c(agents, traffic) * 100, 1), "shrinkage": shrinkage, "scheduled_agents": scheduled } return None # Example: 100 calls per 30 min, 3 min AHT, 80/20 service level
result = find_agents_needed( calls_per_interval=100, aht_seconds=180, interval_seconds=1800, target_sl=0.80, target_time=20, shrinkage=0.30
) if result: print(f"Traffic intensity: {result['traffic_erlangs']} Erlangs") print(f"Raw agents needed: {result['raw_agents']}") print(f"Service level: {result['service_level']}%") print(f"Occupancy: {result['occupancy']}%") print(f"Prob of waiting: {result['prob_waiting']}%") print(f"With {result['shrinkage']*100:.0f}% shrinkage: {result['scheduled_agents']} scheduled agents")
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, CONCAT( LPAD(HOUR(call_date), 2, '0'), ':', IF(MINUTE(call_date) < 30, '00', '30') ) AS interval_start, COUNT(*) AS call_count, AVG(length_in_sec) AS avg_talk_time, AVG(length_in_sec + 30) AS est_aht # add 30s for after-call work
FROM vicidial_closer_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, CONCAT( LPAD(HOUR(call_date), 2, '0'), ':', IF(MINUTE(call_date) < 30, '00', '30') ) AS interval_start, COUNT(*) AS call_count, AVG(length_in_sec) AS avg_talk_time, AVG(length_in_sec + 30) AS est_aht # add 30s for after-call work
FROM vicidial_closer_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, CONCAT( LPAD(HOUR(call_date), 2, '0'), ':', IF(MINUTE(call_date) < 30, '00', '30') ) AS interval_start, COUNT(*) AS call_count, AVG(length_in_sec) AS avg_talk_time, AVG(length_in_sec + 30) AS est_aht # add 30s for after-call work
FROM vicidial_closer_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS total_dials, SUM(CASE WHEN status NOT IN ('NA','B','DC','N','NP','AFTHRS') THEN 1 ELSE 0 END) AS connected_calls, AVG(CASE WHEN length_in_sec > 0 THEN length_in_sec ELSE NULL END) AS avg_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS total_dials, SUM(CASE WHEN status NOT IN ('NA','B','DC','N','NP','AFTHRS') THEN 1 ELSE 0 END) AS connected_calls, AVG(CASE WHEN length_in_sec > 0 THEN length_in_sec ELSE NULL END) AS avg_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
SELECT DATE(call_date) AS call_day, DAYOFWEEK(call_date) AS day_of_week, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS total_dials, SUM(CASE WHEN status NOT IN ('NA','B','DC','N','NP','AFTHRS') THEN 1 ELSE 0 END) AS connected_calls, AVG(CASE WHEN length_in_sec > 0 THEN length_in_sec ELSE NULL END) AS avg_talk_time
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 12 WEEK)
GROUP BY call_day, interval_id
ORDER BY call_day, interval_id;
#!/usr/bin/env python3
"""forecast.py - Call volume forecast from historical data""" import json
from collections import defaultdict def build_forecast(historical_data, forecast_weeks=1): """Build a weighted forecast from historical interval data. historical_data: list of dicts with day_of_week, interval_id, call_count, week_num """ # Group by (day_of_week, interval_id) intervals = defaultdict(list) max_week = max(d["week_num"] for d in historical_data) for d in historical_data: key = (d["day_of_week"], d["interval_id"]) weeks_ago = max_week - d["week_num"] weight = 1.5 if weeks_ago < 4 else 1.0 intervals[key].append({ "calls": d["call_count"], "weight": weight }) forecast = {} for (dow, interval), entries in intervals.items(): total_weight = sum(e["weight"] for e in entries) weighted_avg = sum(e["calls"] * e["weight"] for e in entries) / total_weight forecast[(dow, interval)] = { "predicted_calls": round(weighted_avg, 1), "data_points": len(entries), "confidence": "high" if len(entries) >= 8 else "medium" if len(entries) >= 4 else "low" } return forecast
#!/usr/bin/env python3
"""forecast.py - Call volume forecast from historical data""" import json
from collections import defaultdict def build_forecast(historical_data, forecast_weeks=1): """Build a weighted forecast from historical interval data. historical_data: list of dicts with day_of_week, interval_id, call_count, week_num """ # Group by (day_of_week, interval_id) intervals = defaultdict(list) max_week = max(d["week_num"] for d in historical_data) for d in historical_data: key = (d["day_of_week"], d["interval_id"]) weeks_ago = max_week - d["week_num"] weight = 1.5 if weeks_ago < 4 else 1.0 intervals[key].append({ "calls": d["call_count"], "weight": weight }) forecast = {} for (dow, interval), entries in intervals.items(): total_weight = sum(e["weight"] for e in entries) weighted_avg = sum(e["calls"] * e["weight"] for e in entries) / total_weight forecast[(dow, interval)] = { "predicted_calls": round(weighted_avg, 1), "data_points": len(entries), "confidence": "high" if len(entries) >= 8 else "medium" if len(entries) >= 4 else "low" } return forecast
#!/usr/bin/env python3
"""forecast.py - Call volume forecast from historical data""" import json
from collections import defaultdict def build_forecast(historical_data, forecast_weeks=1): """Build a weighted forecast from historical interval data. historical_data: list of dicts with day_of_week, interval_id, call_count, week_num """ # Group by (day_of_week, interval_id) intervals = defaultdict(list) max_week = max(d["week_num"] for d in historical_data) for d in historical_data: key = (d["day_of_week"], d["interval_id"]) weeks_ago = max_week - d["week_num"] weight = 1.5 if weeks_ago < 4 else 1.0 intervals[key].append({ "calls": d["call_count"], "weight": weight }) forecast = {} for (dow, interval), entries in intervals.items(): total_weight = sum(e["weight"] for e in entries) weighted_avg = sum(e["calls"] * e["weight"] for e in entries) / total_weight forecast[(dow, interval)] = { "predicted_calls": round(weighted_avg, 1), "data_points": len(entries), "confidence": "high" if len(entries) >= 8 else "medium" if len(entries) >= 4 else "low" } return forecast
SELECT DATE(call_date) AS forecast_day, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS actual_calls, f.predicted_calls, ROUND((COUNT(*) - f.predicted_calls) / f.predicted_calls * 100, 1) AS variance_pct
FROM vicidial_closer_log v
JOIN forecast_table f ON DATE(v.call_date) = f.forecast_date AND FLOOR(HOUR(v.call_date) * 2 + MINUTE(v.call_date) / 30) = f.interval_id
WHERE DATE(v.call_date) = CURDATE()
GROUP BY forecast_day, interval_id;
SELECT DATE(call_date) AS forecast_day, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS actual_calls, f.predicted_calls, ROUND((COUNT(*) - f.predicted_calls) / f.predicted_calls * 100, 1) AS variance_pct
FROM vicidial_closer_log v
JOIN forecast_table f ON DATE(v.call_date) = f.forecast_date AND FLOOR(HOUR(v.call_date) * 2 + MINUTE(v.call_date) / 30) = f.interval_id
WHERE DATE(v.call_date) = CURDATE()
GROUP BY forecast_day, interval_id;
SELECT DATE(call_date) AS forecast_day, FLOOR(HOUR(call_date) * 2 + MINUTE(call_date) / 30) AS interval_id, COUNT(*) AS actual_calls, f.predicted_calls, ROUND((COUNT(*) - f.predicted_calls) / f.predicted_calls * 100, 1) AS variance_pct
FROM vicidial_closer_log v
JOIN forecast_table f ON DATE(v.call_date) = f.forecast_date AND FLOOR(HOUR(v.call_date) * 2 + MINUTE(v.call_date) / 30) = f.interval_id
WHERE DATE(v.call_date) = CURDATE()
GROUP BY forecast_day, interval_id;
Adherence % = (Scheduled Time - Non-Adherent Time) / Scheduled Time × 100
Adherence % = (Scheduled Time - Non-Adherent Time) / Scheduled Time × 100
Adherence % = (Scheduled Time - Non-Adherent Time) / Scheduled Time × 100
Adherence = (480 - 30) / 480 × 100 = 93.75%
Adherence = (480 - 30) / 480 × 100 = 93.75%
Adherence = (480 - 30) / 480 × 100 = 93.75%
SELECT user AS agent_id, DATE(event_time) AS work_date, MIN(event_time) AS first_login, MAX(event_time) AS last_event, SUM(CASE WHEN status = 'READY' THEN pause_sec ELSE 0 END) AS ready_time, SUM(CASE WHEN status = 'INCALL' THEN pause_sec ELSE 0 END) AS talk_time, SUM(CASE WHEN status = 'PAUSED' THEN pause_sec ELSE 0 END) AS pause_time, SUM(pause_sec) AS total_time, ROUND(SUM(CASE WHEN status IN ('READY','INCALL') THEN pause_sec ELSE 0 END) / SUM(pause_sec) * 100, 1) AS productive_pct
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY user, DATE(event_time)
ORDER BY productive_pct ASC;
SELECT user AS agent_id, DATE(event_time) AS work_date, MIN(event_time) AS first_login, MAX(event_time) AS last_event, SUM(CASE WHEN status = 'READY' THEN pause_sec ELSE 0 END) AS ready_time, SUM(CASE WHEN status = 'INCALL' THEN pause_sec ELSE 0 END) AS talk_time, SUM(CASE WHEN status = 'PAUSED' THEN pause_sec ELSE 0 END) AS pause_time, SUM(pause_sec) AS total_time, ROUND(SUM(CASE WHEN status IN ('READY','INCALL') THEN pause_sec ELSE 0 END) / SUM(pause_sec) * 100, 1) AS productive_pct
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY user, DATE(event_time)
ORDER BY productive_pct ASC;
SELECT user AS agent_id, DATE(event_time) AS work_date, MIN(event_time) AS first_login, MAX(event_time) AS last_event, SUM(CASE WHEN status = 'READY' THEN pause_sec ELSE 0 END) AS ready_time, SUM(CASE WHEN status = 'INCALL' THEN pause_sec ELSE 0 END) AS talk_time, SUM(CASE WHEN status = 'PAUSED' THEN pause_sec ELSE 0 END) AS pause_time, SUM(pause_sec) AS total_time, ROUND(SUM(CASE WHEN status IN ('READY','INCALL') THEN pause_sec ELSE 0 END) / SUM(pause_sec) * 100, 1) AS productive_pct
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
GROUP BY user, DATE(event_time)
ORDER BY productive_pct ASC;
SELECT user AS agent_id, sub_status AS pause_code, COUNT(*) AS pause_count, SUM(pause_sec) AS total_pause_seconds, ROUND(AVG(pause_sec), 0) AS avg_pause_seconds
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND status = 'PAUSED'
GROUP BY user, sub_status
ORDER BY user, total_pause_seconds DESC;
SELECT user AS agent_id, sub_status AS pause_code, COUNT(*) AS pause_count, SUM(pause_sec) AS total_pause_seconds, ROUND(AVG(pause_sec), 0) AS avg_pause_seconds
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND status = 'PAUSED'
GROUP BY user, sub_status
ORDER BY user, total_pause_seconds DESC;
SELECT user AS agent_id, sub_status AS pause_code, COUNT(*) AS pause_count, SUM(pause_sec) AS total_pause_seconds, ROUND(AVG(pause_sec), 0) AS avg_pause_seconds
FROM vicidial_agent_log
WHERE event_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) AND status = 'PAUSED'
GROUP BY user, sub_status
ORDER BY user, total_pause_seconds DESC;
Admin > Reports > Real-Time Report
Admin > Reports > Real-Time Report
Admin > Reports > Real-Time Report
Admin > Users > Agent Transfer Select agent > Move to Campaign: INBOUND_QUEUE
Admin > Users > Agent Transfer Select agent > Move to Campaign: INBOUND_QUEUE
Admin > Users > Agent Transfer Select agent > Move to Campaign: INBOUND_QUEUE
#!/bin/bash
# intraday-check.sh - Compare actual call volume against forecast
INTERVAL=$(date +%H:%M | awk -F: '{ if ($2 < 30) printf "%s:00", $1; else printf "%s:30", $1;
}')
DOW=$(date +%u) ACTUAL=$(mysql -u cron -pPASS vicidial -N -e " SELECT COUNT(*) FROM vicidial_closer_log WHERE call_date >= CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') AND call_date < CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') + INTERVAL 30 MINUTE") FORECAST=$(mysql -u cron -pPASS vicidial -N -e " SELECT predicted_calls FROM wfm_forecast WHERE day_of_week = ${DOW} AND interval_start = '${INTERVAL}'") if [ -n "$FORECAST" ] && [ "$FORECAST" -gt 0 ]; then VARIANCE=$(echo "scale=1; ($ACTUAL - $FORECAST) / $FORECAST * 100" | bc) echo "[$INTERVAL] Actual: $ACTUAL | Forecast: $FORECAST | Variance: ${VARIANCE}%" # Alert if variance exceeds 15% ABS_VAR=$(echo "$VARIANCE" | tr -d '-') if (( $(echo "$ABS_VAR > 15" | bc -l) )); then echo " WARNING: Variance exceeds 15% threshold" fi
fi
#!/bin/bash
# intraday-check.sh - Compare actual call volume against forecast
INTERVAL=$(date +%H:%M | awk -F: '{ if ($2 < 30) printf "%s:00", $1; else printf "%s:30", $1;
}')
DOW=$(date +%u) ACTUAL=$(mysql -u cron -pPASS vicidial -N -e " SELECT COUNT(*) FROM vicidial_closer_log WHERE call_date >= CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') AND call_date < CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') + INTERVAL 30 MINUTE") FORECAST=$(mysql -u cron -pPASS vicidial -N -e " SELECT predicted_calls FROM wfm_forecast WHERE day_of_week = ${DOW} AND interval_start = '${INTERVAL}'") if [ -n "$FORECAST" ] && [ "$FORECAST" -gt 0 ]; then VARIANCE=$(echo "scale=1; ($ACTUAL - $FORECAST) / $FORECAST * 100" | bc) echo "[$INTERVAL] Actual: $ACTUAL | Forecast: $FORECAST | Variance: ${VARIANCE}%" # Alert if variance exceeds 15% ABS_VAR=$(echo "$VARIANCE" | tr -d '-') if (( $(echo "$ABS_VAR > 15" | bc -l) )); then echo " WARNING: Variance exceeds 15% threshold" fi
fi
#!/bin/bash
# intraday-check.sh - Compare actual call volume against forecast
INTERVAL=$(date +%H:%M | awk -F: '{ if ($2 < 30) printf "%s:00", $1; else printf "%s:30", $1;
}')
DOW=$(date +%u) ACTUAL=$(mysql -u cron -pPASS vicidial -N -e " SELECT COUNT(*) FROM vicidial_closer_log WHERE call_date >= CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') AND call_date < CONCAT(CURDATE(), ' ', '${INTERVAL}', ':00') + INTERVAL 30 MINUTE") FORECAST=$(mysql -u cron -pPASS vicidial -N -e " SELECT predicted_calls FROM wfm_forecast WHERE day_of_week = ${DOW} AND interval_start = '${INTERVAL}'") if [ -n "$FORECAST" ] && [ "$FORECAST" -gt 0 ]; then VARIANCE=$(echo "scale=1; ($ACTUAL - $FORECAST) / $FORECAST * 100" | bc) echo "[$INTERVAL] Actual: $ACTUAL | Forecast: $FORECAST | Variance: ${VARIANCE}%" # Alert if variance exceeds 15% ABS_VAR=$(echo "$VARIANCE" | tr -d '-') if (( $(echo "$ABS_VAR > 15" | bc -l) )); then echo " WARNING: Variance exceeds 15% threshold" fi
fi
Admin > Timeclock > Shift Definition Shift ID: MORNING_A Start Time: 08:00 End Time: 16:30 Lunch Start: 11:30 Lunch End: 12:00 Break 1 Start: 10:00 Break 1 End: 10:15 Break 2 Start: 14:00 Break 2 End: 14:15
Admin > Timeclock > Shift Definition Shift ID: MORNING_A Start Time: 08:00 End Time: 16:30 Lunch Start: 11:30 Lunch End: 12:00 Break 1 Start: 10:00 Break 1 End: 10:15 Break 2 Start: 14:00 Break 2 End: 14:15
Admin > Timeclock > Shift Definition Shift ID: MORNING_A Start Time: 08:00 End Time: 16:30 Lunch Start: 11:30 Lunch End: 12:00 Break 1 Start: 10:00 Break 1 End: 10:15 Break 2 Start: 14:00 Break 2 End: 14:15 - Call volume -- number of calls per time interval (usually 30 minutes)
- Average Handle Time (AHT) -- talk time plus after-call work, in seconds
- Service Level target -- percentage of calls answered within a time threshold (e.g., 80% of calls answered within 20 seconds) - Minimum agents required to meet that service level - 200 calls per hour (100 per 30-minute interval)
- Average Handle Time: 180 seconds (3 minutes)
- Service Level target: 80% of calls answered within 20 seconds - Calculate the average call volume for each 30-minute interval, grouped by day of week
- Weight recent weeks more heavily (last 4 weeks get 60% weight, prior 8 weeks get 40%)
- Apply known adjustments for holidays, marketing campaigns, or seasonal patterns - Late logins (scheduled at 8:00, logged in at 8:12)
- Early logoffs (left at 4:45 instead of 5:00)
- Extended breaks (15-minute break turned into 25 minutes)
- Unauthorized auxiliary/pause time - Adherence = doing the right thing at the right time. Were you logged in when you were scheduled to be? Were you on break when you were scheduled for break?
- Conformance = doing the right amount of total work. Did you work 8 hours total? (You might have come in late and stayed late -- conformance would be fine, adherence would not.) - Cancel non-essential training and meetings -- pull those agents back to the phones
- Offer overtime to agents who already went home (text them, let them accept via app)
- Adjust break schedules -- shorten breaks by 5 minutes, stagger them wider
- If you have a blended inbound/outbound operation, pause outbound campaigns to free agents for inbound - Offer Voluntary Time Off (VTO) to avoid paying agents to sit idle
- Pull agents into coaching sessions or training that was scheduled for later
- Run blended outbound dials to keep agents productive
- Do not send everyone home -- volume can spike back up unpredictably