@@ -98,6 +98,8 @@ const GENERATED_ULID_SEQUENCE = Object.freeze({
9898 note : 4001 ,
9999 diagnostic : 5001 ,
100100} ) ;
101+ const RECOMMENDED_TARGET_LINKED_RECORD_TYPE = "recommended-target" ;
102+ const RECOMMENDED_TARGET_NOTE_KEY = GAME_JOURNEY_KEYS . notes . designPass ;
101103
102104export const GAME_JOURNEY_STATUSES = [
103105 {
@@ -619,6 +621,14 @@ export function createGameJourneyMockRepository(options = {}) {
619621 return currentSessionUser ( ) . userKey ;
620622 }
621623
624+ function safeCurrentUserKey ( ) {
625+ try {
626+ return currentUserKey ( ) || systemUserKey ( ) ;
627+ } catch {
628+ return systemUserKey ( ) ;
629+ }
630+ }
631+
622632 function currentUserCanWrite ( ) {
623633 return Boolean ( currentUserKey ( ) ) ;
624634 }
@@ -631,6 +641,10 @@ export function createGameJourneyMockRepository(options = {}) {
631641 return Boolean ( item && item . createdBy === systemUserKey ( ) ) ;
632642 }
633643
644+ function isRecommendedTargetItem ( item ) {
645+ return item ?. linkedRecordType === RECOMMENDED_TARGET_LINKED_RECORD_TYPE ;
646+ }
647+
634648 function currentUserCanSeeNote ( note ) {
635649 const sessionUser = currentSessionUser ( ) ;
636650 return Boolean ( sessionUser . userKey && ( sessionUser . isAdmin || note . ownerKey === sessionUser . userKey ) ) ;
@@ -745,10 +759,95 @@ export function createGameJourneyMockRepository(options = {}) {
745759
746760 function getItemsForNote ( noteKey ) {
747761 return tables . game_journey_items
748- . filter ( ( item ) => item . noteKey === noteKey )
762+ . filter ( ( item ) => item . noteKey === noteKey && ! isRecommendedTargetItem ( item ) )
749763 . sort ( ( left , right ) => left . order - right . order ) ;
750764 }
751765
766+ function normalizeTargetCount ( value ) {
767+ const parsed = Number ( value ) ;
768+ if ( ! Number . isFinite ( parsed ) ) {
769+ return 0 ;
770+ }
771+ return Math . max ( 0 , Math . trunc ( parsed ) ) ;
772+ }
773+
774+ function readTargetCount ( item , fallbackCount ) {
775+ if ( ! item ?. userDetails ) {
776+ return fallbackCount ;
777+ }
778+ try {
779+ const payload = JSON . parse ( item . userDetails ) ;
780+ return normalizeTargetCount ( payload . suggestedCount ?? fallbackCount ) ;
781+ } catch {
782+ return fallbackCount ;
783+ }
784+ }
785+
786+ function findRecommendedTargetItem ( targetKey ) {
787+ return tables . game_journey_items . find ( ( item ) =>
788+ item . gameKey === GAME_JOURNEY_KEYS . game &&
789+ item . linkedRecordType === RECOMMENDED_TARGET_LINKED_RECORD_TYPE &&
790+ item . linkedRecordId === targetKey ,
791+ ) ;
792+ }
793+
794+ function hydrateRecommendedTarget ( target ) {
795+ const item = findRecommendedTargetItem ( target . key ) ;
796+ return {
797+ ...clone ( target ) ,
798+ suggestedCount : readTargetCount ( item , target . suggestedCount ) ,
799+ persisted : Boolean ( item ) ,
800+ recordKey : item ?. key || "" ,
801+ updatedAt : item ?. updatedAt || "" ,
802+ updatedBy : item ?. updatedBy || "" ,
803+ } ;
804+ }
805+
806+ function listRecommendedTargets ( ) {
807+ return GAME_JOURNEY_RECOMMENDED_TARGETS . map ( hydrateRecommendedTarget ) ;
808+ }
809+
810+ function updateRecommendedTarget ( targetKey , suggestedCount ) {
811+ const activeGame = requireActiveGame ( ) ;
812+ const target = GAME_JOURNEY_RECOMMENDED_TARGETS . find ( ( item ) => item . key === targetKey ) ;
813+ if ( ! activeGame || ! target ) {
814+ return null ;
815+ }
816+
817+ const normalizedCount = normalizeTargetCount ( suggestedCount ) ;
818+ const timestampValue = new Date ( ) . toISOString ( ) ;
819+ const userKey = safeCurrentUserKey ( ) ;
820+ let item = findRecommendedTargetItem ( target . key ) ;
821+ if ( ! item ) {
822+ item = {
823+ key : makeUlid ( nextItemNumber ) ,
824+ gameKey : activeGame . key ,
825+ noteKey : RECOMMENDED_TARGET_NOTE_KEY ,
826+ status : "not-started" ,
827+ title : `Recommended target: ${ target . label } ` ,
828+ userDetails : "" ,
829+ createdBy : userKey ,
830+ updatedBy : userKey ,
831+ templateKey : "" ,
832+ linkedRecordType : RECOMMENDED_TARGET_LINKED_RECORD_TYPE ,
833+ linkedRecordId : target . key ,
834+ indent : 0 ,
835+ order : getItemsForNote ( RECOMMENDED_TARGET_NOTE_KEY ) . length + 1 ,
836+ createdAt : timestampValue ,
837+ updatedAt : timestampValue ,
838+ } ;
839+ nextItemNumber += 1 ;
840+ tables . game_journey_items . push ( item ) ;
841+ }
842+
843+ item . userDetails = JSON . stringify ( { suggestedCount : normalizedCount } ) ;
844+ item . updatedAt = timestampValue ;
845+ item . updatedBy = userKey ;
846+ addActivity ( activeGame . key , RECOMMENDED_TARGET_NOTE_KEY , `Updated ${ target . label } recommended target to ${ normalizedCount } ` , userKey ) ;
847+ persistTables ( ) ;
848+ return hydrateRecommendedTarget ( target ) ;
849+ }
850+
752851 function itemMatchesFilter ( item , filterId ) {
753852 if ( filterId === "system" ) {
754853 return isSystemItem ( item ) ;
@@ -1238,6 +1337,8 @@ export function createGameJourneyMockRepository(options = {}) {
12381337 getCompletionMetricsSnapshot : ( ) => completionMetricsStore . snapshot ( ) ,
12391338 listCompletionMetrics : ( ) => completionMetricsStore . listMetrics ( ) ,
12401339 updateCompletionMetric : ( bucketKey , updates ) => completionMetricsStore . updateMetric ( bucketKey , updates ) ,
1340+ listRecommendedTargets,
1341+ updateRecommendedTarget,
12411342 getSessionUser : ( ) => currentSessionUser ( ) ,
12421343 getSystemUser : ( ) => getMockDbSystemUser ( ) ,
12431344 getActiveGame,
0 commit comments