11import _ from 'lodash'
22import moment from 'moment'
3+ import update from 'react-addons-update'
34
45import { MILESTONE_STATUS } from '../config/constants'
56import { MILESTONE_STATUS_TEXT } from '../config/constants'
@@ -41,4 +42,160 @@ export const getProgressPercent = (totalDays, daysLeft) => {
4142 : 100
4243
4344 return progressPercent
44- }
45+ }
46+
47+ function mergeJsonObjects ( targetObj , sourceObj ) {
48+ return _ . mergeWith ( { } , targetObj , sourceObj , ( target , source ) => {
49+ // Overwrite the array
50+ if ( _ . isArray ( source ) ) {
51+ return source
52+ }
53+ } )
54+ }
55+
56+ function updateMilestone ( milestone , updatedProps ) {
57+ const entityToUpdate = updatedProps
58+ const durationChanged = entityToUpdate . duration && entityToUpdate . duration !== milestone . duration
59+ const statusChanged = entityToUpdate . status && entityToUpdate . status !== milestone . status
60+ const completionDateChanged = entityToUpdate . completionDate
61+ && ! _ . isEqual ( milestone . completionDate , entityToUpdate . completionDate )
62+ const today = moment . utc ( ) . hours ( 0 ) . minutes ( 0 ) . seconds ( 0 ) . milliseconds ( 0 )
63+
64+ // Merge JSON fields
65+ entityToUpdate . details = mergeJsonObjects ( milestone . details , entityToUpdate . details )
66+
67+ let actualStartDateCanged = false
68+ // if status has changed
69+ if ( statusChanged ) {
70+ // if status has changed to be completed, set the compeltionDate if not provided
71+ if ( entityToUpdate . status === MILESTONE_STATUS . COMPLETED ) {
72+ entityToUpdate . completionDate = entityToUpdate . completionDate ? entityToUpdate . completionDate : today . toISOString ( )
73+ entityToUpdate . duration = moment . utc ( entityToUpdate . completionDate )
74+ . diff ( entityToUpdate . actualStartDate , 'days' ) + 1
75+ }
76+ // if status has changed to be active, set the startDate to today
77+ if ( entityToUpdate . status === MILESTONE_STATUS . ACTIVE ) {
78+ // NOTE: not updating startDate as activating a milestone should not update the scheduled start date
79+ // entityToUpdate.startDate = today
80+ // should update actual start date
81+ entityToUpdate . actualStartDate = today . toISOString ( )
82+ actualStartDateCanged = true
83+ }
84+ }
85+
86+ // Updates the end date of the milestone if:
87+ // 1. if duration of the milestone is udpated, update its end date
88+ // OR
89+ // 2. if actual start date is updated, updating the end date of the activated milestone because
90+ // early or late start of milestone, we are essentially changing the end schedule of the milestone
91+ if ( durationChanged || actualStartDateCanged ) {
92+ const updatedStartDate = actualStartDateCanged ? entityToUpdate . actualStartDate : milestone . startDate
93+ const updatedDuration = _ . get ( entityToUpdate , 'duration' , milestone . duration )
94+ entityToUpdate . endDate = moment . utc ( updatedStartDate ) . add ( updatedDuration - 1 , 'days' ) . toDate ( ) . toISOString ( )
95+ }
96+
97+ // if completionDate has changed
98+ if ( ! statusChanged && completionDateChanged ) {
99+ entityToUpdate . duration = moment . utc ( entityToUpdate . completionDate )
100+ . diff ( entityToUpdate . actualStartDate , 'days' ) + 1
101+ entityToUpdate . status = MILESTONE_STATUS . COMPLETED
102+ }
103+
104+ return update ( milestone , { $merge : entityToUpdate } )
105+ }
106+
107+ /**
108+ * Cascades endDate/completionDate changes to all milestones with a greater order than the given one.
109+ * @param {Object } origMilestone the original milestone that was updated
110+ * @param {Object } updMilestone the milestone that was updated
111+ * @returns {Promise<void> } a promise that resolves to the last found milestone. If no milestone exists with an
112+ * order greater than the passed <b>updMilestone</b>, the promise will resolve to the passed
113+ * <b>updMilestone</b>
114+ */
115+ function updateComingMilestones ( origMilestone , updMilestone , timelineMilestones ) {
116+ // flag to indicate if the milestone in picture, is updated for completionDate field or not
117+ const completionDateChanged = ! _ . isEqual ( origMilestone . completionDate , updMilestone . completionDate )
118+ const today = moment . utc ( ) . hours ( 0 ) . minutes ( 0 ) . seconds ( 0 ) . milliseconds ( 0 ) . toISOString ( )
119+ // updated milestone's start date, pefers actual start date over scheduled start date
120+ const updMSStartDate = updMilestone . actualStartDate ? updMilestone . actualStartDate : updMilestone . startDate
121+ // calculates schedule end date for the milestone based on start date and duration
122+ let updMilestoneEndDate = moment . utc ( updMSStartDate ) . add ( updMilestone . duration - 1 , 'days' ) . toDate ( )
123+ // if the milestone, in context, is completed, overrides the end date to the completion date
124+ updMilestoneEndDate = updMilestone . completionDate ? updMilestone . completionDate : updMilestoneEndDate
125+
126+ const affectedMilestones = timelineMilestones . filter ( milestone => milestone . order > updMilestone . order )
127+ const comingMilestones = _ . sortBy ( affectedMilestones , 'order' )
128+ // calculates the schedule start date for the next milestone
129+ let startDate = moment . utc ( updMilestoneEndDate ) . add ( 1 , 'days' ) . toDate ( ) . toISOString ( )
130+ let firstMilestoneFound = false
131+
132+ let updatedTimelineMilestones = timelineMilestones
133+ for ( let i = 0 ; i < comingMilestones . length ; i += 1 ) {
134+ const updateProps = { }
135+ const milestone = comingMilestones [ i ]
136+
137+ // Update the milestone startDate if different than the iterated startDate
138+ if ( ! _ . isEqual ( milestone . startDate , startDate ) ) {
139+ updateProps . startDate = startDate
140+ updateProps . updatedBy = updMilestone . updatedBy
141+ }
142+
143+ // Calculate the endDate, and update it if different
144+ const endDate = moment . utc ( updateProps . startDate || milestone . startDate ) . add ( milestone . duration - 1 , 'days' ) . toDate ( ) . toISOString ( )
145+ if ( ! _ . isEqual ( milestone . endDate , endDate ) ) {
146+ updateProps . endDate = endDate
147+ updateProps . updatedBy = updMilestone . updatedBy
148+ }
149+
150+ // if completionDate is alerted, update status of the first non hidden milestone after the current one
151+ if ( ! firstMilestoneFound && completionDateChanged && ! milestone . hidden ) {
152+ // activate next milestone
153+ updateProps . status = MILESTONE_STATUS . ACTIVE
154+ updateProps . actualStartDate = today
155+ firstMilestoneFound = true
156+ }
157+
158+ // if milestone is not hidden, update the startDate for the next milestone, otherwise keep the same startDate for next milestone
159+ if ( ! milestone . hidden ) {
160+ // Set the next startDate value to the next day after completionDate if present or the endDate
161+ startDate = moment . utc ( milestone . completionDate
162+ ? milestone . completionDate
163+ : updateProps . endDate || milestone . endDate ) . add ( 1 , 'days' ) . toDate ( ) . toISOString ( )
164+ }
165+
166+ const milestoneIdx = updatedTimelineMilestones . findIndex ( item => item . id === milestone . id )
167+ updatedTimelineMilestones = update ( updatedTimelineMilestones , { [ milestoneIdx ] : { $merge : updateProps } } )
168+ }
169+
170+ return updatedTimelineMilestones
171+ }
172+
173+ function cascadeMilestones ( originalMilestone , updatedMilestone , timelineMilestones ) {
174+ const original = originalMilestone
175+ const updated = updatedMilestone
176+
177+ // we need to recalculate change in fields because we update some fields before making actual update
178+ const needToCascade = ! _ . isEqual ( original . completionDate , updated . completionDate ) // completion date changed
179+ || original . duration !== updated . duration // duration changed
180+ || original . actualStartDate !== updated . actualStartDate // actual start date updated
181+
182+ if ( needToCascade ) {
183+ const updatedMilestones = updateComingMilestones ( original , updated , timelineMilestones )
184+ return updatedMilestones
185+ }
186+
187+ return timelineMilestones
188+ }
189+
190+ export const processUpdateMilestone = ( milestone , updatedProps , timelineMilestones ) => {
191+ let updatedTimelineMilestones
192+
193+ const updatedMilestone = updateMilestone ( milestone , updatedProps )
194+
195+ const milestoneIdx = timelineMilestones . findIndex ( item => item . id === updatedMilestone . id )
196+ updatedTimelineMilestones = update ( timelineMilestones , { [ milestoneIdx ] : { $set : updatedMilestone } } )
197+
198+ updatedTimelineMilestones = cascadeMilestones ( milestone , updatedMilestone , updatedTimelineMilestones )
199+
200+ return { updatedMilestone, updatedTimelineMilestones }
201+ }
0 commit comments