@@ -3,20 +3,24 @@ import { Types } from "mongoose";
33
44import {
55 LabelModel ,
6- MajorReqModel ,
76 PlanModel ,
7+ PlanRequirementModel ,
88 PlanTermModel ,
99 SelectedCourseModel ,
10+ SelectedPlanRequirementModel ,
1011} from "@repo/common/models" ;
1112
1213import {
1314 Colleges ,
1415 EditPlanTermInput ,
1516 Plan ,
1617 PlanInput ,
18+ PlanRequirement ,
1719 PlanTerm ,
1820 PlanTermInput ,
1921 SelectedCourseInput ,
22+ SelectedPlanRequirementInput ,
23+ UpdateManualOverrideInput ,
2024} from "../../generated-types/graphql" ;
2125import { RequestContext } from "../../types/request-context" ;
2226import { formatPlan , formatPlanTerm } from "./formatter" ;
@@ -238,20 +242,43 @@ export async function createPlan(
238242 } )
239243 ) ;
240244
245+ const selectedPlanRequirements = await buildSelectedPlanRequirementsForPlan (
246+ majors ,
247+ minors ,
248+ colleges . map ( ( c ) => String ( c ) )
249+ ) ;
250+
241251 const newPlan = await PlanModel . create ( {
242252 userEmail,
243253 planTerms : planTerms ,
244254 majors : majors ,
245255 minors : minors ,
246- majorReqs : [ ] ,
247256 colleges : colleges ,
248257 labels : [ ] ,
249- uniReqsSatisfied : [ ] ,
250- collegeReqsSatisfied : [ ] ,
258+ selectedPlanRequirements : selectedPlanRequirements . map (
259+ ( spr ) =>
260+ new SelectedPlanRequirementModel ( {
261+ planRequirementId : new Types . ObjectId ( spr . planRequirementId ) ,
262+ manualOverrides : spr . manualOverrides ,
263+ } )
264+ ) ,
251265 } ) ;
252266 return formatPlan ( newPlan ) ;
253267}
254268
269+ /** Build selectedPlanRequirements for given majors, minors, colleges (UC + college + major/minor). Used by createPlan, editPlan sync, and migrations. */
270+ export async function buildSelectedPlanRequirementsForPlan (
271+ majors : string [ ] ,
272+ minors : string [ ] ,
273+ colleges : string [ ]
274+ ) : Promise < SelectedPlanRequirementInput [ ] > {
275+ const reqs = await getRequiredPlanRequirements ( majors , minors , colleges ) ;
276+ return reqs . map ( ( r ) => ( {
277+ planRequirementId : r . _id ,
278+ manualOverrides : [ ] ,
279+ } ) ) ;
280+ }
281+
255282export async function editPlan (
256283 plan : PlanInput ,
257284 context : RequestContext
@@ -263,6 +290,10 @@ export async function editPlan(
263290 throw new Error ( "No Plan found for this user" ) ;
264291 }
265292
293+ const majorsChanged = plan . majors != null ;
294+ const minorsChanged = plan . minors != null ;
295+ const collegesChanged = plan . colleges != null ;
296+
266297 if ( plan . colleges != null ) {
267298 gt . colleges = plan . colleges ;
268299 }
@@ -272,19 +303,32 @@ export async function editPlan(
272303 if ( plan . minors != null ) {
273304 gt . minors = plan . minors ;
274305 }
275- if ( plan . majorReqs != null ) {
276- gt . majorReqs = plan . majorReqs . map (
277- ( majorReqInput ) => new MajorReqModel ( majorReqInput )
278- ) ;
279- }
280306 if ( plan . labels != null ) {
281307 gt . labels = plan . labels . map ( ( labelInput ) => new LabelModel ( labelInput ) ) ;
282308 }
283- if ( plan . uniReqsSatisfied != null ) {
284- gt . uniReqsSatisfied = plan . uniReqsSatisfied ;
285- }
286- if ( plan . collegeReqsSatisfied != null ) {
287- gt . collegeReqsSatisfied = plan . collegeReqsSatisfied ;
309+
310+ if ( majorsChanged || minorsChanged || collegesChanged ) {
311+ const wantedReqs = await getRequiredPlanRequirements (
312+ gt . majors ,
313+ gt . minors ,
314+ gt . colleges
315+ ) ;
316+ const wantedIds = new Set ( wantedReqs . map ( ( r ) => r . _id ) ) ;
317+ const currentIds = new Set (
318+ gt . selectedPlanRequirements . map ( ( spr ) => spr . planRequirementId . toString ( ) )
319+ ) ;
320+ const kept = gt . selectedPlanRequirements . filter ( ( spr ) =>
321+ wantedIds . has ( spr . planRequirementId . toString ( ) )
322+ ) ;
323+ const newReqs = wantedReqs . filter ( ( r ) => ! currentIds . has ( r . _id ) ) ;
324+ const newSprs = newReqs . map (
325+ ( r ) =>
326+ new SelectedPlanRequirementModel ( {
327+ planRequirementId : new Types . ObjectId ( r . _id ) ,
328+ manualOverrides : [ ] ,
329+ } )
330+ ) ;
331+ gt . selectedPlanRequirements = [ ...kept , ...newSprs ] ;
288332 }
289333
290334 await gt . save ( ) ;
@@ -297,3 +341,177 @@ export async function deletePlan(context: RequestContext): Promise<string> {
297341 await PlanModel . deleteOne ( { userEmail } ) ;
298342 return userEmail ;
299343}
344+
345+ // Internal: get all PlanRequirement IDs that should be selected for given majors, minors, colleges (UC + college + major/minor). Deduped by _id.
346+ export async function getRequiredPlanRequirements (
347+ majors : string [ ] ,
348+ minors : string [ ] ,
349+ colleges : string [ ]
350+ ) : Promise < PlanRequirement [ ] > {
351+ const byId = new Map < string , PlanRequirement > ( ) ;
352+ const add = ( req : PlanRequirement ) => byId . set ( req . _id , req ) ;
353+
354+ const uc = await getUcRequirements ( ) ;
355+ uc . forEach ( add ) ;
356+ for ( const college of colleges ) {
357+ const coll = await getCollegeRequirements ( college ) ;
358+ coll . forEach ( add ) ;
359+ }
360+ const mm = await getPlanRequirementsByMajorsAndMinors ( majors , minors ) ;
361+ mm . forEach ( add ) ;
362+ return Array . from ( byId . values ( ) ) ;
363+ }
364+
365+ // Get PlanRequirements by majors and minors (internal use)
366+ export async function getPlanRequirementsByMajorsAndMinors (
367+ majors : string [ ] ,
368+ minors : string [ ]
369+ ) : Promise < PlanRequirement [ ] > {
370+ const requirements = await PlanRequirementModel . find ( {
371+ $or : [ { major : { $in : majors } } , { minor : { $in : minors } } ] ,
372+ } ) ;
373+
374+ return requirements . map ( ( req ) => ( {
375+ _id : ( req . _id as Types . ObjectId ) . toString ( ) ,
376+ code : req . code ,
377+ name : req . name ,
378+ isUcReq : req . isUcReq ,
379+ college : req . college || null ,
380+ major : req . major || null ,
381+ minor : req . minor || null ,
382+ createdBy : req . createdBy ,
383+ isOfficial : req . isOfficial ,
384+ createdAt : req . createdAt ?. toISOString ( ) || "" ,
385+ updatedAt : req . updatedAt ?. toISOString ( ) || "" ,
386+ } ) ) ;
387+ }
388+
389+ // Get UC requirements
390+ export async function getUcRequirements ( ) : Promise < PlanRequirement [ ] > {
391+ const requirements = await PlanRequirementModel . find ( { isUcReq : true } ) ;
392+
393+ return requirements . map ( ( req ) => ( {
394+ _id : ( req . _id as Types . ObjectId ) . toString ( ) ,
395+ code : req . code ,
396+ name : req . name ,
397+ isUcReq : req . isUcReq ,
398+ college : req . college || null ,
399+ major : req . major || null ,
400+ minor : req . minor || null ,
401+ createdBy : req . createdBy ,
402+ isOfficial : req . isOfficial ,
403+ createdAt : req . createdAt ?. toISOString ( ) || "" ,
404+ updatedAt : req . updatedAt ?. toISOString ( ) || "" ,
405+ } ) ) ;
406+ }
407+
408+ // Get college requirements
409+ export async function getCollegeRequirements (
410+ college : string
411+ ) : Promise < PlanRequirement [ ] > {
412+ const requirements = await PlanRequirementModel . find ( { college } ) ;
413+
414+ return requirements . map ( ( req ) => ( {
415+ _id : ( req . _id as Types . ObjectId ) . toString ( ) ,
416+ code : req . code ,
417+ name : req . name ,
418+ isUcReq : req . isUcReq ,
419+ college : req . college || null ,
420+ major : req . major || null ,
421+ minor : req . minor || null ,
422+ createdBy : req . createdBy ,
423+ isOfficial : req . isOfficial ,
424+ createdAt : req . createdAt ?. toISOString ( ) || "" ,
425+ updatedAt : req . updatedAt ?. toISOString ( ) || "" ,
426+ } ) ) ;
427+ }
428+
429+ // Get PlanRequirement by ID
430+ export async function getPlanRequirementById (
431+ id : string
432+ ) : Promise < PlanRequirement | null > {
433+ const requirement = await PlanRequirementModel . findById ( id ) ;
434+ if ( ! requirement ) {
435+ return null ;
436+ }
437+
438+ return {
439+ _id : ( requirement . _id as Types . ObjectId ) . toString ( ) ,
440+ code : requirement . code ,
441+ name : requirement . name ,
442+ isUcReq : requirement . isUcReq ,
443+ college : requirement . college || null ,
444+ major : requirement . major || null ,
445+ minor : requirement . minor || null ,
446+ createdBy : requirement . createdBy ,
447+ isOfficial : requirement . isOfficial ,
448+ createdAt : requirement . createdAt ?. toISOString ( ) || "" ,
449+ updatedAt : requirement . updatedAt ?. toISOString ( ) || "" ,
450+ } ;
451+ }
452+
453+ // Update manual override for a specific requirement
454+ export async function updateManualOverride (
455+ input : UpdateManualOverrideInput ,
456+ context : RequestContext
457+ ) : Promise < Plan > {
458+ if ( ! context . user ?. email ) throw new Error ( "Unauthorized" ) ;
459+ const userEmail = context . user . email ;
460+
461+ const gt = await PlanModel . findOne ( { userEmail } ) ;
462+ if ( ! gt ) {
463+ throw new Error ( "No Plan found for this user" ) ;
464+ }
465+
466+ // Find the selectedPlanRequirement
467+ const sprIndex = gt . selectedPlanRequirements . findIndex (
468+ ( spr ) => spr . planRequirementId . toString ( ) === input . planRequirementId
469+ ) ;
470+
471+ if ( sprIndex === - 1 ) {
472+ throw new Error ( "SelectedPlanRequirement not found in user's plan" ) ;
473+ }
474+
475+ // Update the manual override at the specified index
476+ const spr = gt . selectedPlanRequirements [ sprIndex ] ;
477+ if (
478+ input . requirementIndex < 0 ||
479+ input . requirementIndex >= spr . manualOverrides . length
480+ ) {
481+ // Extend the array if needed
482+ while ( spr . manualOverrides . length <= input . requirementIndex ) {
483+ spr . manualOverrides . push ( null ) ;
484+ }
485+ }
486+ // Store null when clearing override so it persists correctly in MongoDB
487+ spr . manualOverrides [ input . requirementIndex ] =
488+ input . manualOverride !== undefined ? input . manualOverride : null ;
489+
490+ await gt . save ( ) ;
491+ return formatPlan ( gt ) ;
492+ }
493+
494+ // Update all selectedPlanRequirements
495+ export async function updateSelectedPlanRequirements (
496+ selectedPlanRequirements : SelectedPlanRequirementInput [ ] ,
497+ context : RequestContext
498+ ) : Promise < Plan > {
499+ if ( ! context . user ?. email ) throw new Error ( "Unauthorized" ) ;
500+ const userEmail = context . user . email ;
501+
502+ const gt = await PlanModel . findOne ( { userEmail } ) ;
503+ if ( ! gt ) {
504+ throw new Error ( "No Plan found for this user" ) ;
505+ }
506+
507+ gt . selectedPlanRequirements = selectedPlanRequirements . map (
508+ ( spr ) =>
509+ new SelectedPlanRequirementModel ( {
510+ planRequirementId : new Types . ObjectId ( spr . planRequirementId ) ,
511+ manualOverrides : spr . manualOverrides ,
512+ } )
513+ ) ;
514+
515+ await gt . save ( ) ;
516+ return formatPlan ( gt ) ;
517+ }
0 commit comments