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

Commit ae41241

Browse files
authored
Merge pull request #115 from anehx/activity_split
Handle activities over multiple days
2 parents 306cf25 + c3f8037 commit ae41241

File tree

19 files changed

+342
-243
lines changed

19 files changed

+342
-243
lines changed

app/adapters/activity-block.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,19 @@ export default ApplicationAdapter.extend({
2525
*/
2626
urlForUpdateRecord() {
2727
return `${this._super(...arguments)}?include=activity`
28+
},
29+
30+
/**
31+
* Custom url for creating records
32+
*
33+
* This causes a reload of the activity so we don't have to do the reload
34+
* ourselves
35+
*
36+
* @method urlForCreateRecord
37+
* @return {String} The URL
38+
* @public
39+
*/
40+
urlForCreateRecord() {
41+
return `${this._super(...arguments)}?include=activity`
2842
}
2943
})

app/index/activities/controller.js

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,12 @@ export default Controller.extend({
3131
* @property {Activity[]} activities
3232
* @public
3333
*/
34-
@computed(
35-
'_allActivities.@each.{start,user,isNew,isDeleted}',
36-
'model',
37-
'user'
38-
)
34+
@computed('_allActivities.@each.{date,user,isNew,isDeleted}', 'model', 'user')
3935
activities(activities, day, user) {
4036
return activities.filter(a => {
4137
return (
42-
a.get('start').isSame(day, 'day') &&
38+
a.get('date') &&
39+
a.get('date').isSame(day, 'day') &&
4340
a.get('user.id') === user.get('id') &&
4441
!a.get('isNew') &&
4542
!a.get('isDeleted')

app/index/activities/edit/template.hbs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
<tbody>
2424
{{#each (reject-by 'isDeleted' true blocks) as |block|}}
2525
<tr
26-
class="{{if block.model.overlaps 'danger'}}"
2726
data-test-activity-block-row
2827
data-test-activity-block-row-id={{block.model.id}}
2928
>
@@ -57,11 +56,6 @@
5756
}}
5857
</td>
5958
<td>
60-
{{#if block.model.overlaps}}
61-
<span title="This block overlaps the day of the activity" class="error-text">
62-
{{fa-icon 'warning'}}
63-
</span>
64-
{{/if}}
6559
{{#if (and block.changeset.from block.changeset.to)}}
6660
{{format-duration (moment-diff block.changeset.from block.changeset.to) false}}
6761
{{/if}}

app/index/activities/route.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,9 @@ export default Route.extend(RouteAutostartTourMixin, {
7979
* @public
8080
*/
8181
async startActivity(activity) {
82-
if (!activity.get('start').isSame(moment(), 'day')) {
82+
if (!activity.get('date').isSame(moment(), 'day')) {
8383
activity = this.store.createRecord('activity', {
84+
date: moment(),
8485
...activity.getProperties('task', 'comment')
8586
})
8687
}
@@ -120,9 +121,11 @@ export default Route.extend(RouteAutostartTourMixin, {
120121
'task.id',
121122
undefined
122123
)
123-
let hasOverlapping = !!this.get('controller.activities').findBy(
124-
'overlaps'
125-
)
124+
let hasOverlapping = !!this.get('controller.activities').find(a => {
125+
return (
126+
a.get('active') && !a.get('activeBlock.from').isSame(moment(), 'day')
127+
)
128+
})
126129

127130
this.set('controller.showUnknownWarning', hasUnknown)
128131
this.set('controller.showOverlappingWarning', hasOverlapping)
@@ -145,7 +148,14 @@ export default Route.extend(RouteAutostartTourMixin, {
145148
try {
146149
await RSVP.all(
147150
this.get('controller.activities')
148-
.filter(a => a.get('task.id') && !a.get('overlaps'))
151+
.filter(
152+
a =>
153+
a.get('task.id') &&
154+
!(
155+
a.get('active') &&
156+
!a.get('activeBlock.from').isSame(moment(), 'day')
157+
)
158+
)
149159
.map(async activity => {
150160
let duration = moment.duration(activity.get('duration'))
151161

@@ -156,7 +166,7 @@ export default Route.extend(RouteAutostartTourMixin, {
156166
let data = {
157167
activity,
158168
duration,
159-
date: activity.get('start'),
169+
date: activity.get('date'),
160170
task: activity.get('task'),
161171
comment: activity.get('comment')
162172
}

app/index/activities/template.hbs

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
<tbody>
1010
{{#each activities as |activity|}}
1111
<tr
12-
class="{{if activity.overlaps 'danger'}}"
1312
title="Click to edit"
1413
data-test-activity-row
1514
data-test-activity-row-id={{activity.id}}
@@ -29,12 +28,6 @@
2928
</td>
3029
<td>{{activity.comment}}</td>
3130
<td>
32-
{{#if activity.overlaps}}
33-
<span title="This activity has an overlapping block" class="error-text">
34-
{{fa-icon 'warning'}}
35-
&nbsp;
36-
</span>
37-
{{/if}}
3831
{{#if activity.active}}
3932
{{duration-since activity.activeBlock.from elapsed=activity.duration}}
4033
{{else}}
@@ -79,7 +72,7 @@
7972
{{#modal.body}}
8073
Overlapping activities will not be taken into account for the timesheet.
8174
{{/modal.body}}
82-
{{#modal.footer data-test-overlap-warning}}
75+
{{#modal.footer data-test-overlapping-warning}}
8376
<button class="btn btn-primary" {{action (if showUnknownWarning (action (mut showOverlappingWarning) false) 'generateReports')}}>That's fine</button>
8477
<button class="btn btn-default" {{action (queue (action (mut showOverlappingWarning) false) (action (mut showUnknownWarning) false))}}>Cancel</button>
8578
{{/modal.footer}}

app/index/controller.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,12 @@ export default Controller.extend({
8282
* @property {Activity[]} _activities
8383
* @private
8484
*/
85-
@computed('date', '_allActivities.@each.{start,user,isDeleted}', 'user')
85+
@computed('date', '_allActivities.@each.{date,user,isDeleted}', 'user')
8686
_activities(day, activities, user) {
8787
let activitiesThen = activities.filter(a => {
8888
return (
89-
a.get('start').isSame(day, 'day') &&
89+
a.get('date') &&
90+
a.get('date').isSame(day, 'day') &&
9091
a.get('user.id') === user.get('id') &&
9192
!a.get('isDeleted')
9293
)
@@ -107,7 +108,7 @@ export default Controller.extend({
107108
* @property {moment.duration} activitySum
108109
* @public
109110
*/
110-
@computed('_activities.@each.{from,to}', '_activeActivityBlockDuration')
111+
@computed('_activities.@each.duration', '_activeActivityBlockDuration')
111112
activitySum(activities, additional) {
112113
return activities.reduce((dur, cur) => {
113114
return dur.add(cur.get('duration'))
@@ -166,6 +167,7 @@ export default Controller.extend({
166167
_attendances(date, attendances, user) {
167168
return attendances.filter(a => {
168169
return (
170+
a.get('date') &&
169171
a.get('date').isSame(date, 'day') &&
170172
a.get('user.id') === user.get('id') &&
171173
!a.get('isDeleted')

app/models/activity-block.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,22 +19,20 @@ import { belongsTo } from 'ember-data/relationships'
1919
*/
2020
export default Model.extend({
2121
/**
22-
* The start date and time
22+
* The start time
2323
*
24-
* @property from
25-
* @type {moment}
24+
* @property {moment} fromTime
2625
* @public
2726
*/
28-
from: attr('django-datetime'),
27+
fromTime: attr('django-time'),
2928

3029
/**
31-
* The end date and time
30+
* The end time
3231
*
33-
* @property to
34-
* @type {moment}
32+
* @property {moment} toTime
3533
* @public
3634
*/
37-
to: attr('django-datetime'),
35+
toTime: attr('django-time'),
3836

3937
/**
4038
* The activity
@@ -46,13 +44,40 @@ export default Model.extend({
4644
activity: belongsTo('activity'),
4745

4846
/**
49-
* Whether the blocks overlaps a day
50-
*
51-
* @property {Boolean} overlaps
47+
* The start time, with the date of the related activity
48+
*
49+
* @property {moment} from
50+
* @public
51+
*/
52+
@computed('activity.date', 'fromTime')
53+
from(date, time) {
54+
return (
55+
time &&
56+
moment(date).set({
57+
h: time.hours(),
58+
m: time.minutes(),
59+
s: time.seconds(),
60+
ms: time.milliseconds()
61+
})
62+
)
63+
},
64+
65+
/**
66+
* The end time, with the date of the related activity
67+
*
68+
* @property {moment} to
5269
* @public
5370
*/
54-
@computed('activity.start', 'to')
55-
overlaps(start, to) {
56-
return !(to || moment()).isSame(start, 'day')
71+
@computed('activity.date', 'toTime')
72+
to(date, time) {
73+
return (
74+
time &&
75+
moment(date).set({
76+
h: time.hours(),
77+
m: time.minutes(),
78+
s: time.seconds(),
79+
ms: time.milliseconds()
80+
})
81+
)
5782
}
5883
})

app/models/activity.js

Lines changed: 107 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,13 @@ import Model from 'ember-data/model'
77
import attr from 'ember-data/attr'
88
import moment from 'moment'
99
import computed from 'ember-computed-decorators'
10+
import RSVP from 'rsvp'
11+
import { inject as service } from '@ember/service'
1012

1113
import { belongsTo, hasMany } from 'ember-data/relationships'
1214

15+
const { min } = Math
16+
1317
/**
1418
* The activity model
1519
*
@@ -28,13 +32,12 @@ export default Model.extend({
2832
comment: attr('string', { defaultValue: '' }),
2933

3034
/**
31-
* The start date and time
35+
* The date
3236
*
33-
* @property start
34-
* @type {moment}
37+
* @property {moment} date
3538
* @public
3639
*/
37-
start: attr('django-datetime', { defaultValue: () => moment() }),
40+
date: attr('django-date'),
3841

3942
/**
4043
* The duration
@@ -72,6 +75,14 @@ export default Model.extend({
7275
*/
7376
blocks: hasMany('activity-block'),
7477

78+
/**
79+
* The notify service
80+
*
81+
* @property {EmberNotify.NotifyService} notify
82+
* @public
83+
*/
84+
notify: service('notify'),
85+
7586
/**
7687
* The currently active block
7788
*
@@ -99,13 +110,100 @@ export default Model.extend({
99110
},
100111

101112
/**
102-
* Whether the activity has any overlapping blocks
113+
* Start the activity
103114
*
104-
* @property {Boolean} overlaps
115+
* @method start
105116
* @public
106117
*/
107-
@computed('start', 'blocks.@each.to')
108-
overlaps(start, blocks) {
109-
return blocks.any(b => b.get('overlaps'))
118+
async start() {
119+
/* istanbul ignore next */
120+
if (this.get('active')) {
121+
return
122+
}
123+
124+
if (this.get('isNew')) {
125+
await this.save()
126+
}
127+
128+
let block = this.get('store').createRecord('activity-block', {
129+
activity: this,
130+
fromTime: moment()
131+
})
132+
133+
await block.save()
134+
135+
// Sadly, we need to do this here since the computed property
136+
// 'activeBlock' on the activity does not sense a change when the blocks
137+
// change from new to actually loaded
138+
this.notifyPropertyChange('blocks')
139+
},
140+
141+
/**
142+
* Stop the activity
143+
*
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
149+
*
150+
* @method stop
151+
* @public
152+
*/
153+
async stop() {
154+
/* istanbul ignore next */
155+
if (!this.get('active')) {
156+
return
157+
}
158+
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+
}
171+
172+
await RSVP.all(
173+
activities.map(async activity => {
174+
if (activity.get('isNew')) {
175+
await activity.save()
176+
}
177+
178+
let block =
179+
activity.get('activeBlock') ||
180+
this.get('store').createRecord('activity-block', {
181+
activity,
182+
fromTime: moment({ h: 0, m: 0, s: 0 })
183+
})
184+
185+
block.set(
186+
'toTime',
187+
moment(
188+
min(
189+
moment(activity.get('date')).set({
190+
h: 23,
191+
m: 59,
192+
s: 59
193+
}),
194+
moment()
195+
)
196+
)
197+
)
198+
199+
await block.save({ adapterOptions: { include: 'activity' } })
200+
})
201+
)
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+
}
110208
}
111209
})

0 commit comments

Comments
 (0)