@@ -370,18 +370,177 @@ public function qso_details_ajax()
370370 */
371371 public function sota ()
372372 {
373+ $ this ->ensure_sota_dataset ();
373374
374- // Grab all worked sota stations
375- $ this ->load ->model ('sota ' );
376- $ data ['sota_all ' ] = $ this ->sota ->get_all ();
375+ $ this ->load ->model ('sota_model ' );
376+ $ this ->load ->model ('modes ' );
377+ $ this ->load ->model ('bands ' );
378+
379+ $ data ['worked_bands ' ] = $ this ->bands ->get_worked_bands ('sota ' );
380+ $ data ['modes ' ] = $ this ->modes ->active ();
381+
382+ $ refs = $ this ->sota_model ->get_unique_refs ();
383+ $ meta = $ this ->sota_model ->get_summits_meta ($ refs );
384+ $ data ['associations ' ] = $ this ->build_sota_options ($ meta , 'association ' );
385+ $ data ['regions ' ] = $ this ->build_sota_options ($ meta , 'region ' );
377386
378- // Render page
379387 $ data ['page_title ' ] = "Awards - SOTA " ;
380388 $ this ->load ->view ('interface_assets/header ' , $ data );
381- $ this ->load ->view ('awards/sota/index ' );
389+ $ this ->load ->view ('awards/sota/index ' , $ data );
382390 $ this ->load ->view ('interface_assets/footer ' );
383391 }
384392
393+ // HTMX: table fragment
394+ public function sota_table () {
395+ $ filters = $ this ->sota_filters_from_request ();
396+ $ this ->ensure_sota_dataset ();
397+ $ this ->load ->model ('sota_model ' );
398+ $ rows = $ this ->sota_model ->fetch_qsos ($ filters );
399+ $ meta = $ this ->sota_model ->get_summits_meta ($ this ->extract_sota_refs ($ rows ));
400+ $ rows = $ this ->filter_rows_by_meta ($ rows , $ meta , $ filters );
401+ $ filteredMeta = array_intersect_key ($ meta , array_fill_keys ($ this ->extract_sota_refs ($ rows ), true ));
402+ $ data = [
403+ 'rows ' => $ rows ,
404+ 'meta ' => $ filteredMeta ,
405+ 'filters ' => $ filters ,
406+ 'confirmed_refs ' => $ this ->confirmed_sota_refs ($ rows ),
407+ 'custom_date_format ' => $ this ->session ->userdata ('user_date_format ' ) ?: $ this ->config ->item ('qso_date_format ' ),
408+ ];
409+ $ this ->load ->view ('awards/sota/components/table ' , $ data );
410+ }
411+
412+ // HTMX: stats fragment
413+ public function sota_stats () {
414+ $ filters = $ this ->sota_filters_from_request ();
415+ $ this ->ensure_sota_dataset ();
416+ $ this ->load ->model ('sota_model ' );
417+ $ data ['total_uniques ' ] = $ this ->sota_model ->get_uniques ($ filters );
418+ $ data ['confirmed_uniques ' ] = $ this ->sota_model ->get_confirmations ($ filters );
419+ $ data ['first_last ' ] = $ this ->sota_model ->get_first_last ($ filters );
420+ $ data ['by_band ' ] = $ this ->sota_model ->get_uniques ($ filters , 'band ' );
421+ $ data ['by_mode ' ] = $ this ->sota_model ->get_uniques ($ filters , 'mode ' );
422+ $ data ['filters ' ] = $ filters ;
423+ $ this ->load ->view ('awards/sota/components/stats ' , $ data );
424+ }
425+
426+ // HTMX: map fragment
427+ public function sota_map () {
428+ $ filters = $ this ->sota_filters_from_request ();
429+ $ this ->ensure_sota_dataset ();
430+ $ this ->load ->model ('sota_model ' );
431+ $ rows = $ this ->sota_model ->fetch_qsos ($ filters );
432+ $ meta = $ this ->sota_model ->get_summits_meta ($ this ->extract_sota_refs ($ rows ));
433+ $ rows = $ this ->filter_rows_by_meta ($ rows , $ meta , $ filters );
434+ $ refs = $ this ->extract_sota_refs ($ rows );
435+
436+ // Group QSOs by SOTA ref for modal display
437+ $ qsos_by_ref = [];
438+ foreach ($ rows as $ row ) {
439+ $ ref = $ this ->normalize_sota_ref ($ row ->COL_SOTA_REF ?? null );
440+ if (!empty ($ ref )) {
441+ if (!isset ($ qsos_by_ref [$ ref ])) {
442+ $ qsos_by_ref [$ ref ] = [];
443+ }
444+ $ qsos_by_ref [$ ref ][] = $ row ;
445+ }
446+ }
447+
448+ $ data = [
449+ 'summits ' => array_intersect_key ($ meta , array_fill_keys ($ refs , true )),
450+ 'confirmed_refs ' => $ this ->confirmed_sota_refs ($ rows ),
451+ 'qsos_by_ref ' => $ qsos_by_ref ,
452+ 'custom_date_format ' => $ this ->session ->userdata ('user_date_format ' ) ?: $ this ->config ->item ('qso_date_format ' ),
453+ ];
454+ $ this ->load ->view ('awards/sota/components/map ' , $ data );
455+ }
456+
457+ private function ensure_sota_dataset () {
458+ $ fullPath = FCPATH . 'assets/json/sota_summits.csv ' ;
459+ $ autoPath = FCPATH . 'assets/json/sota.txt ' ;
460+ if (is_readable ($ fullPath ) && is_readable ($ autoPath )) {
461+ return ;
462+ }
463+ $ this ->load ->library ('sota ' , null , 'sotaLib ' );
464+ $ result = $ this ->sotaLib ->refreshFiles (true );
465+ if (!$ result ['ok ' ]) {
466+ log_message ('error ' , 'Unable to refresh SOTA dataset: ' . $ result ['message ' ]);
467+ }
468+ }
469+
470+ private function sota_filters_from_request () {
471+ $ payload = $ this ->input ->method () === 'post ' ? $ this ->input ->post () : $ this ->input ->get ();
472+ $ filters = [];
473+ $ filters ['from ' ] = $ this ->security ->xss_clean ($ payload ['from ' ] ?? null );
474+ $ filters ['to ' ] = $ this ->security ->xss_clean ($ payload ['to ' ] ?? null );
475+ $ filters ['band ' ] = $ this ->security ->xss_clean ($ payload ['band ' ] ?? 'All ' ) ?: 'All ' ;
476+ $ filters ['mode ' ] = $ this ->security ->xss_clean ($ payload ['mode ' ] ?? 'All ' ) ?: 'All ' ;
477+ $ filters ['association ' ] = $ this ->security ->xss_clean ($ payload ['association ' ] ?? null );
478+ $ filters ['region ' ] = $ this ->security ->xss_clean ($ payload ['region ' ] ?? null );
479+ $ filters ['confirmed ' ] = !empty ($ payload ['confirmed ' ]);
480+ return $ filters ;
481+ }
482+
483+ private function filter_rows_by_meta ($ rows , $ meta , $ filters ) {
484+ if (empty ($ filters ['association ' ]) && empty ($ filters ['region ' ])) {
485+ return $ rows ;
486+ }
487+
488+ $ out = [];
489+ foreach ($ rows as $ row ) {
490+ $ ref = $ this ->normalize_sota_ref ($ row ->COL_SOTA_REF ?? null );
491+ $ info = $ ref && isset ($ meta [$ ref ]) ? $ meta [$ ref ] : null ;
492+ if (!$ info ) {
493+ continue ;
494+ }
495+ if (!empty ($ filters ['association ' ]) && strcasecmp ($ info ['association ' ] ?? '' , $ filters ['association ' ]) !== 0 ) {
496+ continue ;
497+ }
498+ if (!empty ($ filters ['region ' ]) && strcasecmp ($ info ['region ' ] ?? '' , $ filters ['region ' ]) !== 0 ) {
499+ continue ;
500+ }
501+ $ out [] = $ row ;
502+ }
503+ return $ out ;
504+ }
505+
506+ private function extract_sota_refs ($ rows ) {
507+ $ refs = [];
508+ foreach ($ rows as $ r ) {
509+ $ ref = $ this ->normalize_sota_ref ($ r ->COL_SOTA_REF ?? null );
510+ if (!empty ($ ref )) {
511+ $ refs [$ ref ] = true ;
512+ }
513+ }
514+ return array_keys ($ refs );
515+ }
516+
517+ private function confirmed_sota_refs ($ rows ) {
518+ $ refs = [];
519+ foreach ($ rows as $ r ) {
520+ $ ref = $ this ->normalize_sota_ref ($ r ->COL_SOTA_REF ?? null );
521+ if (!empty ($ ref ) && (($ r ->col_qsl_rcvd ?? '' ) === 'Y ' || ($ r ->col_lotw_qsl_rcvd ?? '' ) === 'Y ' || ($ r ->COL_QSL_RCVD ?? '' ) === 'Y ' || ($ r ->COL_LOTW_QSL_RCVD ?? '' ) === 'Y ' )) {
522+ $ refs [$ ref ] = true ;
523+ }
524+ }
525+ return array_keys ($ refs );
526+ }
527+
528+ private function normalize_sota_ref ($ ref ) {
529+ return strtoupper (trim ((string )$ ref ));
530+ }
531+
532+ private function build_sota_options ($ meta , $ field ) {
533+ $ values = [];
534+ foreach ($ meta as $ info ) {
535+ if (!empty ($ info [$ field ])) {
536+ $ values [] = $ info [$ field ];
537+ }
538+ }
539+ $ values = array_values (array_unique ($ values ));
540+ sort ($ values , SORT_NATURAL | SORT_FLAG_CASE );
541+ return $ values ;
542+ }
543+
385544 /*
386545 Handles showing worked WWFFs
387546 Comment field - WWFF:#
@@ -406,18 +565,74 @@ public function wwff()
406565 */
407566 public function pota ()
408567 {
568+ // Render the POTA dashboard shell; content loaded via HTMX
569+ $ this ->load ->model ('modes ' );
570+ $ this ->load ->model ('bands ' );
409571
410- // Grab all worked pota stations
411- $ this ->load ->model ('pota ' );
412- $ data ['pota_all ' ] = $ this ->pota ->get_all ();
413-
414- // Render page
415572 $ data ['page_title ' ] = "Awards - POTA " ;
573+ $ data ['worked_bands ' ] = $ this ->bands ->get_worked_bands ('pota ' );
574+ $ data ['modes ' ] = $ this ->modes ->active ();
575+
416576 $ this ->load ->view ('interface_assets/header ' , $ data );
417- $ this ->load ->view ('awards/pota/index ' );
577+ $ this ->load ->view ('awards/pota/index ' , $ data );
418578 $ this ->load ->view ('interface_assets/footer ' );
419579 }
420580
581+ // HTMX: table fragment
582+ public function pota_table () {
583+ $ filters = $ this ->pota_filters_from_request ();
584+ $ this ->load ->model ('pota ' );
585+ $ data ['rows ' ] = $ this ->pota ->fetch_qsos ($ filters );
586+ $ data ['filters ' ] = $ filters ;
587+ $ this ->load ->view ('awards/pota/components/table ' , $ data );
588+ }
589+
590+ // HTMX: stats fragment (totals, first/last)
591+ public function pota_stats () {
592+ $ filters = $ this ->pota_filters_from_request ();
593+ $ this ->load ->model ('pota ' );
594+ $ data ['total_uniques ' ] = $ this ->pota ->get_uniques ($ filters );
595+ $ data ['confirmed_uniques ' ] = $ this ->pota ->get_confirmations ($ filters );
596+ $ data ['first_last ' ] = $ this ->pota ->get_first_last ($ filters );
597+ $ data ['filters ' ] = $ filters ;
598+ $ this ->load ->view ('awards/pota/components/stats ' , $ data );
599+ }
600+
601+ // HTMX: progress fragment (threshold bars)
602+ public function pota_progress () {
603+ $ filters = $ this ->pota_filters_from_request ();
604+ $ this ->load ->model ('pota ' );
605+ $ worked = $ this ->pota ->get_uniques ($ filters );
606+ $ thresholds = [10 ,25 ,50 ,100 ];
607+ $ data = [
608+ 'worked ' => $ worked ,
609+ 'thresholds ' => $ thresholds ,
610+ ];
611+ $ this ->load ->view ('awards/pota/components/progress ' , $ data );
612+ }
613+
614+ // HTMX: map fragment (Leaflet markers)
615+ public function pota_map () {
616+ $ filters = $ this ->pota_filters_from_request ();
617+ $ this ->load ->model ('pota ' );
618+ $ rows = $ this ->pota ->fetch_qsos ($ filters );
619+ $ refs = [];
620+ foreach ($ rows as $ r ) { $ refs [$ r ->COL_POTA_REF ] = true ; }
621+ $ refs = array_keys ($ refs );
622+ $ data ['parks ' ] = $ this ->pota ->get_parks_meta ($ refs );
623+ $ this ->load ->view ('awards/pota/components/map ' , $ data );
624+ }
625+
626+ private function pota_filters_from_request () {
627+ $ filters = [];
628+ $ filters ['from ' ] = $ this ->security ->xss_clean ($ this ->input ->get ('from ' ));
629+ $ filters ['to ' ] = $ this ->security ->xss_clean ($ this ->input ->get ('to ' ));
630+ $ filters ['band ' ] = $ this ->security ->xss_clean ($ this ->input ->get ('band ' )) ?: 'All ' ;
631+ $ filters ['mode ' ] = $ this ->security ->xss_clean ($ this ->input ->get ('mode ' )) ?: 'All ' ;
632+ $ filters ['confirmed ' ] = $ this ->input ->get ('confirmed ' ) ? true : false ;
633+ return $ filters ;
634+ }
635+
421636 public function cq ()
422637 {
423638 $ CI = &get_instance ();
0 commit comments