This is an extension to this post:
Dynamic Image Loading with jQuery & PHP
Ok, so the problem I needed to solve for this one was to build a dynamic results table from SQL and display the results in an ordered html table with an option to export that data into a CSV file format.
There are some “business” rules in the javascript that we use to determine how the medals are awarded. The code automatically determines medal placement based on what place the athlete finished in and whether or not the athlete is from Arkansas (In State).
There are four sections to this solution. The HTML page, the Javascript code, the CSS styling and finally, the server side PHP code to query the DB.
Here is the HTML:
include"auth/auth_check_header.php"; ?>
$eventid = $_GET['eventid'];
if (strlen($eventid) >0)
{
?>
}
else
{
?>
}
?>
Race Manager
Perfect timing, one race at a time…
View Results
Event
Name
Bib
Age Group
In State?
Start
End
Elapsed
Avg
Place
Medal
So the HTML page has some links to other javascript files. The notable one here is the results.js file which handles all of the building of the html table and also has some rules for how the table will be populated and displayed. The button for the Export to CSV has a Submit action and POSTS to a PHP page (getCSV.php), which will take the generated CSV structure and render to the browser (which causes the Open/Save dialog).
Here is the javascript code for the results.js file
// Server Side Ajax Path
var ajax_url = "ajax/finalize_ajax.php";
function init(eventid)
{
// Event Binding
bindEvents();
// Populate the active events
// Located in common.js
populateEvents();
// Hide the Table Div Initially
$("#results_display").hide();
// Disable the Export Button
$("#btnExport").attr('disabled', 'disabled');
if (eventid != "0")
{
$("#cmb_events").val(eventid);
populateAgeGroupedResults();
}
}
function bindEvents()
{
$("#btnViewAgeGrouped").bind("click",function() {
populateAgeGroupedResults();
});
$("#btnViewOverall").bind("click",function() {
populateOverallResults();
});
}
function populateAgeGroupedResults()
{
// Fade out the Table Div for effect
//$("#results_display").fadeOut("fast");
var selevent = $("#cmb_events").val();
$("#tblResults").find("tr:gt(0)").remove();
var curevent = $('#cmb_events').val();
mycmd = '?cmd=getresults&eventid=' + selevent;
var markup = "";
$.ajax({
url: results_ajax_url + mycmd,
success: function(data)
{
var mydata = eval(data);
var curevent;
var lastevent;
var curagegroup;
var lastagegroup;
var grouppos = 1;
var medal;
var instatecount = 0;
var instate = false;
var elapsedhour;
var elapsedminute;
var elapsedsecond;
var avgspeed;
var distancearray;
var distance;
for (var i=0;i";
markup += "
";
instatecount = 0;
markup +="
";
}
instate = false;
elapsedhour = mydata[i].ElapsedHour;
elapsedminute = mydata[i].ElapsedMinute;
elapsedsecond = mydata[i].ElapsedSecond;
// Get distance and covert to miles
distancearray = mydata[i].EventName.split("K");
distance= distancearray[0];
distance = distance * .621371;
avgspeed =
calculatespeed(elapsedhour,elapsedminute,elapsedsecond,distance)
if (mydata[i].InState == "Y")
{
instatecount++;
instate = true
}
// Build our row html
markup += "
";
markup += "
" + mydata[i].EventName + "
";
markup += "
" + mydata[i].AthleteName + "
";
markup += "
" + mydata[i].BibNumber + "
";
markup += "
" + mydata[i].AgeGroup + "
";
markup += "
" + mydata[i].InState + "
";
markup += "
" + mydata[i].StartTime + "
";
markup += "
" + mydata[i].FinishTime + "
";
markup += "
" + mydata[i].ElapsedTime + "
";
markup += "
" + avgspeed + "
";
markup += "
" + grouppos + "
";
medal = " ";
// We only care about the first 3 in-state competitors
// as far as medals go
if (instatecount < 4)
{
/*
The grouppos is used for the overall placement of the athlete
The rule is that the top 3 in-state athletes get Gold, Silver, Bronze
Out of state athletes can only get a medal if they finish in
the top 3, but cannot take a medal placement from an
in-state athlete
*/
switch (instatecount)
{
case 0:
if (grouppos == 1)
{
medal = "Gold";
}
else if (grouppos == 2)
{
medal = "Silver";
}
else if (grouppos == 3)
{
medal = "Bronze";
}
break;
case 1:
if (instate)
{
// 1st Instate always gets Gold
medal = "Gold";
}
else if (grouppos == 1)
{
medal = "Gold";
}
else if (grouppos == 2)
{
medal = "Silver";
}
else if (grouppos == 3)
{
medal = "Bronze";
}
break;
case 2:
if (instate)
{
// 2nd Instate always gets Silver
medal = "Silver";
}
else if (grouppos == 1)
{
medal = "Gold";
}
else if (grouppos == 2)
{
medal = "Silver";
}
else if (grouppos == 3)
{
medal = "Bronze";
}
break;
case 3:
if (instate)
{
// 3rd Instate always gets Bronze
medal = "Bronze";
}
else if (grouppos == 1)
{
medal = "Gold";
}
else if (grouppos == 2)
{
medal = "Silver";
}
else if (grouppos == 3)
{
medal = "Bronze";
}
break;
}
}
else
{
medal = " "; // No Medal
}
markup += "
" + medal + "
";
markup += "
";
grouppos ++;
lastagegroup = curagegroup;
}
// Empty our container (the table body)
$("#tbodyResults").empty();
// Append the HTML rows
$('#tbodyResults').append(markup);
// Show the medal column in case we hid it
$(".medal").removeClass("hideMedalColumn");
// Show the table
$("#results_display").fadeIn("slow");
// Enable the Export Button
$("#btnExport").removeAttr('disabled');
},
async: false
});
}
function populateOverallResults()
{
var selevent = $("#cmb_events").val();
$("#tblResults").find("tr:gt(0)").remove();
var curevent = $('#cmb_events').val();
mycmd = '?cmd=getoverallresults&eventid=' + selevent;
var markup = "";
$.ajax({
url: results_ajax_url + mycmd,
success: function(data)
{
var mydata = eval(data);
var curevent;
var lastevent;
var curagegroup;
var lastagegroup;
var grouppos = 1;
var medal;
var instatecount = 0;
var instate = false;
for (var i=0;i";
markup += "
" + mydata[i].EventName + "
";
markup += "
" + mydata[i].AthleteName + "
";
markup += "
" + mydata[i].BibNumber + "
";
markup += "
" + mydata[i].AgeGroup + "
";
markup += "
" + mydata[i].InState + "
";
markup += "
" + mydata[i].StartTime + "
";
markup += "
" + mydata[i].FinishTime + "
";
markup += "
" + mydata[i].ElapsedTime + "
";
markup += "
" + avgspeed + "
";
markup += "
" + grouppos + "
";
markup += "
 
";
markup += "
";
grouppos ++;
}
// Empty our container (table body)
$("#tbodyResults").empty();
// Append the HTML rows
$('#tbodyResults').append(markup);
// Hide the Medal column
$(".medal").addClass("hideMedalColumn");
// Show the table
$("#results_display").fadeIn("slow");
// Enable the Export Button
$("#btnExport").removeAttr('disabled');
},
async: false
});
}
function calculatespeed(hour,minute,second, distance) {
// convert time to seconds
var elapsedtime = ((parseFloat(hour) * 3600) + (parseFloat(minute) * 60) + parseFloat(second));
// calculate speed
var returnval = ((distance * 1852) / (elapsedtime * 0.44704));
// Return a 10th precision decimal value
return returnval.toFixed(1);
}
function getCSVData(){
// This uses a jQuery plugin that builds the table html into a csv structure
var csv_value=$('#tblResults').table2CSV({delivery:'value'});
$("#csv_text").val(csv_value);
}
So the javascript does the html generation and uses a jQuery plugin called table2CSV that will parse out the html table into a CSV format. The data comes in from an Ajax call to results_ajax.php in a JSON format.
On to the Server Side PHP file (results_ajax.php)
/*
Filename: results_ajax.php
Purpose: This script gets all the data for the Results Page32
Created: 2010-10-07
Created by: LJ Wilson
History: Date Initials Comments
10/07/10 LJW Created
*/
// Always store your database connection classes outside the Web Root
include("/home/content/21/6193721/includes/clsDatabase_evtmanager.php");
include("/home/content/21/6193721/includes/clsDebug.php");
include("../functions.php");
$db = new Database;
$bug = new Debug;
$cur_date = date("Y-m-d");
$cur_year = date("Y");
$cur_time = date("H:i:s");
$dayofweek = date("l");
$ip=$_SERVER['REMOTE_ADDR'];
if ($db)
{
switch ($_GET['cmd'])
{
case "getresults":
$i = 0;
$v = array();
$eventid = $_GET['eventid'];
$sql = "select e.EventName, CONCAT(a.FName, ' ', a.LName) AS AthleteName, " .
"t.BibNumber, r.AgeGroup, case a.State WHEN 'AR' Then 'Y' ELSE 'N' END AS InState, " .
"t.StartTime, t.FinishTime, " .
"TIMEDIFF(t.FinishTime,t.StartTime) as ElapsedTime, " .
"HOUR(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedHour, " .
"MINUTE(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedMinute, " .
"SECOND(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedSecond " .
"FROM TimeKeeper t " .
"INNER JOIN Athletes a on a.AthleteID = t.AthleteID " .
"INNER JOIN Events e on e.EventID = t.EventID " .
"INNER JOIN Roster r on r.AthleteID = a.AthleteID " .
"and r.EventID = t.EventID " .
"WHERE t.EventID = " . $eventid . " " .
"ORDER BY r.AgeGroup, TIMEDIFF(t.FinishTime,t.StartTime) ASC ";
$rs=$db->query($sql);
if ( $db->RecordCount($rs) >0 ) // Make sure we returned something
{
while ( $row = $db->fetch_array($rs) )
{
$eventname = $row["EventName"];
$athletename = $row["AthleteName"];
$bibnumber = $row["BibNumber"];
$agegroup = $row["AgeGroup"];
$instate = $row["InState"];
$starttime = $row["StartTime"];
$finishtime = $row["FinishTime"];
$elapsedtime = $row["ElapsedTime"];
$elapsedhour = $row["ElapsedHour"];
$elapsedminute = $row["ElapsedMinute"];
$elapsedsecond = $row["ElapsedSecond"];
$agegrouparray1 = explode(" ", $agegroup);
$agegrouparray2 = explode("-", $agegrouparray1[1]);
$agegrouplookup = $agegrouparray1[0] . "s " . $eventname;
$agegrouplookup_age = $agegrouparray2[0];
$recordsql = "SELECT COALESCE(RecordTime,'23:59:59') as RecordTime FROM StateRecords WHERE " .
"Event = '" . $agegrouplookup . "' AND Age = '" . $agegrouplookup_age . "'";
$recordrs=$db->query($recordsql);
$recordrow = $db->fetch_array($recordrs);
$RecordTime = $recordrow["RecordTime"];
$athtime = strtotime($elapsedtime);
$rectime = strtotime($RecordTime);
$diff = ($rectime - $athtime) / 60;
if ($diff >=0 )
{
$v[$i]["ElapsedTime"] = $elapsedtime . " **";
}
else
{
$v[$i]["ElapsedTime"] = $elapsedtime;
}
$v[$i]["EventName"] = $eventname;
$v[$i]["AthleteName"] = $athletename;
$v[$i]["BibNumber"] = $bibnumber;
$v[$i]["AgeGroup"] = $agegroup;
$v[$i]["InState"] = $instate;
$v[$i]["StartTime"] = $starttime;
$v[$i]["FinishTime"] = $finishtime;
$v[$i]["ElapsedHour"] = $elapsedhour;
$v[$i]["ElapsedMinute"] = $elapsedminute;
$v[$i]["ElapsedSecond"] = $elapsedsecond;
//$v[$i]["RecordTime"] = $RecordTime;
//$v[$i]["TimeDiff"] = $diff;
//$v[$i]["RecordSQL"] = $recordsql;
$i++;
}
}
// Send out the response object
echo json_encode($v);
break;
case "getoverallresults":
$i = 0;
$v = array();
$eventid = $_GET['eventid'];
$sql = "select e.EventName, CONCAT(a.FName, ' ', a.LName) AS AthleteName, " .
"t.BibNumber, r.AgeGroup, case a.State WHEN 'AR' Then 'Y' ELSE 'N' END AS InState, " .
"t.StartTime, t.FinishTime, " .
"TIMEDIFF(t.FinishTime,t.StartTime) as ElapsedTime, " .
"HOUR(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedHour, " .
"MINUTE(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedMinute, " .
"SECOND(TIMEDIFF(t.FinishTime,t.StartTime)) as ElapsedSecond " .
"FROM TimeKeeper t " .
"INNER JOIN Athletes a on a.AthleteID = t.AthleteID " .
"INNER JOIN Events e on e.EventID = t.EventID " .
"INNER JOIN Roster r on r.AthleteID = a.AthleteID " .
"and r.EventID = t.EventID " .
"WHERE t.EventID = " . $eventid . " " .
"ORDER BY TIMEDIFF(t.FinishTime,t.StartTime) ASC ";
$rs=$db->query($sql);
if ( $db->RecordCount($rs) >0 ) // Make sure we returned something
{
while ( $row = $db->fetch_array($rs) )
{
$eventname = $row["EventName"];
$athletename = $row["AthleteName"];
$bibnumber = $row["BibNumber"];
$agegroup = $row["AgeGroup"];
$instate = $row["InState"];
$starttime = $row["StartTime"];
$finishtime = $row["FinishTime"];
$elapsedtime = $row["ElapsedTime"];
$elapsedhour = $row["ElapsedHour"];
$elapsedminute = $row["ElapsedMinute"];
$elapsedsecond = $row["ElapsedSecond"];
$v[$i]["EventName"] = $eventname;
$v[$i]["AthleteName"] = $athletename;
$v[$i]["BibNumber"] = $bibnumber;
$v[$i]["AgeGroup"] = $agegroup;
$v[$i]["InState"] = $instate;
$v[$i]["StartTime"] = $starttime;
$v[$i]["FinishTime"] = $finishtime;
$v[$i]["ElapsedTime"] = $elapsedtime;
$v[$i]["ElapsedHour"] = $elapsedhour;
$v[$i]["ElapsedMinute"] = $elapsedminute;
$v[$i]["ElapsedSecond"] = $elapsedsecond;
$i++;
}
}
// Send out the response object
echo json_encode($v);
break;
}
}
This Ajax file only has two sections (getresults and getoverallresults). Also note that the includes for the database and debug classes are outside the webroot. I always make it a habit to store my DB connection strings and my debug log paths outside the webroot. Even though PHP code is protected and is not readable from the browser in a “normal” situation, IF the PHP engine breaks on the server, guess what happens to all the PHP code that is normally protected? It gets rendered to the screen! If the sensitive stuff (usernames and passwords to the DB) are stored outside of the area that a web user can get, then all is still safe even if the PHP engine shuts down. This is a good practice to get into and highly recommended.
Next up is the PHP script needed to output our CSV formatted file (getCSV.php):
header("Content-type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"my-data.csv\"");
$data=stripcslashes($_REQUEST['csv_text']);
echo $data;
All this does is take the string of formatted text from the Ajax call we made in results.js and build a file for output with it.
And lastly, the CSS needed to make it look somewhat pretty:
@charset "UTF-8";
/* CSS Document */
.hideMedalColumn { display: none; }
#btnExport
{
background-color:orange;
color:black;
font-weight:bold;
position:relative;
left:85px;
}
#btnViewAgeGrouped
{
background-color:purple;
color:white;
position:relative;
left:20px;
}
#btnViewOverall
{
background-color:red;
color:white;
position:relative;
left:40px;
}
#results_display
{
position:relative;
top:-150px;
left:400px;
-moz-border-radius:5px 5px;
}
#resultstable
{
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
font-size: 12px;
margin: 10px;
width: 650px;
text-align: left;
border-collapse: collapse;
}
#resultstable th
{
font-size: 13px;
font-weight: normal;
padding: 8px;
background: #FFFFFF;
border-top: 4px solid #aabcfe;
border-bottom: 3px solid #aabcfe;
color: #039;
}
#resultstable td
{
padding: 8px;
background: #e8edff;
border-bottom: 1px solid #fff;
color: #669;
border-top: 1px solid transparent;
}
#resultstable tr:hover td
{
background: #d0dafd;
color: #339;
}
#resultstable-table-b
{
font-family: "Lucida Sans Unicode", "Lucida Grande", Sans-Serif;
font-size: 12px;
margin: 45px;
width: 480px;
text-align: center;
border-collapse: collapse;
border-top: 7px solid #9baff1;
border-bottom: 7px solid #9baff1;
}
#resultstable-table-b th
{
font-size: 13px;
font-weight: normal;
padding: 8px;
background: #e8edff;
border-right: 1px solid #9baff1;
border-left: 1px solid #9baff1;
color: #039;
}
#resultstable-table-b td
{
padding: 8px;
background: #e8edff;
border-right: 1px solid #aabcfe;
border-left: 1px solid #aabcfe;
color: #669;
}
