@@ -201,6 +201,17 @@ def render_missing_parts_by_set(set_results: Dict, merged_df: pd.DataFrame,
201201 "is_spare" : location_info .get ("is_spare" , False )
202202 })
203203
204+ # Deduplicate parts within each set (same part+color: sum quantities)
205+ for set_key in sets_dict :
206+ seen = {}
207+ for entry in sets_dict [set_key ]:
208+ dedup_key = (entry ["part_num" ], entry ["color_name" ])
209+ if dedup_key in seen :
210+ seen [dedup_key ]["quantity" ] += entry ["quantity" ]
211+ else :
212+ seen [dedup_key ] = entry .copy ()
213+ sets_dict [set_key ] = list (seen .values ())
214+
204215 # Display each set with its parts (layout matching location cards)
205216 for set_key , parts_list in sorted (sets_dict .items ()):
206217 with st .expander (f"📦 { set_key } ({ len (parts_list )} part type(s))" , expanded = True ):
@@ -714,3 +725,131 @@ def render_missing_parts_export(merged: pd.DataFrame) -> None:
714725
715726 st .download_button ("📥 Export Missing Parts (Rebrickable Format)" ,
716727 export_csv , "missing_parts_rebrickable.csv" , type = "primary" )
728+
729+
730+ def render_direct_set_search_section (wanted_parts : list , sets_manager ) -> None :
731+ """
732+ Render set search interface for Alternative B (search in owned sets only).
733+
734+ Unlike render_set_search_section, this does not depend on a merged_df.
735+ It takes a pre-built list of (part_num, color_name) tuples and searches
736+ directly in owned sets. Uses separate session state keys (*_b) to avoid
737+ conflicts with Alternative A.
738+
739+ Args:
740+ wanted_parts: List of (part_number, color_name) tuples to search for
741+ sets_manager: SetsManager instance for accessing set data
742+ """
743+ import streamlit as st
744+
745+ if not wanted_parts :
746+ st .info ("No wanted parts to search for." )
747+ return
748+
749+ # Load sets metadata
750+ if st .session_state .get ("sets_data_loaded" , False ) and st .session_state .get ("sets_metadata" ) is not None :
751+ all_sets = st .session_state ["sets_metadata" ]
752+ sets_by_source = {}
753+ for set_data in all_sets :
754+ source = set_data ["source_csv" ]
755+ if source not in sets_by_source :
756+ sets_by_source [source ] = []
757+ sets_by_source [source ].append (set_data )
758+ else :
759+ sets_by_source = sets_manager .get_sets_by_source ()
760+
761+ if not sets_by_source :
762+ st .info ("📭 No sets found. Add sets on the 'My Collection - Sets' page." )
763+ return
764+
765+ total_fetched = sum (
766+ 1 for sets_list in sets_by_source .values ()
767+ for s in sets_list if s .get ("inventory_fetched" , False )
768+ )
769+ if total_fetched == 0 :
770+ st .info ("📭 No set inventories available. Add sets and retrieve inventories on the 'My Collection - Sets' page." )
771+ return
772+
773+ # Set selection interface
774+ st .markdown ("#### Select Sets to Search" )
775+ st .markdown ("Choose which sets to search for the wanted parts:" )
776+
777+ if "selected_sets_for_search_b" not in st .session_state :
778+ st .session_state ["selected_sets_for_search_b" ] = set ()
779+
780+ for source_name , sets_list in sorted (sets_by_source .items ()):
781+ fetched_sets = [s for s in sets_list if s .get ("inventory_fetched" , False )]
782+ unfetched_sets = [s for s in sets_list if not s .get ("inventory_fetched" , False )]
783+
784+ with st .expander (f"📁 { source_name } ({ len (fetched_sets )} /{ len (sets_list )} set(s) with inventory)" , expanded = True ):
785+ if fetched_sets :
786+ col1 , col2 = st .columns ([1 , 1 ])
787+ with col1 :
788+ if st .button ("Select All" , key = f"b_select_all_{ source_name } " ):
789+ for set_data in fetched_sets :
790+ set_num = set_data ["set_number" ]
791+ st .session_state ["selected_sets_for_search_b" ].add (set_num )
792+ st .session_state [f"b_set_checkbox_{ set_num } " ] = True
793+ st .rerun ()
794+ with col2 :
795+ if st .button ("Deselect All" , key = f"b_deselect_all_{ source_name } " ):
796+ for set_data in fetched_sets :
797+ set_num = set_data ["set_number" ]
798+ st .session_state ["selected_sets_for_search_b" ].discard (set_num )
799+ st .session_state [f"b_set_checkbox_{ set_num } " ] = False
800+ st .rerun ()
801+
802+ for set_data in fetched_sets :
803+ set_number = set_data ["set_number" ]
804+ set_name = set_data .get ("set_name" , set_number )
805+ part_count = set_data .get ("part_count" , 0 )
806+
807+ checkbox_label = f"{ set_number } - { set_name } ({ part_count } parts)"
808+
809+ def sync_checkbox_b (set_num = set_number ):
810+ cb_key = f"b_set_checkbox_{ set_num } "
811+ if st .session_state .get (cb_key , False ):
812+ st .session_state ["selected_sets_for_search_b" ].add (set_num )
813+ else :
814+ st .session_state ["selected_sets_for_search_b" ].discard (set_num )
815+
816+ is_selected = set_number in st .session_state ["selected_sets_for_search_b" ]
817+ st .checkbox (
818+ checkbox_label , value = is_selected ,
819+ key = f"b_set_checkbox_{ set_number } " ,
820+ on_change = sync_checkbox_b , args = (set_number ,)
821+ )
822+
823+ if unfetched_sets :
824+ st .markdown (f"⚠️ { len (unfetched_sets )} set(s) without inventory — fetch on 'My Collection - Sets' page:" )
825+ for set_data in unfetched_sets :
826+ set_number = set_data ["set_number" ]
827+ set_name = set_data .get ("set_name" , set_number )
828+ st .checkbox (
829+ f"{ set_number } - { set_name } (no inventory)" ,
830+ value = False , disabled = True ,
831+ key = f"b_set_checkbox_disabled_{ set_number } "
832+ )
833+
834+ # Search button
835+ st .markdown ("---" )
836+ selected_count = len (st .session_state ["selected_sets_for_search_b" ])
837+ if selected_count == 0 :
838+ st .button ("🔍 Search Selected Sets" , key = "b_search_sets_btn" , disabled = True , type = "primary" )
839+ else :
840+ if st .button (f"🔍 Search Selected Sets ({ selected_count } )" , key = "b_search_sets_btn" , type = "primary" ):
841+ with st .spinner (f"Searching { selected_count } set(s)..." ):
842+ inventories_cache = st .session_state .get ("sets_inventories_cache" , {})
843+ selected_sets_list = list (st .session_state ["selected_sets_for_search_b" ])
844+ set_results = sets_manager .search_parts (
845+ wanted_parts ,
846+ selected_sets = selected_sets_list ,
847+ inventories_cache = inventories_cache
848+ )
849+ if set_results :
850+ st .session_state ["set_search_results_b" ] = set_results
851+ st .success (f"✅ Found parts in { len (set_results )} part/color combination(s)!" )
852+ st .rerun ()
853+ else :
854+ st .session_state ["set_search_results_b" ] = {}
855+ st .warning ("No matching parts found in selected sets." )
0 commit comments