| 1 |
<?php |
| 2 |
/** |
| 3 |
* AVCP Table Generator |
| 4 |
* |
| 5 |
* Generates a filterable table of AVCP tender data with export functionality |
| 6 |
* |
| 7 |
* @package AVCP |
| 8 |
* @since 1.0.0 |
| 9 |
*/ |
| 10 |
|
| 11 |
// Prevent direct access |
| 12 |
if (!defined('ABSPATH')) { |
| 13 |
exit; |
| 14 |
} |
| 15 |
|
| 16 |
// Validate and sanitize the year parameter |
| 17 |
if (!isset($anno)) { |
| 18 |
$anno = 'all'; |
| 19 |
} |
| 20 |
|
| 21 |
// Enhanced validation for year parameter |
| 22 |
$anno = sanitize_text_field($anno); |
| 23 |
if ($anno !== 'all' && !empty($anno)) { |
| 24 |
// Validate year format (should be 4 digits) |
| 25 |
if (!preg_match('/^\d{4}$/', $anno)) { |
| 26 |
$anno = 'all'; |
| 27 |
} else { |
| 28 |
// Ensure year is within reasonable range |
| 29 |
$year_int = intval($anno); |
| 30 |
$current_year = date('Y'); |
| 31 |
if ($year_int < 2000 || $year_int > ($current_year + 10)) { |
| 32 |
$anno = 'all'; |
| 33 |
} |
| 34 |
} |
| 35 |
} |
| 36 |
?> |
| 37 |
<script type="text/javascript" src="<?php echo esc_url(plugin_dir_url(__FILE__) . 'includes/excellentexport.min.js'); ?>"></script> |
| 38 |
<div class="avcp-table-container"> |
| 39 |
<table class="order-table table" id="gare" role="table" aria-label="<?php esc_attr_e('Tabella bandi di gara', 'avcp'); ?>"> |
| 40 |
<caption class="screen-reader-text"> |
| 41 |
<?php esc_html_e('Elenco dei bandi di gara pubblicati', 'avcp'); ?> |
| 42 |
</caption> |
| 43 |
<thead> |
| 44 |
<tr> |
| 45 |
<td colspan="6" class="table-header-info"> |
| 46 |
<div class="table-title"> |
| 47 |
<?php esc_html_e('Bandi di gara', 'avcp'); ?> - <strong><?php |
| 48 |
echo ($anno !== 'all') ? esc_html($anno) : esc_html__('Tutti gli anni', 'avcp'); |
| 49 |
?></strong> |
| 50 |
</div> |
| 51 |
<div class="table-search"> |
| 52 |
<label for="s" class="screen-reader-text"><?php esc_html_e('Cerca nei bandi:', 'avcp'); ?></label> |
| 53 |
<input type="search" id="s" class="light-table-filter" |
| 54 |
data-table="order-table" |
| 55 |
placeholder="<?php esc_attr_e('Cerca...', 'avcp'); ?>" |
| 56 |
maxlength="100" |
| 57 |
autocomplete="off" |
| 58 |
aria-describedby="search-help"> |
| 59 |
<span id="search-help" class="screen-reader-text"> |
| 60 |
<?php esc_html_e('Digita per filtrare i risultati della tabella', 'avcp'); ?> |
| 61 |
</span> |
| 62 |
</div> |
| 63 |
</td> |
| 64 |
</tr> |
| 65 |
<tr role="row"> |
| 66 |
<th scope="col" colspan="2"><?php esc_html_e('Oggetto', 'avcp'); ?></th> |
| 67 |
<th scope="col"><?php esc_html_e('CIG', 'avcp'); ?></th> |
| 68 |
<th scope="col"><?php esc_html_e('Importo aggiudicazione', 'avcp'); ?></th> |
| 69 |
<th scope="col"><?php esc_html_e('Durata lavori', 'avcp'); ?></th> |
| 70 |
<th scope="col"><?php esc_html_e('Modalità affidamento', 'avcp'); ?></th> |
| 71 |
</tr> |
| 72 |
</thead> |
| 73 |
<tbody role="rowgroup"> |
| 74 |
|
| 75 |
<?php |
| 76 |
// Prepare query arguments with validation |
| 77 |
$args = array( |
| 78 |
'post_type' => 'avcp', |
| 79 |
'orderby' => 'date', |
| 80 |
'order' => 'DESC', |
| 81 |
'posts_per_page' => -1, |
| 82 |
'post_status' => 'publish', |
| 83 |
'meta_query' => array(), |
| 84 |
'fields' => 'ids', // Only get IDs first for better performance |
| 85 |
'no_found_rows' => true, // Skip pagination count for better performance |
| 86 |
); |
| 87 |
|
| 88 |
// Add taxonomy filter if specific year is requested |
| 89 |
if ($anno !== '' && $anno !== 'all') { |
| 90 |
$args['tax_query'] = array( |
| 91 |
array( |
| 92 |
'taxonomy' => 'annirif', |
| 93 |
'field' => 'slug', |
| 94 |
'terms' => sanitize_title($anno), |
| 95 |
), |
| 96 |
); |
| 97 |
} |
| 98 |
|
| 99 |
// Execute query with error handling |
| 100 |
$post_ids = get_posts($args); |
| 101 |
|
| 102 |
if (!empty($post_ids) && !is_wp_error($post_ids)) : |
| 103 |
|
| 104 |
// Pre-load all meta data in one query for better performance |
| 105 |
$meta_keys = array('avcp_data_inizio', 'avcp_data_fine', 'avcp_cig', 'avcp_aggiudicazione', 'avcp_contraente'); |
| 106 |
$meta_cache = array(); |
| 107 |
|
| 108 |
foreach ($post_ids as $post_id) { |
| 109 |
$post_id = absint($post_id); |
| 110 |
if ($post_id) { |
| 111 |
$meta_cache[$post_id] = get_post_meta($post_id); |
| 112 |
} |
| 113 |
} |
| 114 |
|
| 115 |
foreach ($post_ids as $post_id) : |
| 116 |
$post_id = absint($post_id); |
| 117 |
if (!$post_id) { |
| 118 |
continue; |
| 119 |
} |
| 120 |
|
| 121 |
$post = get_post($post_id); |
| 122 |
if (!$post || !is_object($post)) { |
| 123 |
continue; |
| 124 |
} |
| 125 |
|
| 126 |
// Get meta data from cache |
| 127 |
$post_meta = isset($meta_cache[$post_id]) ? $meta_cache[$post_id] : array(); |
| 128 |
|
| 129 |
$d_i = isset($post_meta['avcp_data_inizio'][0]) ? sanitize_text_field($post_meta['avcp_data_inizio'][0]) : ''; |
| 130 |
$d_f = isset($post_meta['avcp_data_fine'][0]) ? sanitize_text_field($post_meta['avcp_data_fine'][0]) : ''; |
| 131 |
$cig = isset($post_meta['avcp_cig'][0]) ? sanitize_text_field($post_meta['avcp_cig'][0]) : ''; |
| 132 |
$aggiudicazione = isset($post_meta['avcp_aggiudicazione'][0]) ? sanitize_text_field($post_meta['avcp_aggiudicazione'][0]) : ''; |
| 133 |
$contraente = isset($post_meta['avcp_contraente'][0]) ? sanitize_text_field($post_meta['avcp_contraente'][0]) : ''; |
| 134 |
|
| 135 |
// Format dates safely |
| 136 |
$d_i_fmt = '-'; |
| 137 |
$d_f_fmt = '-'; |
| 138 |
|
| 139 |
if ($d_i && strtotime($d_i)) { |
| 140 |
$d_i_fmt = date_i18n("d/m/Y", strtotime($d_i)); |
| 141 |
} |
| 142 |
|
| 143 |
if ($d_f && strtotime($d_f)) { |
| 144 |
$d_f_fmt = date_i18n("d/m/Y", strtotime($d_f)); |
| 145 |
} |
| 146 |
|
| 147 |
// Output table row with proper escaping and accessibility |
| 148 |
echo '<tr role="row">'; |
| 149 |
|
| 150 |
// Title column with link |
| 151 |
$post_title = get_the_title($post_id); |
| 152 |
$post_permalink = get_permalink($post_id); |
| 153 |
|
| 154 |
if ($post_title && $post_permalink) { |
| 155 |
echo '<td colspan="2" role="gridcell">'; |
| 156 |
echo '<a href="' . esc_url($post_permalink) . '" title="' . esc_attr(sprintf(__('Visualizza dettagli di: %s', 'avcp'), $post_title)) . '">'; |
| 157 |
echo esc_html($post_title); |
| 158 |
echo '</a></td>'; |
| 159 |
} else { |
| 160 |
echo '<td colspan="2" role="gridcell">' . esc_html__('Oggetto non disponibile', 'avcp') . '</td>'; |
| 161 |
} |
| 162 |
|
| 163 |
// CIG column |
| 164 |
echo '<td role="gridcell">' . esc_html($cig) . '</td>'; |
| 165 |
|
| 166 |
// Amount column |
| 167 |
echo '<td role="gridcell" style="text-align: center;">€<strong>' . esc_html($aggiudicazione) . '</strong></td>'; |
| 168 |
|
| 169 |
// Duration column |
| 170 |
echo '<td role="gridcell" style="text-align: center;">'; |
| 171 |
echo '<div class="date-info">'; |
| 172 |
echo '<span class="start-date">' . esc_html($d_i_fmt) . '</span><br/>'; |
| 173 |
echo '<span class="end-date">' . esc_html($d_f_fmt) . '</span><br/>'; |
| 174 |
|
| 175 |
// Calculate duration if both dates are valid |
| 176 |
if (class_exists('DateTime') && $d_i && $d_f && strtotime($d_i) && strtotime($d_f)) { |
| 177 |
try { |
| 178 |
$date1 = new DateTime($d_i); |
| 179 |
$date2 = new DateTime($d_f); |
| 180 |
$diff = $date2->diff($date1); |
| 181 |
if ($diff && $diff->days !== false) { |
| 182 |
echo '<small class="duration"><strong>' . esc_html($diff->days) . '</strong> ' . esc_html__('gg', 'avcp') . '</small>'; |
| 183 |
} |
| 184 |
} catch (Exception $e) { |
| 185 |
// Log error if needed, but don't display to user |
| 186 |
error_log('AVCP Date calculation error: ' . $e->getMessage()); |
| 187 |
} |
| 188 |
} |
| 189 |
echo '</div></td>'; |
| 190 |
|
| 191 |
// Contract type column |
| 192 |
$contraente_display = ''; |
| 193 |
if ($contraente && strlen($contraente) > 3) { |
| 194 |
$contraente_display = strtolower(substr($contraente, 3)); |
| 195 |
} |
| 196 |
echo '<td role="gridcell">' . esc_html($contraente_display) . '</td>'; |
| 197 |
|
| 198 |
echo '</tr>'; |
| 199 |
|
| 200 |
endforeach; |
| 201 |
else : |
| 202 |
// No posts found |
| 203 |
echo '<tr role="row"><td colspan="6" role="gridcell" style="text-align: center; padding: 20px;">' . esc_html__('Nessun bando trovato per i criteri selezionati.', 'avcp') . '</td></tr>'; |
| 204 |
endif; |
| 205 |
|
| 206 |
echo '</tbody> |
| 207 |
<tfoot role="rowgroup"> |
| 208 |
<tr role="row"> |
| 209 |
<td colspan="6" role="gridcell" class="table-footer">'; |
| 210 |
|
| 211 |
// Export buttons section |
| 212 |
echo '<div class="export-buttons" style="float:right;" role="group" aria-label="' . esc_attr__('Opzioni di esportazione', 'avcp') . '">'; |
| 213 |
echo '<span class="export-label">' . esc_html__('Scarica in', 'avcp') . ':</span> '; |
| 214 |
|
| 215 |
// XML export |
| 216 |
$xml_url = get_site_url() . '/avcp'; |
| 217 |
echo '<a href="' . esc_url($xml_url) . '" target="_blank" rel="noopener" title="' . esc_attr__('Scarica file XML', 'avcp') . '" class="export-btn">'; |
| 218 |
echo '<button type="button" aria-label="' . esc_attr__('Esporta in formato XML', 'avcp') . '">' . esc_html__('XML', 'avcp') . '</button></a> '; |
| 219 |
|
| 220 |
// Excel export |
| 221 |
$site_name = sanitize_file_name(get_bloginfo('name')); |
| 222 |
$excel_filename = $site_name . '-gare' . sanitize_file_name($anno) . '.xls'; |
| 223 |
echo '<a download="' . esc_attr($excel_filename) . '" href="#" onclick="return ExcellentExport.excel(this, \'gare\', \'Gare\');" class="export-btn">'; |
| 224 |
echo '<button type="button" aria-label="' . esc_attr__('Esporta in formato Excel', 'avcp') . '">' . esc_html__('EXCEL', 'avcp') . '</button></a> '; |
| 225 |
|
| 226 |
// CSV export |
| 227 |
$csv_filename = $site_name . '-gare' . sanitize_file_name($anno) . '.csv'; |
| 228 |
echo '<a download="' . esc_attr($csv_filename) . '" href="#" onclick="return ExcellentExport.csv(this, \'gare\');" class="export-btn">'; |
| 229 |
echo '<button type="button" aria-label="' . esc_attr__('Esporta in formato CSV', 'avcp') . '">' . esc_html__('CSV', 'avcp') . '</button></a>'; |
| 230 |
|
| 231 |
echo '</div>'; |
| 232 |
|
| 233 |
// WPGov logo section |
| 234 |
if (get_option('wpgov_show_love')) { |
| 235 |
$logo_url = plugin_dir_url(__FILE__) . 'images/wpgov.png'; |
| 236 |
echo '<div class="wpgov-credit">'; |
| 237 |
echo '<a href="' . esc_url('https://www.wpgov.it') . '" target="_blank" rel="noopener" title="' . esc_attr__('Software sviluppato da WPGov', 'avcp') . '">'; |
| 238 |
echo '<img src="' . esc_url($logo_url) . '" alt="' . esc_attr__('Logo WPGov', 'avcp') . '" style="max-height: 30px;" /></a>'; |
| 239 |
echo '</div>'; |
| 240 |
} |
| 241 |
|
| 242 |
echo '</td> |
| 243 |
</tr> |
| 244 |
</tfoot> |
| 245 |
</table> |
| 246 |
</div>'; |
| 247 |
|
| 248 |
echo '<div class="clear"></div>'; |
| 249 |
?> |
| 250 |
|
| 251 |
<script type="text/javascript"> |
| 252 |
/** |
| 253 |
* AVCP Table Filter |
| 254 |
* Provides client-side filtering functionality for the tender table |
| 255 |
*/ |
| 256 |
(function(document) { |
| 257 |
'use strict'; |
| 258 |
|
| 259 |
var AVCPTableFilter = (function(Arr) { |
| 260 |
var _input; |
| 261 |
|
| 262 |
/** |
| 263 |
* Handle input events for table filtering |
| 264 |
* @param {Event} e - The input event |
| 265 |
*/ |
| 266 |
function _onInputEvent(e) { |
| 267 |
_input = e.target; |
| 268 |
var tables = document.getElementsByClassName(_input.getAttribute('data-table')); |
| 269 |
|
| 270 |
if (!tables || tables.length === 0) { |
| 271 |
return; |
| 272 |
} |
| 273 |
|
| 274 |
Arr.forEach.call(tables, function(table) { |
| 275 |
if (table.tBodies) { |
| 276 |
Arr.forEach.call(table.tBodies, function(tbody) { |
| 277 |
if (tbody.rows) { |
| 278 |
Arr.forEach.call(tbody.rows, _filter); |
| 279 |
} |
| 280 |
}); |
| 281 |
} |
| 282 |
}); |
| 283 |
} |
| 284 |
|
| 285 |
/** |
| 286 |
* Filter individual table rows based on search input |
| 287 |
* @param {HTMLElement} row - The table row to filter |
| 288 |
*/ |
| 289 |
function _filter(row) { |
| 290 |
if (!row || !row.textContent || !_input) { |
| 291 |
return; |
| 292 |
} |
| 293 |
|
| 294 |
var text = row.textContent.toLowerCase(); |
| 295 |
var val = _input.value.toLowerCase().trim(); |
| 296 |
|
| 297 |
// Show all rows if search is empty |
| 298 |
if (val === '') { |
| 299 |
row.style.display = 'table-row'; |
| 300 |
return; |
| 301 |
} |
| 302 |
|
| 303 |
// Hide/show row based on text match |
| 304 |
row.style.display = text.indexOf(val) === -1 ? 'none' : 'table-row'; |
| 305 |
} |
| 306 |
|
| 307 |
/** |
| 308 |
* Initialize the table filter functionality |
| 309 |
*/ |
| 310 |
function _init() { |
| 311 |
var inputs = document.getElementsByClassName('light-table-filter'); |
| 312 |
|
| 313 |
if (!inputs || inputs.length === 0) { |
| 314 |
return; |
| 315 |
} |
| 316 |
|
| 317 |
Arr.forEach.call(inputs, function(input) { |
| 318 |
if (input) { |
| 319 |
input.oninput = _onInputEvent; |
| 320 |
// Also handle paste events |
| 321 |
input.onpaste = function() { |
| 322 |
setTimeout(_onInputEvent.bind(null, {target: input}), 10); |
| 323 |
}; |
| 324 |
} |
| 325 |
}); |
| 326 |
} |
| 327 |
|
| 328 |
return { |
| 329 |
init: _init |
| 330 |
}; |
| 331 |
})(Array.prototype); |
| 332 |
|
| 333 |
/** |
| 334 |
* Initialize when DOM is ready |
| 335 |
*/ |
| 336 |
document.addEventListener('readystatechange', function() { |
| 337 |
if (document.readyState === 'complete') { |
| 338 |
AVCPTableFilter.init(); |
| 339 |
} |
| 340 |
}); |
| 341 |
|
| 342 |
// Fallback for older browsers |
| 343 |
if (document.readyState === 'complete') { |
| 344 |
AVCPTableFilter.init(); |
| 345 |
} |
| 346 |
|
| 347 |
})(document); |
| 348 |
</script> |
| 349 |
|