1+ import { getLongestDateRange } from '@crowd/common'
2+ import { IMemberOrganization } from '@crowd/types'
3+
14import { BLACKLISTED_MEMBER_TITLES } from '../members/base'
25import { QueryExecutor } from '../queryExecutor'
36
@@ -7,7 +10,7 @@ export interface IAffiliationPeriod {
710 endDate : string | null
811}
912
10- interface IWorkRow {
13+ interface IWorkExperience {
1114 id : string
1215 memberId : string
1316 organizationId : string
@@ -28,8 +31,8 @@ interface IWorkRow {
2831export async function findWorkExperiencesBulk (
2932 qx : QueryExecutor ,
3033 memberIds : string [ ] ,
31- ) : Promise < IWorkRow [ ] > {
32- const rows : IWorkRow [ ] = await qx . select (
34+ ) : Promise < IWorkExperience [ ] > {
35+ const rows : IWorkExperience [ ] = await qx . select (
3336 `
3437 WITH relevant_orgs AS (
3538 SELECT DISTINCT "organizationId"
@@ -75,7 +78,7 @@ export async function findWorkExperiencesBulk(
7578export async function findManualAffiliationsBulk (
7679 qx : QueryExecutor ,
7780 memberIds : string [ ] ,
78- ) : Promise < IWorkRow [ ] > {
81+ ) : Promise < IWorkExperience [ ] > {
7982 return qx . select (
8083 `
8184 SELECT
@@ -98,26 +101,16 @@ export async function findManualAffiliationsBulk(
98101 )
99102}
100103
101- function durationMs ( org : IWorkRow ) : number {
102- const start = new Date ( org . dateStart ?? '' ) . getTime ( )
103- const end = new Date ( org . dateEnd ?? '9999-12-31' ) . getTime ( )
104- return end - start
105- }
106-
107- function longestDateRange ( orgs : IWorkRow [ ] ) : IWorkRow {
108- const withDates = orgs . filter ( ( r ) => r . dateStart )
109- const candidates = withDates . length > 0 ? withDates : orgs
110- return candidates . reduce ( ( best , org ) => ( durationMs ( org ) > durationMs ( best ) ? org : best ) )
111- }
112-
113- function selectPrimaryWorkExperience ( orgs : IWorkRow [ ] ) : IWorkRow {
104+ function selectPrimaryWorkExperience ( orgs : IWorkExperience [ ] ) {
114105 if ( orgs . length === 1 ) return orgs [ 0 ]
115106
116107 // 1. Manual affiliations (segmentId non-null) always win
117108 const manual = orgs . filter ( ( r ) => r . segmentId !== null )
118109 if ( manual . length > 0 ) {
119110 if ( manual . length === 1 ) return manual [ 0 ]
120- return longestDateRange ( manual )
111+ return getLongestDateRange (
112+ manual as unknown as IMemberOrganization [ ] ,
113+ ) as unknown as IWorkExperience
121114 }
122115
123116 // 2. isPrimaryWorkExperience = true — prefer those with a dateStart
@@ -135,11 +128,11 @@ function selectPrimaryWorkExperience(orgs: IWorkRow[]): IWorkRow {
135128 }
136129
137130 // 5. Longest date range as final tiebreaker
138- return longestDateRange ( orgs )
131+ return getLongestDateRange ( orgs as unknown as IMemberOrganization [ ] ) as unknown as IWorkExperience
139132}
140133
141134/** Returns the org used to fill gaps — primary undated wins, then earliest-created undated. */
142- function findFallbackOrg ( rows : IWorkRow [ ] ) : IWorkRow | null {
135+ function findFallbackOrg ( rows : IWorkExperience [ ] ) : IWorkExperience | null {
143136 const primaryUndated = rows . find ( ( r ) => r . isPrimaryWorkExperience && ! r . dateStart && ! r . dateEnd )
144137 if ( primaryUndated ) return primaryUndated
145138
@@ -155,7 +148,7 @@ function findFallbackOrg(rows: IWorkRow[]): IWorkRow | null {
155148 * Collects all date boundaries from the dated rows, capped at today.
156149 * Each dateStart and (dateEnd + 1 day) marks a point where active orgs can change.
157150 */
158- function collectBoundaries ( datedRows : IWorkRow [ ] ) : Date [ ] {
151+ function collectBoundaries ( datedRows : IWorkExperience [ ] ) : Date [ ] {
159152 const today = startOfDay ( new Date ( ) )
160153
161154 const ms = new Set < number > ( [ today . getTime ( ) ] )
@@ -176,7 +169,7 @@ function collectBoundaries(datedRows: IWorkRow[]): Date[] {
176169 . map ( ( t ) => new Date ( t ) )
177170}
178171
179- function orgsActiveAt ( rows : IWorkRow [ ] , boundaryDate : Date ) : IWorkRow [ ] {
172+ function orgsActiveAt ( rows : IWorkExperience [ ] , boundaryDate : Date ) : IWorkExperience [ ] {
180173 return rows . filter ( ( role ) => {
181174 if ( ! role . dateStart && ! role . dateEnd ) return true // truly undated: active at every boundary
182175
@@ -202,12 +195,12 @@ function dayBefore(date: Date): Date {
202195
203196/** Iterates boundary intervals and builds non-overlapping affiliation windows. */
204197function buildTimeline (
205- allRows : IWorkRow [ ] ,
206- fallbackOrg : IWorkRow | null ,
198+ allRows : IWorkExperience [ ] ,
199+ fallbackOrg : IWorkExperience | null ,
207200 boundaries : Date [ ] ,
208201) : IAffiliationPeriod [ ] {
209202 const affiliations : IAffiliationPeriod [ ] = [ ]
210- let currentOrg : IWorkRow = null
203+ let currentOrg : IWorkExperience = null
211204 let currentWindowStart : Date = null
212205 let uncoveredPeriodStart : Date = null
213206
@@ -288,7 +281,7 @@ function buildTimeline(
288281 return affiliations
289282}
290283
291- function resolveAffiliationsForMember ( rows : IWorkRow [ ] ) : IAffiliationPeriod [ ] {
284+ function resolveAffiliationsForMember ( rows : IWorkExperience [ ] ) : IAffiliationPeriod [ ] {
292285 // If one undated work-experience org is marked primary, drop other undated work-experience orgs
293286 // to avoid infinite conflicts. Manual affiliations (segmentId !== null) are never dropped.
294287 const primaryUndated = rows . find ( ( r ) => r . isPrimaryWorkExperience && ! r . dateStart && ! r . dateEnd )
@@ -327,7 +320,7 @@ export async function resolveAffiliationsByMemberIds(
327320 findManualAffiliationsBulk ( qx , memberIds ) ,
328321 ] )
329322
330- const byMember = new Map < string , IWorkRow [ ] > ( )
323+ const byMember = new Map < string , IWorkExperience [ ] > ( )
331324 for ( const row of [ ...workExperiences , ...manualAffiliations ] ) {
332325 const list = byMember . get ( row . memberId ) ?? [ ]
333326 list . push ( row )
0 commit comments