Skip to content

Commit eae388c

Browse files
authored
Merge pull request #4 from eporsche/default_resting_times
adds mandatory resting times to daybreak
2 parents 625cae6 + 533bb0f commit eae388c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+1350
-87
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ This program is inspired by papershift. If you are looking for something more ro
2222
## Open Todos
2323

2424
- [ ] Documentation
25-
- [ ] Add automatic pause times after "x" working hours
25+
- [x] Add automatic pause times after "x" working hours
2626
- [ ] Include holiday importer for other countries
2727
- [ ] Make timezone of location configurable and make use of it
2828
- [ ] Add more absence times calculators
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
namespace App\Actions;
4+
5+
use App\Contracts\AddsDefaultRestingTime;
6+
use Illuminate\Support\Facades\Validator;
7+
use DB;
8+
9+
class AddDefaultRestingTime implements AddsDefaultRestingTime
10+
{
11+
public function add($location, array $data)
12+
{
13+
Validator::make($data, [
14+
'min_hours' => ['required'],
15+
'duration' => ['required'],
16+
])->validate();
17+
18+
DB::transaction(function () use ($location, $data) {
19+
$defaultRestingTime = $location->defaultRestingTimes()->create($data);
20+
21+
$location->allUsers()->each(function ($user) use ($defaultRestingTime) {
22+
23+
$user->defaultRestingTimes()->attach($defaultRestingTime);
24+
});
25+
26+
});
27+
28+
}
29+
}

app/Actions/AddTimeTracking.php

Lines changed: 37 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use App\Contracts\AddsTimeTrackings;
1111
use Illuminate\Support\Facades\Validator;
1212
use Illuminate\Validation\ValidationException;
13+
use DB;
1314

1415
class AddTimeTracking implements AddsTimeTrackings
1516
{
@@ -25,38 +26,56 @@ public function add($employee, array $data, array $pauseTimes)
2526
Validator::make($data,[
2627
'starts_at' => ['required', $this->dateFormatter->dateTimeFormatRule() ],
2728
'ends_at' => ['required', $this->dateFormatter->dateTimeFormatRule() , 'after_or_equal:starts_at'],
28-
'manual_pause' => ['required', 'boolean'],
2929
'description' => ['nullable', 'string']
3030
])->validateWithBag('addTimeTracking');
3131

3232
$startsAt = $this->dateFormatter->timeStrToCarbon($data['starts_at']);
3333
$endsAt = $this->dateFormatter->timeStrToCarbon($data['ends_at']);
3434

35-
//check date
3635
$this->ensureDateIsNotBeforeEmploymentDate($employee, $startsAt);
3736
$this->ensureDateIsNotTooFarInTheFuture($endsAt);
38-
39-
//check time
4037
$this->ensureGivenTimeIsNotOverlappingWithExisting($employee, $startsAt, $endsAt);
4138

42-
//check pause times
43-
$periods = PeriodCalculator::fromTimesArray($pauseTimes)->periods;
39+
$this->validatePauseTimes(
40+
PeriodCalculator::fromTimesArray($pauseTimes),
41+
$startsAt,
42+
$endsAt
43+
);
4444

45-
foreach ($periods as $index => $period) {
46-
$this->ensurePeriodIsNotTooSmall($period);
47-
$this->ensurePeriodsAreNotOverlapping($periods, $index, $period);
48-
$this->ensurePeriodWithinWorkingHours($period, $startsAt, $endsAt);
49-
}
45+
DB::transaction(function () use ($employee, $startsAt, $endsAt, $data, $pauseTimes) {
46+
$trackedTime = $employee->timeTrackings()->create(array_merge([
47+
'location_id' => $employee->currentLocation->id,
48+
'starts_at' => $startsAt,
49+
'ends_at' => $endsAt,
5050

51-
$trackedTime = $employee->timeTrackings()->create(array_merge([
52-
'location_id' => $employee->currentLocation->id,
53-
'starts_at' => $startsAt,
54-
'ends_at' => $endsAt
55-
], Arr::except($data, ['starts_at','ends_at'])));
51+
], Arr::except($data, ['starts_at','ends_at'])));
52+
53+
$trackedTime->pauseTimes()->createMany($pauseTimes);
54+
55+
$trackedTime->updatePauseTime();
56+
});
5657

57-
$trackedTime->pauseTimes()->createMany($pauseTimes);
5858
}
5959

60+
protected function validatePauseTimes($pauseTimePeriodCalculator, $startsAt, $endsAt)
61+
{
62+
if (!$pauseTimePeriodCalculator->hasPeriods()) {
63+
return;
64+
}
65+
66+
$pauseTimePeriodCalculator->periods->each(function ($period, $index) use ($pauseTimePeriodCalculator, $startsAt, $endsAt) {
67+
$this->ensurePeriodIsNotTooSmall($period);
68+
$this->ensurePeriodsAreNotOverlapping($pauseTimePeriodCalculator->periods, $index, $period);
69+
$this->ensurePeriodWithinWorkingHours($period, $startsAt, $endsAt);
70+
});
71+
}
72+
73+
protected function calculatePauseTimeFromDefaultRestingTimes($employee, $workingTimeInSeconds)
74+
{
75+
return optional(
76+
$employee->defaultRestingTimes()->firstWhere('min_hours','<=',$workingTimeInSeconds)
77+
)->duration->inSeconds();
78+
}
6079

6180
protected function ensureDateIsNotTooFarInTheFuture($endsAt)
6281
{
@@ -115,7 +134,7 @@ protected function ensurePeriodWithinWorkingHours($period, $startsAt, $endsAt)
115134

116135
protected function ensurePeriodsAreNotOverlapping($periods, $index, $period)
117136
{
118-
$haystack = Arr::except($periods, [$index]);
137+
$haystack = Arr::except($periods->toArray(), [$index]);
119138
foreach ($haystack as $needle) {
120139
/**
121140
* @var CarbonPeriod $period

app/Actions/Fortify/CreateNewUser.php

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Carbon\Carbon;
66
use App\Models\User;
77
use App\Models\Account;
8+
use App\Models\Duration;
89
use App\Models\Location;
910
use Laravel\Jetstream\Jetstream;
1011
use Illuminate\Support\Facades\DB;
@@ -38,14 +39,45 @@ public function create(array $input)
3839
'password' => Hash::make($input['password']),
3940
'date_of_employment' => Carbon::today()
4041
]), function (User $user) {
42+
/*
43+
|--------------------------------------------------------------------------
44+
| Set up a new user environment.
45+
|--------------------------------------------------------------------------
46+
*/
47+
48+
//create new account for user
4149
$this->createAccount($user);
50+
51+
//create default target hours for user
52+
$this->createDefaultTargetHours($user);
53+
54+
//create new location for user
4255
$location = $this->createLocation($user);
56+
57+
//create and assign default absent types
4358
$this->createDefaultAbsentTypes($user, $location);
44-
$this->createDefaultTargetHours($user);
59+
60+
//create default resting times for user
61+
$this->createDefaultRestingTime($user, $location);
4562
});
4663
});
4764
}
4865

66+
public function createDefaultRestingTime($user, $location)
67+
{
68+
$defaultRestingTimes = $location->defaultRestingTimes()->createMany([
69+
[
70+
'min_hours' => new Duration(21600), //6*60*60
71+
'duration' => new Duration(1800) //30*60
72+
], [
73+
'min_hours' => new Duration(39600), //11*60*60
74+
'duration' => new Duration(2700) //45*60
75+
]
76+
]);
77+
78+
$user->defaultRestingTimes()->sync($defaultRestingTimes);
79+
}
80+
4981
/**
5082
* Create the default Location.
5183
*
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
namespace App\Actions;
3+
4+
use App\Models\Location;
5+
use App\Contracts\RemovesDefaultRestingTime;
6+
7+
class RemoveDefaultRestingTime implements RemovesDefaultRestingTime
8+
{
9+
/**
10+
* Remove default resting time from a location
11+
*
12+
* @param mixed $location
13+
* @param mixed $defaultRestingTimeIdBeingRemoved
14+
* @return void
15+
*/
16+
public function remove(Location $location, $defaultRestingTimeIdBeingRemoved)
17+
{
18+
$location->defaultRestingTimes()->whereKey($defaultRestingTimeIdBeingRemoved)->delete();
19+
}
20+
}

app/Actions/UpdateTimeTracking.php

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public function update(User $employee, $timeTrackingId, array $data, array $paus
2929
]),[
3030
'starts_at' => ['required', $this->dateFormatter->dateTimeFormatRule() ],
3131
'ends_at' => ['required', $this->dateFormatter->dateTimeFormatRule() , 'after_or_equal:starts_at'],
32-
'manual_pause' => ['required', 'boolean'],
3332
'description' => ['nullable', 'string'],
3433
'time_tracking_id' => ['required', 'exists:time_trackings,id'],
3534
])->validateWithBag('addTimeTracking');
@@ -43,13 +42,12 @@ public function update(User $employee, $timeTrackingId, array $data, array $paus
4342
$this->ensureGivenTimeIsNotOverlappingWithExisting($employee, $startsAt, $endsAt, $timeTrackingId);
4443

4544
//check pause times
46-
$periods = PeriodCalculator::fromTimesArray($pauseTimes)->periods;
4745

48-
foreach ($periods as $index => $period) {
49-
$this->ensurePeriodIsNotTooSmall($period);
50-
$this->ensurePeriodsAreNotOverlapping($periods, $index, $period);
51-
$this->ensurePeriodWithinWorkingHours($period, $startsAt, $endsAt);
52-
}
46+
$this->validatePauseTimes(
47+
PeriodCalculator::fromTimesArray($pauseTimes),
48+
$startsAt,
49+
$endsAt
50+
);
5351

5452
$trackedTime = $employee->timeTrackings()->whereKey($timeTrackingId)->first();
5553

@@ -62,6 +60,21 @@ public function update(User $employee, $timeTrackingId, array $data, array $paus
6260
$trackedTime->pauseTimes->each->delete();
6361

6462
$trackedTime->pauseTimes()->createMany($pauseTimes);
63+
64+
$trackedTime->updatePauseTime();
65+
});
66+
}
67+
68+
protected function validatePauseTimes($pauseTimePeriodCalculator, $startsAt, $endsAt)
69+
{
70+
if (!$pauseTimePeriodCalculator->hasPeriods()) {
71+
return;
72+
}
73+
74+
$pauseTimePeriodCalculator->periods->each(function ($period, $index) use ($pauseTimePeriodCalculator, $startsAt, $endsAt) {
75+
$this->ensurePeriodIsNotTooSmall($period);
76+
$this->ensurePeriodsAreNotOverlapping($pauseTimePeriodCalculator->periods, $index, $period);
77+
$this->ensurePeriodWithinWorkingHours($period, $startsAt, $endsAt);
6578
});
6679
}
6780

app/Calculators/PeriodCalculator.php

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@
22

33
namespace App\Calculators;
44

5-
use Brick\Math\BigDecimal;
6-
use Brick\Math\BigInteger;
7-
use Brick\Math\BigRational;
8-
use Brick\Math\RoundingMode;
95
use Carbon\CarbonPeriod;
6+
use Brick\Math\BigDecimal;
107
use Carbon\CarbonInterval;
118
use Carbon\CarbonImmutable;
9+
use Illuminate\Support\Arr;
10+
use Brick\Math\RoundingMode;
1211
use Illuminate\Support\Collection;
1312

1413
class PeriodCalculator
@@ -44,7 +43,7 @@ public function fromTimesArray($timesArray)
4443

4544
$periods[] = $period;
4645
}
47-
$this->periods = $periods;
46+
$this->periods = collect($periods);
4847

4948
return $this;
5049
}
@@ -71,4 +70,20 @@ public function toMinutes() : BigDecimal
7170
}
7271
return $minutes;
7372
}
73+
74+
public function toSeconds() : BigDecimal
75+
{
76+
$seconds = BigDecimal::zero();
77+
foreach ($this->periods as $period) {
78+
$seconds = $seconds->plus(
79+
BigDecimal::of($period->start->diffInSeconds($period->end))
80+
);
81+
}
82+
return $seconds;
83+
}
84+
85+
public function hasPeriods() : bool
86+
{
87+
return count($this->periods) ? true : false;
88+
}
7489
}

app/Casts/DurationCast.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace App\Casts;
4+
5+
use App\Models\Duration;
6+
use Brick\Math\BigDecimal;
7+
use PHPUnit\Framework\MockObject\UnknownTypeException;
8+
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9+
10+
class DurationCast implements CastsAttributes
11+
{
12+
/**
13+
* Cast the given value.
14+
*
15+
* @param \Illuminate\Database\Eloquent\Model $model
16+
* @param string $key
17+
* @param mixed $value
18+
* @param array $attributes
19+
* @return mixed
20+
*/
21+
public function get($model, $key, $value, $attributes)
22+
{
23+
if (!$value) {
24+
return new Duration(0);
25+
}
26+
return new Duration($value);
27+
}
28+
29+
/**
30+
* Prepare the given value for storage.
31+
*
32+
* @param \Illuminate\Database\Eloquent\Model $model
33+
* @param string $key
34+
* @param mixed $value
35+
* @param array $attributes
36+
* @return mixed
37+
*/
38+
public function set($model, $key, $value, $attributes)
39+
{
40+
if ($value instanceof BigDecimal || is_string($value)) {
41+
return $value;
42+
}
43+
44+
if ($value instanceof Duration) {
45+
return $value->inSeconds();
46+
}
47+
48+
throw new UnknownTypeException("Value should be of type BigDecimal or Duration");
49+
}
50+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use App\Models\TimeTracking;
6+
use Illuminate\Console\Command;
7+
use App\Models\DefaultRestingTime;
8+
9+
class RecalculatePauseTimes extends Command
10+
{
11+
/**
12+
* The name and signature of the console command.
13+
*
14+
* @var string
15+
*/
16+
protected $signature = 'daybreak:recalculate-pause-times';
17+
18+
/**
19+
* The console command description.
20+
*
21+
* @var string
22+
*/
23+
protected $description = 'Recalculate and update time tracking pause times based on given pause times and default resting times';
24+
25+
/**
26+
* Create a new command instance.
27+
*
28+
* @return void
29+
*/
30+
// public function __construct()
31+
// {
32+
// parent::__construct();
33+
// }
34+
35+
/**
36+
* Execute the console command.
37+
*
38+
* @return int
39+
*/
40+
public function handle()
41+
{
42+
TimeTracking::with(['user.defaultRestingTimes','pauseTimes'])->get()->each->updatePauseTime();
43+
}
44+
}

0 commit comments

Comments
 (0)