@@ -12,15 +12,119 @@ export async function insertClusteringRun(document: Document) {
1212 return result . insertedId ;
1313}
1414
15- export async function getRunWithSessions ( runId : string ) {
15+ type ResultPaginationOptions = {
16+ sessionsPage ?: number ;
17+ sessionsLimit ?: number ;
18+ clustersPage ?: number ;
19+ clustersLimit ?: number ;
20+ sessionSearch ?: string ;
21+ sessionCluster ?: string ;
22+ sessionStatus ?: string ;
23+ sessionDateFrom ?: string ;
24+ sessionDateTo ?: string ;
25+ distanceMin ?: number ;
26+ distanceMax ?: number ;
27+ clusterId ?: string ;
28+ clusterSizeMin ?: number ;
29+ clusterSizeMax ?: number ;
30+ clusterCentroid ?: string ;
31+ clusterAnomalyMin ?: number ;
32+ clusterAnomalyMax ?: number ;
33+ } ;
34+
35+ function safePagination ( page = 1 , limit = 10 ) {
36+ return {
37+ page : Math . max ( 1 , page ) ,
38+ limit : Math . max ( 1 , Math . min ( limit , 200 ) ) ,
39+ } ;
40+ }
41+
42+ function clusterLabel ( clusterId : unknown ) {
43+ const value = Number ( clusterId ) ;
44+ if ( ! Number . isFinite ( value ) ) return "—" ;
45+ return value < 0 ? "noise" : `C${ value + 1 } ` ;
46+ }
47+
48+ function matchesText ( value : unknown , query : string | undefined ) {
49+ return ! query || String ( value ?? "" ) . toLowerCase ( ) . includes ( query . toLowerCase ( ) ) ;
50+ }
51+
52+ function matchesRange ( value : unknown , min : number | undefined , max : number | undefined ) {
53+ const numeric = Number ( value ?? 0 ) ;
54+ return numeric >= ( min ?? Number . NEGATIVE_INFINITY ) && numeric <= ( max ?? Number . POSITIVE_INFINITY ) ;
55+ }
56+
57+ function matchesDate ( value : unknown , from : string | undefined , to : string | undefined ) {
58+ const timestamp = value ? new Date ( String ( value ) ) . getTime ( ) : Number . NaN ;
59+ const fromTime = from ? new Date ( from ) . getTime ( ) : Number . NEGATIVE_INFINITY ;
60+ const toTime = to ? new Date ( to ) . getTime ( ) : Number . POSITIVE_INFINITY ;
61+ if ( Number . isNaN ( timestamp ) ) return ! from && ! to ;
62+ return timestamp >= fromTime && timestamp <= toTime ;
63+ }
64+
65+ function pageSlice < T > ( items : T [ ] , page : number , limit : number ) {
66+ return items . slice ( ( page - 1 ) * limit , page * limit ) ;
67+ }
68+
69+ export async function getRunWithSessions ( runId : string , options : ResultPaginationOptions = { } ) {
70+ if ( ! ObjectId . isValid ( runId ) ) return null ;
1671 const run = await getCollection ( "clustering_runs" ) . findOne ( { _id : new ObjectId ( runId ) } ) ;
1772 if ( ! run ) return null ;
1873
19- const sessionIds = ( ( run . results as Document ) ?. sessionAssignments as Document [ ] | undefined ) ?. map ( ( item ) => item . sessionId as ObjectId ) ?? [ ] ;
20- const sessions = await getCollection ( "sessions" ) . find ( { _id : { $in : sessionIds } } ) . toArray ( ) ;
21- const studentIds = sessions . map ( ( session ) => session . studentId as ObjectId ) ;
74+ const sessionPagination = safePagination ( options . sessionsPage , options . sessionsLimit ?? 15 ) ;
75+ const clusterPagination = safePagination ( options . clustersPage , options . clustersLimit ?? 10 ) ;
76+ const assignments = ( ( run . results as Document ) ?. sessionAssignments as Document [ ] | undefined ) ?? [ ] ;
77+ const allSessionIds = assignments . map ( ( item ) => item . sessionId as ObjectId ) . filter ( Boolean ) ;
78+ const allSessions = await getCollection ( "sessions" ) . find ( { _id : { $in : allSessionIds } } ) . toArray ( ) ;
79+ const allStudentIds = allSessions . map ( ( session ) => session . studentId as ObjectId ) . filter ( Boolean ) ;
80+ const allStudents = await getCollection ( "students" ) . find ( { _id : { $in : allStudentIds } } ) . toArray ( ) ;
81+ const sessionsById = new Map ( allSessions . map ( ( session ) => [ String ( session . _id ) , session ] ) ) ;
82+ const studentsById = new Map ( allStudents . map ( ( student ) => [ String ( student . _id ) , student ] ) ) ;
83+
84+ const filteredAssignments = assignments . filter ( ( assignment ) => {
85+ const session = sessionsById . get ( String ( assignment . sessionId ) ) ;
86+ const student = studentsById . get ( String ( session ?. studentId ?? "" ) ) ;
87+ const label = clusterLabel ( assignment . clusterId ) ;
88+ return (
89+ matchesText ( student ?. fullName ?? session ?. studentId ?? assignment . sessionId , options . sessionSearch ) &&
90+ ( ! options . sessionCluster || options . sessionCluster === "all" || label === options . sessionCluster ) &&
91+ ( ! options . sessionStatus || options . sessionStatus === "all" || ( options . sessionStatus === "anomaly" ? Boolean ( assignment . isAnomaly ) : ! assignment . isAnomaly ) ) &&
92+ matchesDate ( session ?. startTime , options . sessionDateFrom , options . sessionDateTo ) &&
93+ matchesRange ( assignment . distanceToCentroid , options . distanceMin , options . distanceMax )
94+ ) ;
95+ } ) ;
96+
97+ const pagedAssignments = pageSlice ( filteredAssignments , sessionPagination . page , sessionPagination . limit ) ;
98+ const pagedSessionIds = pagedAssignments . map ( ( item ) => item . sessionId as ObjectId ) . filter ( Boolean ) ;
99+ const sessions = allSessions . filter ( ( session ) => pagedSessionIds . some ( ( id ) => String ( id ) === String ( session . _id ) ) ) ;
100+ const studentIds = sessions . map ( ( session ) => session . studentId as ObjectId ) . filter ( Boolean ) ;
22101 const students = await getCollection ( "students" ) . find ( { _id : { $in : studentIds } } ) . toArray ( ) ;
23- return { run, sessions, students } ;
102+
103+ const clusters = ( ( run . results as Document ) ?. clusters as Document [ ] | undefined ) ?? [ ] ;
104+ const filteredClusters = clusters . filter ( ( cluster , index ) => {
105+ const label = clusterLabel ( cluster . clusterId ?? index ) ;
106+ const centroid = Array . isArray ( cluster . centroid ) ? cluster . centroid . slice ( 0 , 3 ) . join ( ", " ) : "—" ;
107+ const anomalyRate = Number ( cluster . anomalyRate ?? 0 ) * 100 ;
108+ return (
109+ matchesText ( label , options . clusterId ) &&
110+ matchesRange ( cluster . size , options . clusterSizeMin , options . clusterSizeMax ) &&
111+ matchesText ( centroid , options . clusterCentroid ) &&
112+ matchesRange ( anomalyRate , options . clusterAnomalyMin , options . clusterAnomalyMax )
113+ ) ;
114+ } ) ;
115+ const pagedClusters = pageSlice ( filteredClusters , clusterPagination . page , clusterPagination . limit ) ;
116+
117+ return {
118+ run,
119+ assignments : pagedAssignments ,
120+ clusters : pagedClusters ,
121+ sessions,
122+ students,
123+ pagination : {
124+ sessions : { total : filteredAssignments . length , page : sessionPagination . page , limit : sessionPagination . limit } ,
125+ clusters : { total : filteredClusters . length , page : clusterPagination . page , limit : clusterPagination . limit } ,
126+ } ,
127+ } ;
24128}
25129
26130export async function deleteClusteringRun ( runId : string ) {
0 commit comments