diff --git a/website/ajax/action.design.auto-enter.inc b/website/ajax/action.design.auto-enter.inc new file mode 100644 index 000000000..6cb605857 --- /dev/null +++ b/website/ajax/action.design.auto-enter.inc @@ -0,0 +1,58 @@ +beginTransaction(); + +try { + // Get all eligible racers + $stmt = $db->query('SELECT racerid FROM RegistrationInfo WHERE exclude = 0'); + $racers = $stmt->fetchAll(PDO::FETCH_COLUMN); + + // For each racer, check if they're already in this award category + // If not, add them to it + $check_stmt = $db->prepare('SELECT COUNT(*) FROM DesignEntries + WHERE racerid = :racerid AND awardid = :awardid'); + $insert_stmt = $db->prepare('INSERT INTO DesignEntries (racerid, awardid) + VALUES (:racerid, :awardid)'); + + $racers_added = 0; + foreach ($racers as $racerid) { + $check_stmt->execute(array(':racerid' => $racerid, ':awardid' => $awardid)); + $exists = $check_stmt->fetchColumn(); + + if (!$exists) { + $insert_stmt->execute(array( + ':racerid' => $racerid, + ':awardid' => $awardid + )); + $racers_added++; + } + } + + // Commit the transaction + $db->commit(); + + // Return count of added racers for feedback + json_out('racers_added', $racers_added); + json_success(); + +} catch (Exception $e) { + // Roll back on error + $db->rollBack(); + json_failure('database', $e->getMessage()); +} +?> diff --git a/website/ajax/action.design.save-entries.inc b/website/ajax/action.design.save-entries.inc new file mode 100644 index 000000000..035d3b61f --- /dev/null +++ b/website/ajax/action.design.save-entries.inc @@ -0,0 +1,49 @@ +beginTransaction(); + +try { + // First delete all existing entries for this racer + $stmt = $db->prepare('DELETE FROM DesignEntries WHERE racerid = :racerid'); + $stmt->execute(array(':racerid' => $racerid)); + + // Then add the new entries + $insert_stmt = $db->prepare('INSERT INTO DesignEntries (racerid, awardid) VALUES (:racerid, :awardid)'); + + foreach ($entries as $entry) { + if ($entry['selected']) { + $insert_stmt->execute(array( + ':racerid' => $racerid, + ':awardid' => $entry['awardid'] + )); + } + } + + // Commit the transaction + $db->commit(); + + json_success(); +} catch (Exception $e) { + // Roll back on error + $db->rollBack(); + json_failure('database', $e->getMessage()); +} +?> diff --git a/website/ajax/action.design.toggle-entry.inc b/website/ajax/action.design.toggle-entry.inc new file mode 100644 index 000000000..85e911b64 --- /dev/null +++ b/website/ajax/action.design.toggle-entry.inc @@ -0,0 +1,25 @@ +prepare('INSERT OR IGNORE INTO DesignEntries (racerid, awardid) VALUES (:racerid, :awardid)'); + $stmt->execute(array(':racerid' => $racerid, ':awardid' => $awardid)); + } else { + $stmt = $db->prepare('DELETE FROM DesignEntries WHERE racerid = :racerid AND awardid = :awardid'); + $stmt->execute(array(':racerid' => $racerid, ':awardid' => $awardid)); + } + json_success(); +} catch (Exception $e) { + json_failure('database', $e->getMessage()); +} +?> diff --git a/website/ajax/query.award.design-list.inc b/website/ajax/query.award.design-list.inc new file mode 100644 index 000000000..80c7d777c --- /dev/null +++ b/website/ajax/query.award.design-list.inc @@ -0,0 +1,30 @@ +query('SELECT a.awardid, a.awardname, a.classid, a.rankid, at.awardtype + FROM Awards a + INNER JOIN AwardTypes at ON a.awardtypeid = at.awardtypeid + WHERE at.awardtype IN ("Design General", "Design Trophy") + ORDER BY a.sort'); + +$awards = array(); +foreach ($stmt as $row) { + $awards[] = array( + 'awardid' => (int)$row['awardid'], // Ensure integer type + 'awardname' => $row['awardname'], + 'classid' => (int)$row['classid'], + 'rankid' => (int)$row['rankid'], + 'awardtype' => $row['awardtype'] + ); +} + +// Log the results for debugging +error_log("Design awards query found " . count($awards) . " awards"); +foreach ($awards as $award) { + error_log("Design award ID: " . $award['awardid'] . ", Name: " . $award['awardname']); +} + +json_out('awards', $awards); +json_success(); +?> diff --git a/website/ajax/query.award.eligible-racers.inc b/website/ajax/query.award.eligible-racers.inc new file mode 100644 index 000000000..6ff9a4b6b --- /dev/null +++ b/website/ajax/query.award.eligible-racers.inc @@ -0,0 +1,89 @@ + $awardid)); + +error_log("Is design award check result: " . $is_design); + +// Get award information (classid, rankid) +$award = read_single_row('SELECT classid, rankid FROM Awards WHERE awardid = :awardid', + array(':awardid' => $awardid), PDO::FETCH_ASSOC); + +if ($is_design > 0) { + // Fix the SQL query for carphoto + $carphoto_field = (schema_version() < 2) ? '\'\' as carphoto' : 'r.carphoto'; + + // For design awards, only show racers who have entered this category + $sql = "SELECT r.racerid, r.firstname, r.lastname, r.carnumber, r.carname, + r.classid, r.rankid, r.imagefile, $carphoto_field + FROM RegistrationInfo r + INNER JOIN DesignEntries de ON r.racerid = de.racerid + WHERE de.awardid = :awardid AND r.exclude = 0 AND r.passedinspection = 1 + ORDER BY r.lastname, r.firstname"; + + error_log("Design award SQL: " . $sql); + + $stmt = $db->prepare($sql); + $stmt->execute(array(':awardid' => $awardid)); + + // Count results for debugging + $racers_count = 0; +} else { + // For non-design awards, show racers based on class/rank eligibility + $sql = 'SELECT racerid, firstname, lastname, carnumber, carname, classid, rankid, + imagefile, ' . (schema_version() < 2 ? '\'\' as ' : '') . ' carphoto + FROM RegistrationInfo + WHERE exclude = 0 AND passedinspection = 1'; + + // Add class/rank filters if specified for the award + if ($award && $award['classid'] > 0) { + $sql .= ' AND classid = ' . $award['classid']; + } + if ($award && $award['rankid'] > 0) { + $sql .= ' AND rankid = ' . $award['rankid']; + } + + $sql .= ' ORDER BY lastname, firstname'; + $stmt = $db->query($sql); +} + +$racers = array(); +foreach ($stmt as $row) { + $racers_count++; + $carphoto_url = ''; + if (!empty($row['carphoto'])) { + $carphoto_url = car_photo_repository()->lookup(RENDER_JUDGING)->render_url($row['carphoto']); + } + + $racers[] = array( + 'racerid' => (int)$row['racerid'], // Ensure integer type + 'name' => $row['firstname'] . ' ' . $row['lastname'], + 'firstname' => $row['firstname'], + 'lastname' => $row['lastname'], + 'carnumber' => $row['carnumber'], + 'carname' => $row['carname'], + 'classid' => (int)$row['classid'], + 'rankid' => (int)$row['rankid'], + 'carphoto' => $carphoto_url + ); +} + +error_log("Found $racers_count eligible racers for award ID: $awardid"); + +json_out('eligible_racers', $racers); +json_success(); +?> diff --git a/website/ajax/query.design.entries.inc b/website/ajax/query.design.entries.inc new file mode 100644 index 000000000..070c542b0 --- /dev/null +++ b/website/ajax/query.design.entries.inc @@ -0,0 +1,22 @@ +prepare('SELECT de.awardid, a.awardname + FROM DesignEntries de + INNER JOIN Awards a ON de.awardid = a.awardid + WHERE de.racerid = :racerid'); +$stmt->execute(array(':racerid' => $racerid)); + +$entries = array(); +foreach ($stmt as $row) { + $entries[] = array( + 'awardid' => $row['awardid'], + 'awardname' => $row['awardname'] + ); +} + +json_out('entries', $entries); +json_success(); +?> diff --git a/website/ajax/query.design.entry-count.inc b/website/ajax/query.design.entry-count.inc new file mode 100644 index 000000000..cbaaa10cb --- /dev/null +++ b/website/ajax/query.design.entry-count.inc @@ -0,0 +1,11 @@ + $awardid)); + +json_out('entry_count', $count); +json_success(); +?> diff --git a/website/ajax/query.racer.by-design-category.inc b/website/ajax/query.racer.by-design-category.inc new file mode 100644 index 000000000..ce38c4af0 --- /dev/null +++ b/website/ajax/query.racer.by-design-category.inc @@ -0,0 +1,47 @@ +prepare($sql); +$stmt->execute(array(':awardid' => $awardid)); + +$racers = array(); +$rowno = 0; +foreach ($stmt as $rs) { + $rowno++; + $racers[] = array( + 'racerid' => $rs['racerid'], + 'carnumber' => $rs['carnumber'], + 'lastname' => $rs['lastname'], + 'firstname' => $rs['firstname'], + 'carname' => $rs['carname'], + 'classid' => $rs['classid'], + 'rankid' => $rs['rankid'], + 'headshot' => headshot_repository()->lookup(RENDER_TINY)->get_url_for_racer_icon($rs['racerid'], $rs['imagefile']), + 'rowno' => $rowno + ); +} + +json_out('racers', $racers); +json_success(); +?> diff --git a/website/awards-editor.php b/website/awards-editor.php index 3fd32698a..9b572acbd 100644 --- a/website/awards-editor.php +++ b/website/awards-editor.php @@ -23,7 +23,7 @@ - + @@ -122,6 +122,31 @@ + + diff --git a/website/checkin.php b/website/checkin.php index bdb8c5fcb..af83244e7 100644 --- a/website/checkin.php +++ b/website/checkin.php @@ -262,6 +262,123 @@ function addrow0(racer) { data-on-text="Eligible"/>
+
+

Design Award Categories

+

Select design categories this racer wants to enter:

+
+ +
+
+ + diff --git a/website/css/checkin.css b/website/css/checkin.css index 5e02e1238..5fea67157 100644 --- a/website/css/checkin.css +++ b/website/css/checkin.css @@ -302,3 +302,20 @@ p.warning a.ui-link { div.delete-extension { margin-top: 40px; } + +/* Design Categories Styling */ +#design_categories_section { + margin-top: 15px; + border-top: 1px solid #ddd; + padding-top: 15px; +} + +#design_categories_section h3 { + margin-top: 0; + margin-bottom: 5px; +} + +#design_categories_list label { + font-weight: normal; + font-size: 16px; +} diff --git a/website/img/save-button.png b/website/img/save-button.png new file mode 100644 index 000000000..4a8a65654 Binary files /dev/null and b/website/img/save-button.png differ diff --git a/website/inc/save-banner.inc b/website/inc/save-banner.inc new file mode 100644 index 000000000..a2d3f9471 --- /dev/null +++ b/website/inc/save-banner.inc @@ -0,0 +1,30 @@ +\n"; + + echo "
".htmlspecialchars($banner_title, ENT_QUOTES, 'UTF-8')."
\n"; + + if ($back_button && in_array($back_button, ['index.php', 'setup.php', 'checkin.php', + 'kiosk-dashboard.php', 'coordinator.php'])) { + echo "
\n"; + echo "\n"; + echo "
\n"; + } + + if (isset($_SESSION['role']) && $_SESSION['role']) { + echo ""; + echo "
"; + echo ""; + echo "

v10.0

"; + echo "
"; + echo "
\n"; + } + + echo "\n"; + echo "\n"; +} +?> + diff --git a/website/js/awards-editor.js b/website/js/awards-editor.js index a16bb4fbe..075db9099 100644 --- a/website/js/awards-editor.js +++ b/website/js/awards-editor.js @@ -185,3 +185,156 @@ $(function() { } }); }); + +// Design Categories Management +// Map to keep track of current assignments +var designCategoryAssignments = {}; +var currentAwardId = null; + +function loadDesignCategoryAssignments(awardId) { + designCategoryAssignments = {}; + currentAwardId = awardId; + $('#design_category_name').text($('li[data-awardid="' + awardId + '"]').attr('data-awardname')); + + // Get the entry count for this award + updateEntryCount(); + + // First get all racers + $.ajax(g_action_url, { + type: 'GET', + data: { + query: 'racer.list' + }, + success: function(data) { + var racers = data.racers || []; + + // Then get current assignments for this award + $.ajax(g_action_url, { + type: 'GET', + data: { + query: 'award.eligible-racers', + awardid: awardId + }, + success: function(data) { + var eligibleRacers = data.eligible_racers || []; + var tbody = $('#racer_assignments tbody'); + tbody.empty(); + + // Mark eligible racers + eligibleRacers.forEach(function(racer) { + designCategoryAssignments[racer.racerid] = true; + }); + + // Create table rows for all racers + racers.forEach(function(racer) { + if (racer.exclude) return; // Skip excluded racers + + var isAssigned = designCategoryAssignments[racer.racerid] || false; + var row = $(''); + + row.append('' + racer.carnumber + ''); + row.append('' + racer.firstname + ' ' + racer.lastname + ''); + + var checkbox = $(''); + checkbox.change(function() { + toggleDesignCategoryAssignment(awardId, racer.racerid, this.checked); + }); + + row.append($('').append(checkbox)); + tbody.append(row); + }); + } + }); + } + }); +} + +function toggleDesignCategoryAssignment(awardId, racerId, assign) { + $.ajax(g_action_url, { + type: 'POST', + data: { + action: 'design.toggle-entry', + racerid: racerId, + awardid: awardId, + selected: assign ? 'true' : 'false' + }, + success: function(data) { + designCategoryAssignments[racerId] = assign; + updateEntryCount(); + } + }); +} + +function updateEntryCount() { + if (!currentAwardId) return; + + $.ajax(g_action_url, { + type: 'GET', + data: { + query: 'design.entry-count', + awardid: currentAwardId + }, + success: function(data) { + var count = data.entry_count || 0; + $('#entry_count_display').text('Current entries: ' + count + ' racer' + (count != 1 ? 's' : '')); + } + }); +} + +function autoEnterAllRacers() { + if (!currentAwardId) return; + + if (confirm('This will enter ALL racers for this award. Continue?')) { + $.ajax(g_action_url, { + type: 'POST', + data: { + action: 'design.auto-enter', + awardid: currentAwardId + }, + success: function() { + // Reload assignments + loadDesignCategoryAssignments(currentAwardId); + alert('All racers have been entered for this award'); + } + }); + } +} + +// Add buttons to awards for managing design categories +function addDesignCategoryButtons() { + $('#all_awards li').each(function() { + var awardtypeid = $(this).attr('data-awardtypeid'); + var awardtype = $(this).find('.awardtype').text(); + + // Only add button for design awards + if (awardtype.indexOf('Design') >= 0) { + if ($(this).find('.design-category-btn').length === 0) { + var btn = $('Manage Entries'); + btn.click(function(e) { + e.preventDefault(); + e.stopPropagation(); + var awardId = $(this).closest('li').attr('data-awardid'); + loadDesignCategoryAssignments(awardId); + show_modal('#design_category_modal'); + }); + + $(this).find('.class-and-rank').append(btn); + } + } + }); +} + +// Auto-enter button handler +$(function() { + $('#auto_enter_all_btn').click(function(e) { + e.preventDefault(); + autoEnterAllRacers(); + }); + + // Override the update_awards function to add our buttons + var originalUpdateAwards = update_awards; + update_awards = function(data) { + originalUpdateAwards(data); + addDesignCategoryButtons(); + }; +}); diff --git a/website/js/vote.js b/website/js/vote.js index 454d04a0f..4982ed033 100644 --- a/website/js/vote.js +++ b/website/js/vote.js @@ -49,7 +49,8 @@ function set_up_ballot() { // From the main screen, clicking on an award opens the "racers" modal, which // lets the user choose a racer. function click_one_award(div) { - g_awardid = div.attr('data-awardid'); + var award_id = div.attr('data-awardid'); + g_awardid = award_id; write_racers_headline(); set_full_ballot_message(); @@ -60,17 +61,42 @@ function click_one_award(div) { $("#racers_modal").width(ww - 200).height(wh - 200); $("#racer_view_award_name").text(award_name); - $("#racers div.ballot_racer").removeClass('hidden'); + // Check if this is a design award + if (g_is_design_award && g_is_design_award[award_id]) { + $.ajax('action.php', { + type: 'GET', + data: { + query: 'award.eligible-racers', + awardid: award_id + }, + success: function(data) { + // Hide all racers initially + $("#racers div.ballot_racer").addClass('hidden'); + + // Show only eligible racers + var eligible_racers = data.eligible_racers || []; + eligible_racers.forEach(function(racer) { + $("#racers div.ballot_racer[data-racerid=" + racer.racerid + "]").removeClass('hidden'); + }); + }, + error: function(xhr, status, error) { + } + }); + } else { + // For non-design awards, use existing class/rank filtering + $("#racers div.ballot_racer").removeClass('hidden'); + + var classids = div.attr('data-eligible-classids').split(','); + var rankids = div.attr('data-eligible-rankids').split(','); + $("#racers div.ballot_racer").each(function() { + $(this).toggleClass('hidden', + classids.indexOf($(this).attr('data-classid')) < 0 || + rankids.indexOf($(this).attr('data-rankid')) < 0); + }); + } - var classids = div.attr('data-eligible-classids').split(','); - var rankids = div.attr('data-eligible-rankids').split(','); - $("#racers div.ballot_racer").each(function() { - $(this).toggleClass('hidden', - classids.indexOf($(this).attr('data-classid')) < 0 || - rankids.indexOf($(this).attr('data-rankid')) < 0); - }); - show_modal("#racers_modal", function() {}); + return false; } function write_racers_headline() { @@ -105,17 +131,41 @@ function show_racer_view_modal(div) { function toggle_vote(div) { var award_ballot = g_ballot[g_awardid]; - if (award_ballot['votes'].includes(g_racerid)) { - award_ballot['votes'] = - award_ballot['votes'].filter(function(v) { return v != g_racerid; }); + var wasChecked = award_ballot['votes'].includes(g_racerid); + + if (wasChecked) { + // Unchecking the box + award_ballot['votes'] = award_ballot['votes'].filter(function(v) { return v != g_racerid; }); div.find('img').attr('src', 'img/checkbox-without-check.png'); - } else if (award_ballot['votes'].length >= award_ballot['max_votes']) { - console.log("Full ballot!"); + // Do not close when unchecking } else { - award_ballot['votes'].push(g_racerid); - div.find('img').attr('src', 'img/checkbox-with-check.png'); + // Checking the box + if (award_ballot['votes'].length >= award_ballot['max_votes']) { + } else { + award_ballot['votes'].push(g_racerid); + div.find('img').attr('src', 'img/checkbox-with-check.png'); + + // Send AJAX request and update UI + $.ajax('action.php', + {type: 'POST', + data: {action: 'vote.cast', + awardid: g_awardid, + 'votes': JSON.stringify(award_ballot['votes'])}, + success: function() { + // Close modal after checking and successful save + setTimeout(function() { + close_secondary_modal("#racer_view_modal"); + }, 300); + } + }); + + write_racers_headline(); + set_up_ballot(); + return; // Early return to prevent the code below from executing + } } + // This AJAX call only happens for unchecking or when ballot is full $.ajax('action.php', {type: 'POST', data: {action: 'vote.cast', @@ -141,9 +191,6 @@ function set_full_ballot_message() { $("#full-ballot-max").text(max_votes); - console.log("Award_ballot:");console.log(award_ballot); - console.log("g_racerid: " + g_racerid + ", includes=" + (award_ballot['votes'].includes(g_racerid))); - console.log("max_votes=" + max_votes + ", votes length=" + award_ballot['votes'].length); $("#full-ballot").toggleClass('hidden', award_ballot['votes'].includes(g_racerid) || award_ballot['votes'].length < max_votes); diff --git a/website/print/docs/racer/car-pass/document.inc b/website/print/docs/racer/car-pass/document.inc new file mode 100644 index 000000000..a319b72dc --- /dev/null +++ b/website/print/docs/racer/car-pass/document.inc @@ -0,0 +1,472 @@ +query('SELECT a.awardid, a.awardname + FROM Awards a + INNER JOIN AwardTypes at ON a.awardtypeid = at.awardtypeid + WHERE at.awardtype IN ("Design General", "Design Trophy") + ORDER BY a.awardname'); + + $category_values = array( + array('value' => '0', 'desc' => 'All Categories') + ); + + foreach ($stmt as $row) { + $category_values[] = array( + 'value' => $row['awardid'], + 'desc' => $row['awardname'] + ); + } + } catch (Exception $e) { + // If query fails, just provide the "All Categories" option + $category_values = array( + array('value' => '0', 'desc' => 'All Categories') + ); + } + + return array( + 'layout' => array( + 'type' => 'radio', + 'desc' => 'Size', + 'values' => array( + array('value' => '4-up', 'desc' => '8-1/2 x 11, 4-up'), + array('value' => '4x6', 'desc' => '4x6') + ) + ), + 'with_logo' => array( + 'type' => 'bool', + 'desc' => 'Include logo image', + 'default' => true + ), + 'show_date' => array( + 'type' => 'bool', + 'desc' => 'Show date on car pass', + 'default' => true + ), + 'date_text' => array( + 'type' => 'string', + 'desc' => 'Date to display', + 'default' => date('F j, Y') + ), + 'category_filter' => array( + 'type' => 'radio', + 'desc' => 'Filter by Design Category', + 'values' => $category_values + ), + 'show_categories' => array( + 'type' => 'bool', + 'desc' => 'Show design categories on pass', + 'default' => true + ), + 'bg_color' => array( + 'type' => 'radio', + 'desc' => 'Background color', + 'values' => array( + array('value' => 'blue', 'desc' => 'Blue'), + array('value' => 'green', 'desc' => 'Green'), + array('value' => 'grey', 'desc' => 'Grey') + ) + ) + ); + } + + protected $y_coords; + + function StartDocument() { + $this->set_parameters($this->get_option('layout')); + if ($this->get_option('layout') == '4x6') { + $this->initialize_pdf('P', 'in', array(4, 6)); + $this->initialize_layout(3.825, 6); + } else { + $this->initialize_pdf('P', 'in', 'Letter'); + $this->initialize_layout(3.825, 5.000); + } + } + + function set_parameters($layout) { + if ($layout == '4-up') { + $this->y_coords = array( + 'car_number' => 0.35, + 'name' => 0.65, + 'car_photo' => 1.0, + 'categories' => 3.30, // Moved down from car photo + 'date' => 4.70, // Moved lower + 'logo' => 4.75, // Moved even lower + ); + } else if ($layout == '4x6') { + $this->y_coords = array( + 'car_number' => 0.35, + 'name' => 0.65, + 'car_photo' => 1.0, + 'categories' => 4.35, // Moved down from car photo + 'date' => 5.70, // Moved lower + 'logo' => 5.80, // Moved even lower + ); + } + } + + // Override DrawOne to filter by category if needed + function DrawOne(&$racer) { + $category_filter = $this->get_option('category_filter'); + + // Skip racer if they're not in the selected category + if ($category_filter != '0') { + $racer_categories = $this->get_racer_categories($racer['racerid']); + if (!in_array($category_filter, $racer_categories)) { + return; // Skip this racer + } + } + + // Call parent DrawOne to continue with the normal drawing process + parent::DrawOne($racer); + } + + function DrawOneAt($x, $y, &$racer) { + // Set background color based on option - with default if not set + $bgColor = $this->get_option('bg_color'); + if (!$bgColor) { + $bgColor = 'grey'; // Default color if none selected + } + + if ($bgColor == 'blue') { + $this->pdf->SetFillColor(220, 235, 255); + } else if ($bgColor == 'green') { + $this->pdf->SetFillColor(220, 255, 220); + } else { + $this->pdf->SetFillColor(240, 240, 240); + } + + // Draw background + if ($this->get_option('layout') == '4x6') { + $this->pdf->Rect($x, $y, 3.825, 6, 'F'); + } else { + $this->pdf->Rect($x, $y, 3.825, 5, 'F'); + } + + // Draw border + $this->pdf->SetDrawColor(0, 0, 0); + if ($this->get_option('layout') == '4x6') { + $this->pdf->Rect($x, $y, 3.825, 6, 'D'); + } else { + $this->pdf->Rect($x, $y, 3.825, 5, 'D'); + } + + // Draw car number at top + $this->pdf->SetFont('Helvetica', 'B', 16); + $this->pdf->SetTextColor(0, 0, 128); + $this->pdf->CenteredText($x + 3.825/2, $y + $this->y_coords['car_number'], 'Car #'.$racer['carnumber']); + + // Draw racer name below car number + $this->pdf->SetFont('Times', 'B', 14); + $this->pdf->SetTextColor(0, 0, 0); + $this->pdf->CenteredText($x + 3.825/2, $y + $this->y_coords['name'], + $racer['firstname'].' '.$racer['lastname']); + + // Get car photo using several methods in order of preference + $car_photo = $this->find_car_photo($racer); + + // Draw car photo if found - LARGER SIZE + if ($car_photo && file_exists($car_photo)) { + try { + // Calculate dimensions to maintain aspect ratio + list($width, $height) = getimagesize($car_photo); + $max_width = 3.5; // Increased from 2.5 + $max_height = 2.35; // Increased from 2.0 + + if ($width && $height) { + if ($width/$height > $max_width/$max_height) { + // Width limited + $display_width = $max_width; + $display_height = $height * ($max_width/$width); + } else { + // Height limited + $display_height = $max_height; + $display_width = $width * ($max_height/$height); + } + + $offset_x = ($max_width - $display_width) / 2; + $this->pdf->Image($car_photo, + $x + (3.825 - $display_width) / 2, + $y + $this->y_coords['car_photo'], + $display_width); + } else { + // If getimagesize failed, just use a fixed size + $this->pdf->Image($car_photo, + $x + 0.6625, + $y + $this->y_coords['car_photo'], + 2.5); + } + } catch (Exception $e) { + // If image fails to load, just continue without it + } + } + + // List design categories the racer is entered in - SIMPLIFIED APPROACH + if ($this->get_option('show_categories')) { + $categories = $this->get_racer_category_names($racer['racerid']); + if (!empty($categories)) { + // Calculate category position + $cat_y = $y + $this->y_coords['categories']; + $cat_x = $x + 0.25; + $cat_width = 3.325; + + // Get category text + $category_text = implode(", ", $categories); + + // Create background for categories + $this->pdf->SetFillColor(235, 235, 255); + + // Measure the text to determine how many lines we need + $this->pdf->SetFont('Times', 'I', 10); + $line_height = 0.15; + $available_width = $cat_width - 0.2 - $this->pdf->GetStringWidth("Entered in: "); + + // Calculate how many lines we need for the categories + $textWidth = $this->pdf->GetStringWidth($category_text); + $lines_needed = max(1, ceil($textWidth / $available_width)); + + // Draw the background box + $box_height = ($lines_needed + 0.5) * $line_height; + $this->pdf->Rect($cat_x, $cat_y - 0.05, $cat_width, $box_height, 'F'); + + // Print "Entered in:" in bold + $this->pdf->SetFont('Times', 'B', 10); + $this->pdf->SetTextColor(0, 0, 0); + $this->pdf->SetXY($cat_x + 0.1, $cat_y); + $this->pdf->Write(0.15, "Entered in: "); + + // Calculate position after prefix + $prefix_width = $this->pdf->GetStringWidth("Entered in: "); + + // Print categories in italic, forcing it to wrap properly + $this->pdf->SetFont('Times', 'I', 10); + $this->pdf->SetXY($cat_x + 0.1 + $prefix_width, $cat_y); + $this->pdf->MultiCell($cat_width - 0.2 - $prefix_width, $line_height, $category_text, 0, 'L'); + } + } + + // Draw date if option selected - moved to bottom right + if ($this->get_option('show_date')) { + $this->pdf->SetFont('Times', '', 12); + $this->pdf->SetTextColor(0, 0, 0); + $this->pdf->SetXY($x + 1.9125, $y + $this->y_coords['date'] - 0.15); + $this->pdf->Cell(1.9125, 0.15, $this->get_option('date_text'), 0, 0, 'R'); + } + + // Draw logo if option selected - moved to bottom left corner very low + if ($this->get_option('with_logo')) { + $emblem = image_file_path('emblem'); + if ($emblem && file_exists($emblem)) { + $logo_size = 0.75; // Slightly smaller + // Positioning the logo in the very bottom left + $bottom_margin = 0.1; // Distance from bottom edge + + // Calculate position based on layout + if ($this->get_option('layout') == '4x6') { + $logo_y = $y + 6 - $logo_size - $bottom_margin; + } else { + $logo_y = $y + 5 - $logo_size - $bottom_margin; + } + + $this->pdf->Image($emblem, $x + 0.15, $logo_y, $logo_size, $logo_size); + } + } + } + + private function find_car_photo(&$racer) { + try { + // Get the racer ID and car number + $racerId = $racer['racerid']; + $carNumber = $racer['carnumber']; + + // Try to get the current database path + $db_path = ''; + + // Method 1: Try to extract path from the database connection + global $db; + if (isset($db) && method_exists($db, 'query')) { + $stmt = $db->query("PRAGMA database_list"); + if ($stmt) { + $db_info = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($db_info['file'])) { + $db_path = dirname($db_info['file']); + } + } + } + + // Method 2: Try to get database info from DerbyNet function + if (empty($db_path) && function_exists('get_database_info')) { + $db_info = get_database_info(); + if (isset($db_info['path'])) { + $db_path = dirname($db_info['path']); + } + } + + // Method 3: Fall back to scanning the directory + if (empty($db_path)) { + // Look in the standard location for the latest database + $base_dir = '/var/lib/derbynet'; + $latest_year = ''; + $latest_db = ''; + + if (is_dir($base_dir)) { + $years = scandir($base_dir); + foreach ($years as $year) { + if ($year != '.' && $year != '..' && is_dir("$base_dir/$year")) { + if ($year > $latest_year) { + $latest_year = $year; + } + } + } + + if (!empty($latest_year)) { + $year_dir = "$base_dir/$latest_year"; + $dbs = scandir($year_dir); + $latest_time = 0; + + foreach ($dbs as $db) { + if ($db != '.' && $db != '..' && is_dir("$year_dir/$db")) { + $db_time = filemtime("$year_dir/$db"); + if ($db_time > $latest_time) { + $latest_time = $db_time; + $latest_db = $db; + } + } + } + + if (!empty($latest_db)) { + $db_path = "$year_dir/$latest_db"; + } + } + } + } + + // Fallback to hardcoded path if all dynamic methods fail + if (empty($db_path)) { + $db_path = '/var/lib/derbynet/2025/2025 Derby'; + } + + // Look in the cars directory with the correct "Carid" prefix pattern + $car_dir = $db_path . '/cars/'; + if (is_dir($car_dir)) { + // Try with racerid + $patterns = array( + $car_dir . 'Carid' . sprintf('%03d', $racerId) . '.jpg', // Carid001.jpg + $car_dir . 'Carid' . $racerId . '.jpg', // Carid1.jpg + $car_dir . 'Carid' . sprintf('%03d', $racerId) . '_1.jpg', // Carid001_1.jpg - variant + $car_dir . 'Carid' . $racerId . '_1.jpg', // Carid1_1.jpg - variant + ); + + foreach ($patterns as $path) { + if (file_exists($path)) { + return $path; + } + } + + // Try with car number + $car_patterns = array( + $car_dir . 'Carid' . sprintf('%03d', $carNumber) . '.jpg', + $car_dir . 'Carid' . $carNumber . '.jpg', + $car_dir . 'Carid' . sprintf('%03d', $carNumber) . '_1.jpg', + $car_dir . 'Carid' . $carNumber . '_1.jpg', + ); + + foreach ($car_patterns as $path) { + if (file_exists($path)) { + return $path; + } + } + } + + // Method 2: Try using the built-in DerbyNet photo functions + if (function_exists('photo_file_path') && schema_version() >= 2 && !empty($racer['carphoto'])) { + $car_photo = photo_file_path('car', $racer['carphoto']); + if ($car_photo && file_exists($car_photo)) { + return $car_photo; + } + } + + // Method 3: Look in other photo directories as fallback + $photo_dirs = array( + '/var/www/html/derbynet/Photos/', + '/var/www/html/derbynet/photos/', + '/var/www/html/derbynet/car-photos/', + '/var/www/html/derbynet/Images/cars/', + '/var/www/html/derbynet/images/cars/' + ); + + foreach ($photo_dirs as $dir) { + if (is_dir($dir)) { + // Try with Car prefix and .jpg extension + $car_path = $dir.'Car'.$carNumber.'.jpg'; + if (file_exists($car_path)) { + return $car_path; + } + } + } + } catch (Exception $e) { + // If any error occurs in photo finding, just return false + } + + return false; + } + + // Get the list of design category IDs for a racer + private function get_racer_categories($racerid) { + $categories = array(); + try { + global $db; + $stmt = $db->prepare("SELECT awardid FROM DesignEntries WHERE racerid = :racerid"); + $stmt->execute(array(':racerid' => $racerid)); + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $categories[] = $row['awardid']; + } + } catch (Exception $e) { + // If there's an error, return an empty array + } + + return $categories; + } + + // Get the list of design category names for a racer + private function get_racer_category_names($racerid) { + $categories = array(); + try { + global $db; + $stmt = $db->prepare(" + SELECT a.awardname + FROM DesignEntries de + INNER JOIN Awards a ON de.awardid = a.awardid + INNER JOIN AwardTypes at ON a.awardtypeid = at.awardtypeid + WHERE de.racerid = :racerid + AND at.awardtype IN ('Design General', 'Design Trophy') + ORDER BY a.awardname + "); + $stmt->execute(array(':racerid' => $racerid)); + + while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { + $categories[] = $row['awardname']; + } + } catch (Exception $e) { + // If there's an error, return an empty array + } + + return $categories; + } +} +?> diff --git a/website/sql/sqlite/design-entries.inc b/website/sql/sqlite/design-entries.inc new file mode 100644 index 000000000..65a993d01 --- /dev/null +++ b/website/sql/sqlite/design-entries.inc @@ -0,0 +1,15 @@ + diff --git a/website/sql/sqlite/schema.inc b/website/sql/sqlite/schema.inc index 910b8c793..a9f79c181 100644 --- a/website/sql/sqlite/schema.inc +++ b/website/sql/sqlite/schema.inc @@ -193,6 +193,7 @@ make_index("Rounds", "round"), @include(sql_file_path('action-history')), +@include(sql_file_path('design-entries')), array( "INSERT INTO RaceInfo (itemkey, itemvalue) VALUES ('schema', ".expected_schema_version().")", "INSERT INTO RaceInfo (itemkey, itemvalue) VALUES ('photos-on-now-racing', 'head')", diff --git a/website/vote.php b/website/vote.php index ed7f08257..56c84dc78 100644 --- a/website/vote.php +++ b/website/vote.php @@ -3,7 +3,7 @@ require_once('inc/data.inc'); require_once('inc/authorize.inc'); session_write_close(); -require_once('inc/banner.inc'); +require_once('inc/save-banner.inc'); require_once('inc/photo-config.inc'); require_once('inc/awards.inc'); require_once('inc/voterid.inc'); @@ -27,8 +27,22 @@ var g_ballot; var g_awardid; var g_racerid; +var g_is_design_award = {}; $(function() { get_ballot(); }); + $(function() { + $.ajax('action.php', { + type: 'GET', + data: { + query: 'award.design-list' + }, + success: function(data) { + (data.awards || []).forEach(function(award) { + g_is_design_award[award.awardid] = true; + }); + } + }); + });