Skip to content
This repository was archived by the owner on May 13, 2024. It is now read-only.

Commit c3f8037

Browse files
author
Jonas Metzener
committed
Improve handling of overlapping activities
* Active activities can be transformed to timesheet entries if not overlapping * Show a warning if trying to transform overlapping activities * Do not split overlapping activities if they overlap more than one day * Show an info message if stopping >1 day overlapping activities
1 parent a0243e1 commit c3f8037

File tree

6 files changed

+155
-61
lines changed

6 files changed

+155
-61
lines changed

app/index/activities/route.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,16 @@ export default Route.extend(RouteAutostartTourMixin, {
121121
'task.id',
122122
undefined
123123
)
124-
let hasActive = !!this.get('controller.activities').findBy('active')
124+
let hasOverlapping = !!this.get('controller.activities').find(a => {
125+
return (
126+
a.get('active') && !a.get('activeBlock.from').isSame(moment(), 'day')
127+
)
128+
})
125129

126130
this.set('controller.showUnknownWarning', hasUnknown)
127-
this.set('controller.showActiveWarning', hasActive)
131+
this.set('controller.showOverlappingWarning', hasOverlapping)
128132

129-
if (!hasUnknown && !hasActive) {
133+
if (!hasUnknown && !hasOverlapping) {
130134
this.send('generateReports')
131135
}
132136
},
@@ -139,15 +143,26 @@ export default Route.extend(RouteAutostartTourMixin, {
139143
*/
140144
async generateReports() {
141145
this.set('controller.showUnknownWarning', false)
142-
this.set('controller.showActiveWarning', false)
146+
this.set('controller.showOverlappingWarning', false)
143147

144148
try {
145149
await RSVP.all(
146150
this.get('controller.activities')
147-
.filter(a => a.get('task.id') && !a.get('active'))
151+
.filter(
152+
a =>
153+
a.get('task.id') &&
154+
!(
155+
a.get('active') &&
156+
!a.get('activeBlock.from').isSame(moment(), 'day')
157+
)
158+
)
148159
.map(async activity => {
149160
let duration = moment.duration(activity.get('duration'))
150161

162+
if (activity.get('active')) {
163+
duration.add(moment().diff(activity.get('activeBlock.from')))
164+
}
165+
151166
let data = {
152167
activity,
153168
duration,

app/index/activities/template.hbs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -65,16 +65,16 @@
6565
</div>
6666
</div>
6767

68-
{{#sy-modal visible=showActiveWarning as |modal|}}
68+
{{#sy-modal visible=showOverlappingWarning as |modal|}}
6969
{{#modal.header}}
70-
Active activity
70+
Activities overlap
7171
{{/modal.header}}
7272
{{#modal.body}}
73-
Active activities will not be taken into account for the timesheet.
73+
Overlapping activities will not be taken into account for the timesheet.
7474
{{/modal.body}}
75-
{{#modal.footer data-test-active-warning}}
76-
<button class="btn btn-primary" {{action (if showUnknownWarning (action (mut showActiveWarning) false) 'generateReports')}}>That's fine</button>
77-
<button class="btn btn-default" {{action (queue (action (mut showActiveWarning) false) (action (mut showUnknownWarning) false))}}>Cancel</button>
75+
{{#modal.footer data-test-overlapping-warning}}
76+
<button class="btn btn-primary" {{action (if showUnknownWarning (action (mut showOverlappingWarning) false) 'generateReports')}}>That's fine</button>
77+
<button class="btn btn-default" {{action (queue (action (mut showOverlappingWarning) false) (action (mut showUnknownWarning) false))}}>Cancel</button>
7878
{{/modal.footer}}
7979
{{/sy-modal}}
8080

@@ -86,7 +86,7 @@
8686
Unknown tasks will not be taken into account for the timesheet.
8787
{{/modal.body}}
8888
{{#modal.footer data-test-unknown-warning}}
89-
<button class="btn btn-primary" {{action (if showActiveWarning (action (mut showUnknownWarning) false) 'generateReports')}}>That's fine</button>
90-
<button class="btn btn-default" {{action (queue (action (mut showUnknownWarning) false) (action (mut showActiveWarning) false))}}>Cancel</button>
89+
<button class="btn btn-primary" {{action (if showOverlappingWarning (action (mut showUnknownWarning) false) 'generateReports')}}>That's fine</button>
90+
<button class="btn btn-default" {{action (queue (action (mut showUnknownWarning) false) (action (mut showOverlappingWarning) false))}}>Cancel</button>
9191
{{/modal.footer}}
9292
{{/sy-modal}}

app/models/activity.js

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import attr from 'ember-data/attr'
88
import moment from 'moment'
99
import computed from 'ember-computed-decorators'
1010
import RSVP from 'rsvp'
11+
import { inject as service } from '@ember/service'
1112

1213
import { belongsTo, hasMany } from 'ember-data/relationships'
1314

@@ -74,6 +75,14 @@ export default Model.extend({
7475
*/
7576
blocks: hasMany('activity-block'),
7677

78+
/**
79+
* The notify service
80+
*
81+
* @property {EmberNotify.NotifyService} notify
82+
* @public
83+
*/
84+
notify: service('notify'),
85+
7786
/**
7887
* The currently active block
7988
*
@@ -132,7 +141,11 @@ export default Model.extend({
132141
/**
133142
* Stop the activity
134143
*
135-
* When stopping an activity of a past day, we need to split it
144+
* If the activity was started yesterday, we create a new identical
145+
* activity today so we handle working over midnight
146+
*
147+
* If the activity was started even before, we ignore it since it must be
148+
* a mistake, so we end the activity a second before midnight that day
136149
*
137150
* @method stop
138151
* @public
@@ -143,22 +156,21 @@ export default Model.extend({
143156
return
144157
}
145158

146-
let daysBetween = moment().diff(this.get('date'), 'days')
159+
let activities = [this]
160+
161+
if (moment().diff(this.get('date'), 'days') === 1) {
162+
activities.push(
163+
this.get('store').createRecord('activity', {
164+
task: this.get('task'),
165+
comment: this.get('comment'),
166+
user: this.get('user'),
167+
date: moment(this.get('date')).add(1, 'days')
168+
})
169+
)
170+
}
147171

148172
await RSVP.all(
149-
[
150-
this,
151-
...Object.keys(Array(daysBetween).fill())
152-
.map(Number)
153-
.map(n => {
154-
return this.get('store').createRecord('activity', {
155-
task: this.get('task'),
156-
comment: this.get('comment'),
157-
user: this.get('user'),
158-
date: moment(this.get('date')).add(n + 1, 'days')
159-
})
160-
})
161-
].map(async activity => {
173+
activities.map(async activity => {
162174
if (activity.get('isNew')) {
163175
await activity.save()
164176
}
@@ -187,5 +199,11 @@ export default Model.extend({
187199
await block.save({ adapterOptions: { include: 'activity' } })
188200
})
189201
)
202+
203+
if (moment().diff(this.get('date'), 'days') > 1) {
204+
this.get('notify').info(
205+
'The activity overlapped multiple days, which is not possible. The activity was stopped at midnight of the day it was started.'
206+
)
207+
}
190208
}
191209
})

mirage/scenarios/default.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import moment from 'moment'
22

33
export default function(server) {
4-
// server.logging = false
4+
server.logging = false
55

66
server.loadFixtures('absence-types')
77

tests/acceptance/index-activities-test.js

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import destroyApp from '../helpers/destroy-app'
77
import { expect } from 'chai'
88
import startApp from '../helpers/start-app'
99
import moment from 'moment'
10+
import formatDuration from 'timed/utils/format-duration'
1011

1112
describe('Acceptance | index activities', function() {
1213
let application
@@ -204,39 +205,43 @@ describe('Acceptance | index activities', function() {
204205
expect(currentURL()).to.equal('/reports')
205206
})
206207

207-
it('shows a warning when generating reports from active activities', async function() {
208-
server.create('activity', 'active', { userId: this.user.id })
208+
it('shows a warning when generating reports from overlapping activities', async function() {
209+
let date = moment().subtract(1, 'days')
209210

210-
await visit('/')
211+
server.create('activity', 'active', { userId: this.user.id, date })
212+
213+
await visit(`/?day=${date.format('YYYY-MM-DD')}`)
211214

212215
await click('button:contains(Generate timesheet)')
213-
await click('[data-test-active-warning] button:contains(Cancel)')
216+
await click('[data-test-overlapping-warning] button:contains(Cancel)')
214217

215-
expect(currentURL()).to.equal('/')
218+
expect(currentURL()).to.not.contain('reports')
216219

217220
await click('button:contains(Generate timesheet)')
218-
await click('[data-test-active-warning] button:contains(fine)')
221+
await click('[data-test-overlapping-warning] button:contains(fine)')
219222

220-
expect(currentURL()).to.equal('/reports')
223+
expect(currentURL()).to.contain('reports')
221224
})
222225

223226
it('can handle both warnings', async function() {
224-
server.create('activity', 'unknown', { userId: this.user.id })
225-
server.create('activity', 'active', { userId: this.user.id })
227+
let date = moment().subtract(1, 'days')
226228

227-
await visit('/')
229+
server.create('activity', 'unknown', { userId: this.user.id, date })
230+
server.create('activity', 'active', { userId: this.user.id, date })
231+
232+
await visit(`/?day=${date.format('YYYY-MM-DD')}`)
228233

229234
// both close if one clicks cancel
230235
await click('button:contains(Generate timesheet)')
231236
expect(find('.modal--visible')).to.have.length(2)
232-
await click('[data-test-active-warning] button:contains(Cancel)')
237+
await click('[data-test-overlapping-warning] button:contains(Cancel)')
233238
expect(find('.modal--visible')).to.have.length(0)
234-
expect(currentURL()).to.equal('/')
239+
expect(currentURL()).to.not.contain('reports')
235240

236241
// both must be fine if it should continue
237242
await click('button:contains(Generate timesheet)')
238243
expect(find('.modal--visible')).to.have.length(2)
239-
await click('[data-test-active-warning] button:contains(fine)')
244+
await click('[data-test-overlapping-warning] button:contains(fine)')
240245
expect(find('.modal--visible')).to.have.length(1)
241246
await click('[data-test-unknown-warning] button:contains(Cancel)')
242247
expect(find('.modal--visible')).to.have.length(0)
@@ -245,69 +250,120 @@ describe('Acceptance | index activities', function() {
245250
expect(find('.modal--visible')).to.have.length(2)
246251
await click('[data-test-unknown-warning] button:contains(fine)')
247252
expect(find('.modal--visible')).to.have.length(1)
248-
await click('[data-test-active-warning] button:contains(Cancel)')
253+
await click('[data-test-overlapping-warning] button:contains(Cancel)')
249254
expect(find('.modal--visible')).to.have.length(0)
250-
expect(currentURL()).to.equal('/')
255+
expect(currentURL()).to.not.contain('reports')
251256

252257
// if both are fine continue
253258
await click('button:contains(Generate timesheet)')
254259
expect(find('.modal--visible')).to.have.length(2)
255-
await click('[data-test-active-warning] button:contains(fine)')
260+
await click('[data-test-overlapping-warning] button:contains(fine)')
256261
expect(find('.modal--visible')).to.have.length(1)
257262
await click('[data-test-unknown-warning] button:contains(fine)')
258263
expect(find('.modal--visible')).to.have.length(0)
259-
expect(currentURL()).to.equal('/reports')
264+
expect(currentURL()).to.contain('reports')
260265
})
261266

262-
it('splits overlapping activities when stopping', async function() {
267+
it('splits 1 day overlapping activities when stopping', async function() {
263268
let activity = server.create('activity', 'active', {
264269
userId: this.user.id,
265-
date: moment().subtract(2, 'days')
270+
date: moment().subtract(1, 'days')
266271
})
267272

268273
await visit('/')
269274

270275
await click('[data-test-record-stop]')
271276

272-
// todayday block should be from 00:00 to now
273-
expect(find(`[data-test-activity-row] td:contains(${activity.comment})`)).to
274-
.be.ok
277+
// today block should be from 00:00 to now
278+
expect(
279+
find(`[data-test-activity-row] td:contains(${activity.comment})`)
280+
).to.have.length(1)
275281

276282
await click(`[data-test-activity-row] td:contains(${activity.comment})`)
277283

278284
expect(
279285
find('[data-test-activity-block-row] td:eq(1) input').val()
280286
).to.equal('00:00')
281287

282-
// yesterday block should be from 00:00 to now
288+
// yesterday block should be from old start time to 23:59
283289
await visit('/')
284290
await click('[data-test-previous]')
285291

286-
expect(find(`[data-test-activity-row] td:contains(${activity.comment})`)).to
287-
.be.ok
292+
expect(
293+
find(`[data-test-activity-row] td:contains(${activity.comment})`)
294+
).to.have.length(1)
288295

289296
await click(`[data-test-activity-row] td:contains(${activity.comment})`)
290297

291298
expect(
292-
find('[data-test-activity-block-row] td:eq(1) input').val()
293-
).to.equal('00:00')
299+
find('[data-test-activity-block-row]:last td:eq(3) input').val()
300+
).to.equal('23:59')
301+
})
294302

303+
it("doesn't split >1 days overlapping activities when stopping", async function() {
304+
let activity = server.create('activity', 'active', {
305+
userId: this.user.id,
306+
date: moment().subtract(2, 'days')
307+
})
308+
309+
await visit('/')
310+
311+
await click('[data-test-record-stop]')
312+
313+
// today block should not exist
295314
expect(
296-
find('[data-test-activity-block-row] td:eq(3) input').val()
297-
).to.equal('23:59')
315+
find(`[data-test-activity-row] td:contains(${activity.comment})`)
316+
).to.have.length(0)
317+
318+
// yesterday block should not exist
319+
await visit('/')
320+
await click('[data-test-previous]')
321+
322+
expect(
323+
find(`[data-test-activity-row] td:contains(${activity.comment})`)
324+
).to.have.length(0)
298325

299326
// day before yesterday block should be from old start time to 23:59
300327
await visit('/')
301328
await click('[data-test-previous]')
302329
await click('[data-test-previous]')
303330

304-
expect(find(`[data-test-activity-row] td:contains(${activity.comment})`)).to
305-
.be.ok
331+
expect(
332+
find(`[data-test-activity-row] td:contains(${activity.comment})`)
333+
).to.have.length(1)
306334

307335
await click(`[data-test-activity-row] td:contains(${activity.comment})`)
308336

309337
expect(
310338
find('[data-test-activity-block-row]:last td:eq(3) input').val()
311339
).to.equal('23:59')
312340
})
341+
342+
it('can generate active reports which do not overlap', async function() {
343+
let activity = server.create('activity', 'active', { userId: this.user.id })
344+
let { id, duration } = activity
345+
346+
duration = moment.duration(duration, 'HH:mm:ss').add(
347+
moment().diff(
348+
moment(
349+
activity.blocks.models.find(b => {
350+
return !b.toTime
351+
}).fromTime,
352+
'HH:mm:ss'
353+
)
354+
)
355+
)
356+
357+
await visit('/')
358+
359+
await click(find('button:contains(Generate timesheet)'))
360+
361+
expect(currentURL()).to.equal('/reports')
362+
363+
expect(find('[data-test-report-row]')).to.have.length(7)
364+
365+
expect(
366+
find(`${`[data-test-report-row-id="${id}"]`} [name=duration]`).val()
367+
).to.equal(formatDuration(duration, false))
368+
})
313369
})

tests/unit/models/activity-test.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ import { expect } from 'chai'
44

55
describe('Unit | Model | activity', function() {
66
setupModelTest('activity', {
7-
needs: ['model:activity-block', 'model:task', 'model:user']
7+
needs: [
8+
'model:activity-block',
9+
'model:task',
10+
'model:user',
11+
'service:notify'
12+
]
813
})
914

1015
it('exists', function() {

0 commit comments

Comments
 (0)