From 70d6b27ad2ded7d153088c79ef0aebd4f9e0086e Mon Sep 17 00:00:00 2001 From: Bryan Mitchell Date: Mon, 23 Mar 2026 00:07:47 -0400 Subject: [PATCH] Add design categories feature, car pass printable, and voting UX improvements - Design categories: allows creating design awards (Best Animal, Best Detail, etc.) and assigning racers to categories during check-in or via the awards editor - Car pass printable: prints each car photo with their design category entries - Voting: filters ballot to show only eligible racers for design awards, auto-closes modal after vote selection, Save button replaces Back button - New DesignEntries table added to schema - New AJAX endpoints for design entry management --- website/ajax/action.design.auto-enter.inc | 58 +++ website/ajax/action.design.save-entries.inc | 49 ++ website/ajax/action.design.toggle-entry.inc | 25 + website/ajax/query.award.design-list.inc | 30 ++ website/ajax/query.award.eligible-racers.inc | 89 ++++ website/ajax/query.design.entries.inc | 22 + website/ajax/query.design.entry-count.inc | 11 + .../ajax/query.racer.by-design-category.inc | 47 ++ website/awards-editor.php | 27 +- website/checkin.php | 117 +++++ website/css/checkin.css | 17 + website/img/save-button.png | Bin 0 -> 4994 bytes website/inc/save-banner.inc | 30 ++ website/js/awards-editor.js | 153 ++++++ website/js/vote.js | 87 +++- .../print/docs/racer/car-pass/document.inc | 472 ++++++++++++++++++ website/sql/sqlite/design-entries.inc | 15 + website/sql/sqlite/schema.inc | 1 + website/vote.php | 16 +- 19 files changed, 1244 insertions(+), 22 deletions(-) create mode 100644 website/ajax/action.design.auto-enter.inc create mode 100644 website/ajax/action.design.save-entries.inc create mode 100644 website/ajax/action.design.toggle-entry.inc create mode 100644 website/ajax/query.award.design-list.inc create mode 100644 website/ajax/query.award.eligible-racers.inc create mode 100644 website/ajax/query.design.entries.inc create mode 100644 website/ajax/query.design.entry-count.inc create mode 100644 website/ajax/query.racer.by-design-category.inc create mode 100644 website/img/save-button.png create mode 100644 website/inc/save-banner.inc create mode 100644 website/print/docs/racer/car-pass/document.inc create mode 100644 website/sql/sqlite/design-entries.inc 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 0000000000000000000000000000000000000000..4a8a65654770a7a61a79b2a464db06647b3890da GIT binary patch literal 4994 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\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; + }); + } + }); + });