diff --git a/application/config/migration.php b/application/config/migration.php index 8ccc1dac..60928918 100644 --- a/application/config/migration.php +++ b/application/config/migration.php @@ -22,7 +22,7 @@ $config['migration_enabled'] = TRUE; | */ -$config['migration_version'] = 203; +$config['migration_version'] = 204; /* |-------------------------------------------------------------------------- diff --git a/application/controllers/Api.php b/application/controllers/Api.php index 3cc8bb03..afdbeca7 100644 --- a/application/controllers/Api.php +++ b/application/controllers/Api.php @@ -814,4 +814,129 @@ class API extends CI_Controller { $latlng = $this->qra->qra2latlong($qra); return $latlng; } + + /** + * API endpoint to get recent QSOs from a public logbook + * + * @api GET /api/recent_qsos/{public_slug}/{limit} + * + * @param string public_slug Required. Public slug identifier for the logbook + * @param int limit Optional. Number of QSOs to return (default: 10, max: 50) + * + * @return json Returns JSON array with recent QSO data or error message + * + * @throws 404 Not Found - Logbook not found or empty logbook + * @throws 400 Bad Request - Invalid limit parameter + * + * @example + * Request: GET /api/recent_qsos/my-public-logbook/5 + * + * Response: + * { + * "qsos": [ + * { + * "date": "2024-01-15", + * "time": "14:30", + * "callsign": "W1AW", + * "mode": "SSB", + * "band": "20M", + * "rst_sent": "59", + * "rst_rcvd": "59" + * } + * ], + * "count": 1, + * "logbook_slug": "my-public-logbook" + * } + */ + function recent_qsos($public_slug = null, $limit = 10) { + header('Content-type: application/json'); + + if($public_slug == null) { + http_response_code(400); + echo json_encode(['status' => 'failed', 'reason' => 'missing public_slug parameter']); + return; + } + + // Validate and sanitize limit parameter + $limit = intval($limit); + if ($limit <= 0) { + $limit = 10; // default + } + if ($limit > 50) { + $limit = 50; // maximum + } + + $this->load->model('logbooks_model'); + $this->load->model('logbook_model'); + + if($this->logbooks_model->public_slug_exists($public_slug)) { + $logbook_id = $this->logbooks_model->public_slug_exists_logbook_id($public_slug); + if($logbook_id != false) { + // Get associated station locations for mysql queries + $logbooks_locations_array = $this->logbooks_model->list_logbook_relationships($logbook_id); + + if (!$logbooks_locations_array) { + http_response_code(404); + echo json_encode(['status' => 'failed', 'reason' => 'Empty Logbook']); + return; + } + + // Get recent QSOs using existing method + $recent_qsos_query = $this->logbook_model->get_last_qsos($limit, $logbooks_locations_array); + + if ($recent_qsos_query == null) { + http_response_code(404); + echo json_encode(['status' => 'failed', 'reason' => 'No QSOs found']); + return; + } + + // Format the data for JSON response + $qsos = array(); + foreach ($recent_qsos_query->result() as $row) { + $qso = array( + 'date' => date('Y-m-d', strtotime($row->COL_TIME_ON)), + 'time' => date('H:i', strtotime($row->COL_TIME_ON)), + 'callsign' => strtoupper($row->COL_CALL), + 'mode' => $row->COL_SUBMODE ? $row->COL_SUBMODE : $row->COL_MODE, + 'band' => $row->COL_SAT_NAME ? $row->COL_SAT_NAME : $row->COL_BAND, + 'rst_sent' => $row->COL_RST_SENT, + 'rst_rcvd' => $row->COL_RST_RCVD + ); + + // Add optional fields if they exist + if ($row->COL_STX_STRING) { + $qso['stx_string'] = $row->COL_STX_STRING; + } + if ($row->COL_SRX_STRING) { + $qso['srx_string'] = $row->COL_SRX_STRING; + } + if ($row->COL_GRIDSQUARE) { + $qso['gridsquare'] = $row->COL_GRIDSQUARE; + } + if ($row->COL_QTH) { + $qso['qth'] = $row->COL_QTH; + } + if ($row->COL_NAME) { + $qso['name'] = $row->COL_NAME; + } + + $qsos[] = $qso; + } + + http_response_code(200); + echo json_encode([ + 'qsos' => $qsos, + 'count' => count($qsos), + 'logbook_slug' => $public_slug + ], JSON_PRETTY_PRINT); + + } else { + http_response_code(404); + echo json_encode(['status' => 'failed', 'reason' => $public_slug.' has no associated station locations']); + } + } else { + http_response_code(404); + echo json_encode(['status' => 'failed', 'reason' => 'logbook not found']); + } + } } diff --git a/application/controllers/Qrz.php b/application/controllers/Qrz.php index 4190f79b..d32a88e1 100644 --- a/application/controllers/Qrz.php +++ b/application/controllers/Qrz.php @@ -324,7 +324,7 @@ class Qrz extends CI_Controller { // Return the structured error array here too for consistency return ['status' => 'error', 'message' => $error_message]; } - $url = 'http://logbook.qrz.com/api'; // Correct URL + $url = 'https://logbook.qrz.com/api'; // Correct URL $post_data['KEY'] = $qrz_api_key; // Correct parameter $post_data['ACTION'] = 'FETCH'; // Correct parameter @@ -336,11 +336,35 @@ class Qrz extends CI_Controller { curl_setopt( $ch, CURLOPT_FOLLOWLOCATION, 1); // Okay curl_setopt( $ch, CURLOPT_HEADER, 0); // Correct - don't need response headers curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true); // Correct - get response as string + curl_setopt( $ch, CURLOPT_TIMEOUT, 300); // 5 minute timeout + curl_setopt( $ch, CURLOPT_CONNECTTIMEOUT, 30); // 30 second connection timeout + curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); + curl_setopt($ch, CURLOPT_BUFFERSIZE, 128000); + curl_setopt($ch, CURLOPT_ENCODING, 'gzip, deflate'); $content = curl_exec($ch); // Get raw content $curl_error = curl_error($ch); // Check for cURL errors + $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); // Get HTTP response code curl_close($ch); + if ($curl_error) { // Check for cURL level errors first + $error_message = "QRZ download cURL error: " . $curl_error; + log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); + return ['status' => 'error', 'message' => $error_message]; + } + + if ($http_code !== 200) { + $error_message = "QRZ download HTTP error: HTTP " . $http_code; + log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); + return ['status' => 'error', 'message' => $error_message]; + } + + if ($content === false || $content === '') { // Check if curl_exec failed or returned empty + $error_message = "QRZ download failed: No content received from QRZ.com."; + log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); + return ['status' => 'error', 'message' => $error_message]; + } + // Find the start of the ADIF data after "ADIF=" $adif_start_pos = strpos($content, 'ADIF='); if ($adif_start_pos !== false) { @@ -388,18 +412,6 @@ class Qrz extends CI_Controller { $content = substr($content, 0, $truncate_pos); } - if ($curl_error) { // Check for cURL level errors first - $error_message = "QRZ download cURL error: " . $curl_error; - log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); - return ['status' => 'error', 'message' => $error_message]; - } - - if ($content === false || $content === '') { // Check if curl_exec failed or returned empty - $error_message = "QRZ download failed: No content received from QRZ.com."; - log_message('error', $error_message . ' API Key used: ' . $qrz_api_key); - return ['status' => 'error', 'message' => $error_message]; - } - // Check for QRZ API specific error messages if (strpos($content, 'STATUS=FAIL') !== false || strpos($content, 'STATUS=AUTH') !== false) { // Extract reason if possible, otherwise use full content @@ -446,7 +458,7 @@ class Qrz extends CI_Controller { $config['qrz_rcvd_mark'] = 'Y'; ini_set('memory_limit', '-1'); - set_time_limit(0); + set_time_limit(1800); // 30 minutes max execution time instead of unlimited $this->load->library('adif_parser'); @@ -475,8 +487,24 @@ class Qrz extends CI_Controller { $batch_data = []; $batch_size = 500; // Process 500 records at a time $record_count = 0; // Initialize record counter + $max_records = 50000; // Safety limit to prevent runaway processing + $start_time = time(); // Track processing time + $max_processing_time = 1200; // 20 minutes max for processing + while ($record = $this->adif_parser->get_record()) { $record_count++; // Increment counter for each record read + + // Safety checks to prevent runaway processing + if ($record_count > $max_records) { + log_message('error', 'QRZ download: Exceeded maximum record limit of ' . $max_records . ' records. Processing stopped.'); + break; + } + + if ((time() - $start_time) > $max_processing_time) { + log_message('error', 'QRZ download: Exceeded maximum processing time of ' . $max_processing_time . ' seconds. Processing stopped at record ' . $record_count . '.'); + break; + } + if ((!(isset($record['app_qrzlog_qsldate']))) || (!(isset($record['qso_date'])))) { continue; } @@ -509,6 +537,12 @@ class Qrz extends CI_Controller { if (count($batch_data) >= $batch_size) { $table .= $this->logbook_model->process_qrz_batch($batch_data); $batch_data = []; // Reset batch + + // Log progress every 1000 records to help monitor long-running processes + if ($record_count % 1000 == 0) { + $elapsed_time = time() - $start_time; + log_message('info', 'QRZ download progress: ' . $record_count . ' records processed in ' . $elapsed_time . ' seconds.'); + } } } @@ -517,6 +551,10 @@ class Qrz extends CI_Controller { $table .= $this->logbook_model->process_qrz_batch($batch_data); } + // Log successful completion with statistics + $processing_time = time() - $start_time; + log_message('info', 'QRZ download completed successfully. Processed ' . $record_count . ' records in ' . $processing_time . ' seconds.'); + if ($table != "") { $data['tableheaders'] = $tableheaders; $data['table'] = $table; diff --git a/application/controllers/Qso.php b/application/controllers/Qso.php index a78840bc..d92f2b51 100755 --- a/application/controllers/Qso.php +++ b/application/controllers/Qso.php @@ -40,6 +40,13 @@ class QSO extends CI_Controller { $data['bands'] = $this->bands->get_user_bands_for_qso_entry(); $data['user_default_band'] = $this->session->userdata('user_default_band'); $data['sat_active'] = array_search("SAT", $this->bands->get_user_bands(), true); + + // Set user's preferred date format + if($this->session->userdata('user_date_format')) { + $data['user_date_format'] = $this->session->userdata('user_date_format'); + } else { + $data['user_date_format'] = $this->config->item('qso_date_format'); + } $this->load->library('form_validation'); diff --git a/application/controllers/Station.php b/application/controllers/Station.php index f0600ec1..7a2f4e55 100644 --- a/application/controllers/Station.php +++ b/application/controllers/Station.php @@ -83,6 +83,8 @@ class Station extends CI_Controller { $this->load->view('station_profile/edit'); $this->load->view('interface_assets/footer'); } else { + // Get all the posted data from the form and save it to log file + if ($this->stations->edit() !== false) { // [eQSL default msg] ADD to user options (option_type='eqsl_default_qslmsg'; option_name='key_station_id'; option_key=station_id; option_value=value) // $eqsl_default_qslmsg = xss_clean($this->input->post('eqsl_default_qslmsg', true)); diff --git a/application/migrations/204_tag_2_6_22.php b/application/migrations/204_tag_2_6_22.php new file mode 100644 index 00000000..35a8bc6b --- /dev/null +++ b/application/migrations/204_tag_2_6_22.php @@ -0,0 +1,30 @@ +db->where('option_name', 'version'); + $this->db->update('options', array('option_value' => '2.6.22')); + + // Trigger Version Info Dialog + $this->db->where('option_type', 'version_dialog'); + $this->db->where('option_name', 'confirmed'); + $this->db->update('user_options', array('option_value' => 'false')); + + } + + public function down() + { + $this->db->where('option_name', 'version'); + $this->db->update('options', array('option_value' => '2.6.21')); + } +} \ No newline at end of file diff --git a/application/models/Logbook_model.php b/application/models/Logbook_model.php index 49a7d9ce..0b7e394d 100755 --- a/application/models/Logbook_model.php +++ b/application/models/Logbook_model.php @@ -8,10 +8,11 @@ class Logbook_model extends CI_Model { $callsign = str_replace('Ø', '0', $this->input->post('callsign')); - // Join date+time - $datetime = date("Y-m-d", strtotime($this->input->post('start_date'))) . " " . $this->input->post('start_time'); + // Join date+time - Parse date according to user's format preference + $parsed_date = $this->parse_user_date($this->input->post('start_date')); + $datetime = $parsed_date . " " . $this->input->post('start_time'); if ($this->input->post('end_time') != null) { - $datetime_off = date("Y-m-d", strtotime($this->input->post('start_date'))) . " " . $this->input->post('end_time'); + $datetime_off = $parsed_date . " " . $this->input->post('end_time'); // if time off < time on, and time off is on 00:xx >> add 1 day (concidering start and end are between 23:00 and 00:59) // $_tmp_datetime_off = strtotime($datetime_off); if (($_tmp_datetime_off < strtotime($datetime)) && (substr($this->input->post('end_time'), 0, 2) == "00")) { @@ -4888,7 +4889,7 @@ class Logbook_model extends CI_Model foreach ($batch_data as $record) { $this->db->or_group_start(); // Start group for this record's AND conditions $this->db->where($this->config->item('table_name').'.COL_CALL', $record['call']); - $this->db->where($this->config->item('table_name').'.COL_TIME_ON', $record['time_on']); + $this->db->like($this->config->item('table_name').'.COL_TIME_ON', $record['time_on'], 'after'); $this->db->where($this->config->item('table_name').'.COL_BAND', $record['band']); $this->db->group_end(); // End group for this record's AND conditions } @@ -4901,7 +4902,8 @@ class Logbook_model extends CI_Model // Index DB results for faster lookup $indexed_results = []; foreach ($db_results as $row) { - $key = $row['COL_CALL'] . '|' . $row['COL_TIME_ON'] . '|' . $row['COL_BAND']; + $time = substr($row['COL_TIME_ON'], 0, 16); + $key = $row['COL_CALL'] . '|' . $time . '|' . $row['COL_BAND']; $indexed_results[$key] = $row['COL_PRIMARY_KEY']; } @@ -4919,7 +4921,7 @@ class Logbook_model extends CI_Model $update_batch_data[] = [ 'COL_PRIMARY_KEY' => $primary_key, 'COL_QRZCOM_QSO_DOWNLOAD_DATE' => $record['qsl_date'], - 'COL_QRZCOM_QSO_UPLOAD_STATUS' => $record['qsl_rcvd'] // Should be 'Y' if confirmed + 'COL_QRZCOM_QSO_DOWNLOAD_STATUS' => $record['qsl_rcvd'] // Should be 'Y' if confirmed ]; } @@ -4942,6 +4944,42 @@ class Logbook_model extends CI_Model // Step 6: Return Table HTML return $table; } + + /** + * Parse date from user input according to user's preferred date format + * @param string $date_input The date string from user input + * @param string $user_format The user's preferred date format (e.g., 'd/m/Y', 'Y-m-d') + * @return string Returns date in Y-m-d format for database storage, or original input if parsing fails + */ + private function parse_user_date($date_input, $user_format = null) { + if (empty($date_input)) { + return $date_input; + } + + // If no user format provided, try to get it from session or config + if ($user_format === null) { + if ($this->session->userdata('user_date_format')) { + $user_format = $this->session->userdata('user_date_format'); + } else { + $user_format = $this->config->item('qso_date_format'); + } + } + + // Try to parse with the user's format first + $date = DateTime::createFromFormat($user_format, $date_input); + if ($date !== false) { + return $date->format('Y-m-d'); + } + + // Fallback to strtotime for formats it can handle (mostly Y-m-d, m/d/Y, etc.) + $timestamp = strtotime($date_input); + if ($timestamp !== false) { + return date('Y-m-d', $timestamp); + } + + // If all parsing fails, return the original input and let the database handle it + return $date_input; + } } // Function to validate ADIF date format @@ -4952,4 +4990,4 @@ function validateADIFDate($date, $format = 'Ymd') { $d = DateTime::createFromFormat($format, $date); return $d && $d->format($format) == $date; -} \ No newline at end of file +} diff --git a/application/models/Stations.php b/application/models/Stations.php index 03463427..83670c1e 100644 --- a/application/models/Stations.php +++ b/application/models/Stations.php @@ -77,9 +77,9 @@ class Stations extends CI_Model { // Check if the state is Canada and get the correct state if ($this->input->post('dxcc') == 1 && $this->input->post('station_ca_state') !="") { - $state = $this->input->post('station_ca_state'); + $state = xss_clean($this->input->post('station_ca_state', true)); } else { - $state = $this->input->post('station_state'); + $state = xss_clean($this->input->post('station_state', true)); } // Create data array with field values @@ -131,9 +131,9 @@ class Stations extends CI_Model { // Check if the state is Canada and get the correct state if ($this->input->post('dxcc') == 1 && $this->input->post('station_ca_state') !="") { - $state = $this->input->post('station_ca_state'); + $state = xss_clean($this->input->post('station_ca_state', true)); } else { - $state = $this->input->post('station_state'); + $state = xss_clean($this->input->post('station_state', true)); } $data = array( diff --git a/application/views/contesting/index.php b/application/views/contesting/index.php index 1ca451bd..54425989 100644 --- a/application/views/contesting/index.php +++ b/application/views/contesting/index.php @@ -102,48 +102,48 @@
- +
- +
- +
diff --git a/application/views/qslprint/qslprint.php b/application/views/qslprint/qslprint.php index 90cced00..1d434248 100644 --- a/application/views/qslprint/qslprint.php +++ b/application/views/qslprint/qslprint.php @@ -45,7 +45,7 @@ if ($qsos->result() != NULL) { foreach ($qsos->result() as $qsl) { echo ''; - echo '
'; + echo '
'; ?>COL_CALL)); ?>Lookup <?php echo strtoupper($qsl->COL_CALL); ?> on QRZ.com Lookup <?php echo strtoupper($qsl->COL_CALL); ?> on HamQTH Lookup <?php echo strtoupper($qsl->COL_CALL); ?> on eQSL.cc'; $timestamp = strtotime($qsl->COL_TIME_ON); echo date($custom_date_format, $timestamp); echo ''; echo ''; $timestamp = strtotime($qsl->COL_TIME_ON); echo date('H:i', $timestamp); echo ''; diff --git a/application/views/qso/index.php b/application/views/qso/index.php index 5993a1a3..e7ad97f5 100755 --- a/application/views/qso/index.php +++ b/application/views/qso/index.php @@ -69,8 +69,8 @@ required pattern="[0-3][0-9]-[0-1][0-9]-[0-9]{4}"> + echo date($user_date_format); + } ?>" required>
@@ -102,7 +102,7 @@ - +
@@ -113,8 +113,8 @@ required pattern="[0-3][0-9]-[0-1][0-9]-[0-9]{4}"> + echo date($user_date_format); + } ?>" required>
@@ -131,7 +131,7 @@ - +
diff --git a/assets/js/sections/common.js b/assets/js/sections/common.js index 4a73a34e..b899327d 100644 --- a/assets/js/sections/common.js +++ b/assets/js/sections/common.js @@ -634,5 +634,7 @@ if ($('.table-responsive .dropdown-toggle').length>0) { } function getDataTablesLanguageUrl() { - return base_url + "/assets/json/datatables_languages/" + lang_datatables_language + ".json"; + // Check if lang_datatables_language is defined, otherwise use a default + var language = (typeof lang_datatables_language !== 'undefined') ? lang_datatables_language : 'english'; + return base_url + "/assets/json/datatables_languages/" + language + ".json"; } diff --git a/assets/js/sections/qslprint.js b/assets/js/sections/qslprint.js index a08167fa..afddcdd4 100644 --- a/assets/js/sections/qslprint.js +++ b/assets/js/sections/qslprint.js @@ -77,20 +77,93 @@ $(".station_id").change(function(){ type: 'post', data: {'station_id': station_id}, success: function(html) { - $('.resulttable').empty(); - $('.resulttable').append(html); + try { + // Destroy existing DataTable if it exists + if ($.fn.DataTable.isDataTable('#qslprint_table')) { + $('#qslprint_table').DataTable().destroy(); + } + $('.resulttable').empty(); + $('.resulttable').append(html); + // Reinitialize DataTable + $('#qslprint_table').DataTable({ + "stateSave": true, + paging: false, + "language": { + url: getDataTablesLanguageUrl(), + }, + "drawCallback": function(settings) { + // Re-attach event handlers after DataTable draws/redraws + attachCheckboxEvents(); + } + }); + // Attach checkbox events immediately after initialization + attachCheckboxEvents(); + } catch (error) { + console.error('Error reinitializing DataTable:', error); + } } }); }); -$('#qslprint_table').DataTable({ - "stateSave": true, - paging: false, - "language": { - url: getDataTablesLanguageUrl(), +// Initialize DataTable only if it exists and isn't already initialized +$(document).ready(function() { + try { + if ($('#qslprint_table').length && !$.fn.DataTable.isDataTable('#qslprint_table')) { + $('#qslprint_table').DataTable({ + "stateSave": true, + paging: false, + "language": { + url: getDataTablesLanguageUrl(), + }, + "drawCallback": function(settings) { + // Re-attach event handlers after DataTable draws/redraws + attachCheckboxEvents(); + } + }); + } + // Initial attachment of events + attachCheckboxEvents(); + } catch (error) { + console.error('Error initializing DataTable:', error); + // Still try to attach checkbox events even if DataTable fails + attachCheckboxEvents(); } }); +// Function to attach checkbox events +function attachCheckboxEvents() { + // Remove any existing handlers to prevent duplicates + $('#checkBoxAll').off('change.qslprint'); + $('.qso-checkbox').off('click.qslprint'); + + // Attach select all functionality + $('#checkBoxAll').on('change.qslprint', function (event) { + var isChecked = this.checked; + $('#qslprint_table tbody tr .qso-checkbox').each(function (i) { + $(this).prop("checked", isChecked); + if (isChecked) { + $(this).closest('tr').addClass('activeRow'); + } else { + $(this).closest('tr').removeClass('activeRow'); + } + }); + }); + + // Attach individual checkbox functionality + $(document).on('click.qslprint', '.qso-checkbox', function() { + if ($(this).is(":checked")) { + $(this).closest('tr').addClass('activeRow'); + } else { + $(this).closest('tr').removeClass('activeRow'); + } + + // Update the "select all" checkbox state + var totalCheckboxes = $('#qslprint_table tbody tr .qso-checkbox').length; + var checkedCheckboxes = $('#qslprint_table tbody tr .qso-checkbox:checked').length; + $('#checkBoxAll').prop('checked', totalCheckboxes === checkedCheckboxes); + }); +} + function showOqrs(id) { $.ajax({ url: base_url + 'index.php/qslprint/show_oqrs', @@ -135,30 +208,8 @@ function mark_qsl_sent(id, method) { }); } -$('#checkBoxAll').change(function (event) { - if (this.checked) { - $('.qslprint tbody tr').each(function (i) { - $(this).closest('tr').addClass('activeRow'); - $(this).closest('tr').find("input[type=checkbox]").prop("checked", true); - }); - } else { - $('.qslprint tbody tr').each(function (i) { - $(this).closest('tr').removeClass('activeRow'); - $(this).closest('tr').find("input[type=checkbox]").prop("checked", false); - }); - } -}); - -$('.qslprint').on('click', 'input[type="checkbox"]', function() { - if ($(this).is(":checked")) { - $(this).closest('tr').addClass('activeRow'); - } else { - $(this).closest('tr').removeClass('activeRow'); - } -}); - function markSelectedQsos() { - var elements = $('.qslprint tbody input:checked'); + var elements = $('.qso-checkbox:checked'); var nElements = elements.length; if (nElements == 0) { return; @@ -179,18 +230,22 @@ function markSelectedQsos() { 'method' : '' }, success: function(data) { - if (data !== []) { + if (data && data.length > 0) { $.each(data, function(k, v) { $("#qslprint_"+this.qsoID).remove(); }); } $('.markallprinted').prop("disabled", false); + }, + error: function(xhr, status, error) { + console.error('Error marking QSOs as printed:', error); + $('.markallprinted').prop("disabled", false); } }); } function removeSelectedQsos() { - var elements = $('.qslprint tbody input:checked'); + var elements = $('.qso-checkbox:checked'); var nElements = elements.length; if (nElements == 0) { return; @@ -213,12 +268,16 @@ function removeSelectedQsos() { 'method' : '' }, success: function(data) { - if (data !== []) { + if (data && data.length > 0) { $.each(data, function(k, v) { $("#qslprint_"+this.qsoID).remove(); }); } $('.removeall').prop("disabled", false); + }, + error: function(xhr, status, error) { + console.error('Error removing QSOs from queue:', error); + $('.removeall').prop("disabled", false); } }); }