@@ -85,6 +85,101 @@ function getUnifiedTypeName (typeId) {
8585 return canonical || typeId
8686}
8787
88+ /**
89+ * Normalize a challenge id used to match duplicate history rows across tracks.
90+ * @param {* } value raw challenge id from a memberStatsHistory response row
91+ * @returns {string|undefined } normalized challenge id when available
92+ */
93+ function normalizeHistoryChallengeId ( value ) {
94+ if ( _ . isNil ( value ) ) {
95+ return undefined
96+ }
97+
98+ const challengeId = String ( value ) . trim ( )
99+ return challengeId || undefined
100+ }
101+
102+ /**
103+ * Normalize a history placement so only positive integer placements are copied.
104+ * @param {* } value raw placement value from a history row
105+ * @returns {number|undefined } visible placement when available
106+ */
107+ function normalizeHistoryPlacement ( value ) {
108+ const placement = _ . toInteger ( value )
109+ return Number . isInteger ( placement ) && placement > 0 ? placement : undefined
110+ }
111+
112+ /**
113+ * Return the rating value that a history card can display.
114+ * @param {Object } row unified history row
115+ * @returns {* } newRating or rating when either value is available
116+ */
117+ function getHistoryDisplayRating ( row ) {
118+ return _ . isNil ( row && row . newRating ) ? row && row . rating : row . newRating
119+ }
120+
121+ /**
122+ * Fill missing display fields on duplicate challenge history rows.
123+ *
124+ * A completed challenge can appear once under its source track and again under a
125+ * configured rating path, for example DEVELOPMENT / Challenge and DATA_SCIENCE /
126+ * AI Engineering. The source-track row may only have the challenge card metadata,
127+ * while the rating-path row carries the placement and rating needed by profile
128+ * details. Existing values are preserved and only missing placement/newRating
129+ * fields are copied.
130+ *
131+ * @param {Array<Object> } rows unified history rows
132+ * @returns {Array<Object> } history rows with duplicate display fields backfilled
133+ */
134+ function fillMissingHistoryDisplayFields ( rows ) {
135+ const displayFieldsByChallengeId = new Map ( )
136+
137+ _ . forEach ( rows || [ ] , ( row ) => {
138+ const challengeId = normalizeHistoryChallengeId ( row && row . challengeId )
139+ if ( ! challengeId ) {
140+ return
141+ }
142+
143+ const placement = normalizeHistoryPlacement ( row . placement )
144+ const rating = getHistoryDisplayRating ( row )
145+ if ( ! placement && _ . isNil ( rating ) ) {
146+ return
147+ }
148+
149+ const existing = displayFieldsByChallengeId . get ( challengeId ) || { }
150+ displayFieldsByChallengeId . set ( challengeId , {
151+ placement : placement && ( ! existing . placement || placement < existing . placement )
152+ ? placement
153+ : existing . placement ,
154+ rating : _ . isNil ( existing . rating ) ? rating : existing . rating
155+ } )
156+ } )
157+
158+ return _ . map ( rows || [ ] , ( row ) => {
159+ const challengeId = normalizeHistoryChallengeId ( row && row . challengeId )
160+ const displayFields = challengeId ? displayFieldsByChallengeId . get ( challengeId ) : undefined
161+ if ( ! displayFields ) {
162+ return row
163+ }
164+
165+ const existingPlacement = normalizeHistoryPlacement ( row . placement )
166+ const placement = existingPlacement || displayFields . placement
167+ const newRating = _ . isNil ( row . newRating ) && ! _ . isNil ( displayFields . rating )
168+ ? displayFields . rating
169+ : row . newRating
170+
171+ if ( placement === existingPlacement && newRating === row . newRating ) {
172+ return row
173+ }
174+
175+ return {
176+ ...row ,
177+ placement,
178+ newRating
179+ }
180+ } )
181+ }
182+
88183function isUuidValue ( value ) {
89184 return uuidPattern . test ( String ( value || '' ) . trim ( ) )
90185}
@@ -823,15 +918,16 @@ function buildUnifiedStatsHistoryResponse (member, historyStats, fields) {
823918 } ) )
824919 . filter ( row => _ . includes ( supportedUnifiedHistoryTrackNames , row . resolvedTrackName ) )
825920 . value ( )
826- const first = _ . head ( validRows ) || { }
921+ const displayRows = fillMissingHistoryDisplayFields ( validRows )
922+ const first = _ . head ( displayRows ) || { }
827923 const item = {
828924 userId : helper . bigIntToNumber ( member . userId ) ,
829925 groupId : helper . bigIntToNumber ( first . groupId ) ,
830926 handle : member . handle ,
831927 handleLower : member . handleLower
832928 }
833929
834- const groupedByTrackType = _ . groupBy ( validRows , row => `${ row . resolvedTrackName } ::${ row . resolvedTypeName } ` )
930+ const groupedByTrackType = _ . groupBy ( displayRows , row => `${ row . resolvedTrackName } ::${ row . resolvedTypeName } ` )
835931 _ . forEach ( groupedByTrackType , ( trackHistory , key ) => {
836932 const [ trackName , typeName ] = key . split ( '::' )
837933 if ( _ . includes ( groupedSubTrackStatsTrackNames , trackName ) ) {
0 commit comments