Skip to content

Commit 4b72663

Browse files
committed
Add rounding feature
1 parent e1185af commit 4b72663

File tree

14 files changed

+735
-28
lines changed

14 files changed

+735
-28
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App\Enums;
6+
7+
use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
8+
9+
enum TimeEntryRoundingType: string
10+
{
11+
use LaravelEnumHelper;
12+
13+
case Up = 'up';
14+
case Down = 'down';
15+
case Nearest = 'nearest';
16+
}

app/Http/Controllers/Api/V1/Public/ReportController.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,9 @@ public function show(Request $request, TimeEntryAggregationService $timeEntryAgg
7373
false,
7474
$report->properties->start,
7575
$report->properties->end,
76-
true
76+
true,
77+
$report->properties->roundingType,
78+
$report->properties->roundingMinutes,
7779
);
7880
$historyData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
7981
$timeEntriesQuery->clone(),
@@ -84,7 +86,9 @@ public function show(Request $request, TimeEntryAggregationService $timeEntryAgg
8486
true,
8587
$report->properties->start,
8688
$report->properties->end,
87-
true
89+
true,
90+
$report->properties->roundingType,
91+
$report->properties->roundingMinutes,
8892
);
8993

9094
return new DetailedWithDataReportResource($report, $data, $historyData);

app/Http/Controllers/Api/V1/TimeEntryController.php

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
use App\Service\ReportExport\TimeEntriesReportExport;
3434
use App\Service\TimeEntryAggregationService;
3535
use App\Service\TimeEntryFilter;
36+
use App\Service\TimeEntryService;
3637
use App\Service\TimezoneService;
3738
use Gotenberg\Exceptions\GotenbergApiErrored;
3839
use Gotenberg\Exceptions\NoOutputFileInResponse;
@@ -47,6 +48,7 @@
4748
use Illuminate\Support\Collection;
4849
use Illuminate\Support\Facades\Auth;
4950
use Illuminate\Support\Facades\Blade;
51+
use Illuminate\Support\Facades\DB;
5052
use Illuminate\Support\Facades\Log;
5153
use Illuminate\Support\Facades\Storage;
5254
use Maatwebsite\Excel\Facades\Excel;
@@ -140,8 +142,15 @@ public function index(Organization $organization, TimeEntryIndexRequest $request
140142
*/
141143
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
142144
{
145+
$select = TimeEntry::SELECT_COLUMNS;
146+
if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) {
147+
$select = array_diff($select, ['start', 'end']);
148+
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start');
149+
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end');
150+
}
143151
$timeEntriesQuery = TimeEntry::query()
144152
->whereBelongsTo($organization, 'organization')
153+
->select($select)
145154
->orderBy('start', 'desc');
146155

147156
$filter = new TimeEntryFilter($timeEntriesQuery);
@@ -183,6 +192,8 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ
183192
$user = $this->user();
184193
$timezone = $user->timezone;
185194
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
195+
$roundingType = $request->getRoundingType();
196+
$roundingMinutes = $request->getRoundingMinutes();
186197

187198
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
188199
$timeEntriesQuery->with([
@@ -207,16 +218,19 @@ public function indexExport(Organization $organization, TimeEntryIndexExportRequ
207218
if ($viewFile === false) {
208219
throw new \LogicException('View file not found');
209220
}
221+
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
210222
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
211-
$timeEntriesQuery->clone()->reorder()->withOnly([]),
223+
$timeEntriesAggregateQuery,
212224
null,
213225
null,
214226
$user->timezone,
215227
$user->week_start,
216228
false,
217229
null,
218230
null,
219-
$showBillableRate
231+
$showBillableRate,
232+
$roundingType,
233+
$roundingMinutes,
220234
);
221235
$html = Blade::render($viewFile, [
222236
'timeEntries' => $timeEntriesQuery->get(),
@@ -324,6 +338,8 @@ public function aggregate(Organization $organization, TimeEntryAggregateRequest
324338
$group1Type = $request->getGroup();
325339
$group2Type = $request->getSubGroup();
326340
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
341+
$roundingType = $request->getRoundingType();
342+
$roundingMinutes = $request->getRoundingMinutes();
327343

328344
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
329345
$timeEntriesAggregateQuery,
@@ -334,7 +350,9 @@ public function aggregate(Organization $organization, TimeEntryAggregateRequest
334350
$request->getFillGapsInTimeGroups(),
335351
$request->getStart(),
336352
$request->getEnd(),
337-
$showBillableRate
353+
$showBillableRate,
354+
$roundingType,
355+
$roundingMinutes
338356
);
339357

340358
return [
@@ -373,6 +391,8 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
373391
$group = $request->getGroup();
374392
$subGroup = $request->getSubGroup();
375393
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
394+
$roundingType = $request->getRoundingType();
395+
$roundingMinutes = $request->getRoundingMinutes();
376396

377397
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
378398
$timeEntriesAggregateQuery->clone(),
@@ -383,7 +403,9 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
383403
false,
384404
$request->getStart(),
385405
$request->getEnd(),
386-
$showBillableRate
406+
$showBillableRate,
407+
$roundingType,
408+
$roundingMinutes
387409
);
388410
$dataHistoryChart = $timeEntryAggregationService->getAggregatedTimeEntries(
389411
$timeEntriesAggregateQuery->clone(),
@@ -394,7 +416,9 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
394416
true,
395417
$request->getStart(),
396418
$request->getEnd(),
397-
$showBillableRate
419+
$showBillableRate,
420+
$roundingType,
421+
$roundingMinutes
398422
);
399423
$currency = $organization->currency;
400424
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
@@ -477,7 +501,7 @@ public function aggregateExport(Organization $organization, TimeEntryAggregateEx
477501
/**
478502
* @return Builder<TimeEntry>
479503
*/
480-
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest $request, ?Member $member): Builder
504+
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
481505
{
482506
$timeEntriesQuery = TimeEntry::query()
483507
->whereBelongsTo($organization, 'organization');

app/Http/Requests/V1/TimeEntry/TimeEntryAggregateExportRequest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use App\Enums\ExportFormat;
88
use App\Enums\TimeEntryAggregationType;
99
use App\Enums\TimeEntryAggregationTypeInterval;
10+
use App\Enums\TimeEntryRoundingType;
1011
use App\Http\Requests\V1\BaseFormRequest;
1112
use App\Models\Client;
1213
use App\Models\Member;
@@ -164,6 +165,18 @@ public function rules(): array
164165
'string',
165166
'in:true,false',
166167
],
168+
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
169+
'rounding_type' => [
170+
'nullable',
171+
'string',
172+
Rule::enum(TimeEntryRoundingType::class),
173+
],
174+
// Defines the length of the interval that the time entry rounding rounds to.
175+
'rounding_minutes' => [
176+
'nullable',
177+
'numeric',
178+
'integer',
179+
],
167180
];
168181
}
169182

@@ -211,4 +224,22 @@ public function getFormatValue(): ExportFormat
211224
{
212225
return ExportFormat::from($this->validated('format'));
213226
}
227+
228+
public function getRoundingType(): ?TimeEntryRoundingType
229+
{
230+
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
231+
return null;
232+
}
233+
234+
return TimeEntryRoundingType::from($this->validated('rounding_type'));
235+
}
236+
237+
public function getRoundingMinutes(): ?int
238+
{
239+
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
240+
return null;
241+
}
242+
243+
return (int) $this->validated('rounding_minutes');
244+
}
214245
}

app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Http\Requests\V1\TimeEntry;
66

77
use App\Enums\TimeEntryAggregationType;
8+
use App\Enums\TimeEntryRoundingType;
89
use App\Http\Requests\V1\BaseFormRequest;
910
use App\Models\Client;
1011
use App\Models\Member;
@@ -146,6 +147,18 @@ public function rules(): array
146147
'string',
147148
'in:true,false',
148149
],
150+
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
151+
'rounding_type' => [
152+
'nullable',
153+
'string',
154+
Rule::enum(TimeEntryRoundingType::class),
155+
],
156+
// Defines the length of the interval that the time entry rounding rounds to.
157+
'rounding_minutes' => [
158+
'nullable',
159+
'numeric',
160+
'integer',
161+
],
149162
];
150163
}
151164

@@ -173,4 +186,22 @@ public function getEnd(): ?Carbon
173186
{
174187
return $this->input('end') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC') : null;
175188
}
189+
190+
public function getRoundingType(): ?TimeEntryRoundingType
191+
{
192+
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
193+
return null;
194+
}
195+
196+
return TimeEntryRoundingType::from($this->validated('rounding_type'));
197+
}
198+
199+
public function getRoundingMinutes(): ?int
200+
{
201+
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
202+
return null;
203+
}
204+
205+
return (int) $this->validated('rounding_minutes');
206+
}
176207
}

app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace App\Http\Requests\V1\TimeEntry;
66

77
use App\Enums\ExportFormat;
8+
use App\Enums\TimeEntryRoundingType;
89
use App\Models\Member;
910
use App\Models\Organization;
1011
use App\Models\Project;
@@ -133,6 +134,18 @@ public function rules(): array
133134
'string',
134135
'in:true,false',
135136
],
137+
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
138+
'rounding_type' => [
139+
'nullable',
140+
'string',
141+
Rule::enum(TimeEntryRoundingType::class),
142+
],
143+
// Defines the length of the interval that the time entry rounding rounds to.
144+
'rounding_minutes' => [
145+
'nullable',
146+
'numeric',
147+
'integer',
148+
],
136149
];
137150
}
138151

@@ -170,4 +183,22 @@ public function getFormatValue(): ExportFormat
170183
{
171184
return ExportFormat::from($this->validated('format'));
172185
}
186+
187+
public function getRoundingType(): ?TimeEntryRoundingType
188+
{
189+
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
190+
return null;
191+
}
192+
193+
return TimeEntryRoundingType::from($this->validated('rounding_type'));
194+
}
195+
196+
public function getRoundingMinutes(): ?int
197+
{
198+
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
199+
return null;
200+
}
201+
202+
return (int) $this->validated('rounding_minutes');
203+
}
173204
}

app/Http/Requests/V1/TimeEntry/TimeEntryIndexRequest.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
namespace App\Http\Requests\V1\TimeEntry;
66

7+
use App\Enums\TimeEntryRoundingType;
78
use App\Http\Requests\V1\BaseFormRequest;
89
use App\Models\Client;
910
use App\Models\Member;
1011
use App\Models\Organization;
1112
use App\Models\Project;
1213
use App\Models\Tag;
1314
use App\Models\Task;
15+
use Illuminate\Contracts\Validation\Rule as RuleContract;
1416
use Illuminate\Contracts\Validation\ValidationRule;
1517
use Illuminate\Database\Eloquent\Builder;
18+
use Illuminate\Foundation\Http\FormRequest;
19+
use Illuminate\Validation\Rule;
1620
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
1721

1822
/**
@@ -23,7 +27,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
2327
/**
2428
* Get the validation rules that apply to the request.
2529
*
26-
* @return array<string, array<string|ValidationRule>>
30+
* @return array<string, array<string|ValidationRule|RuleContract>>
2731
*/
2832
public function rules(): array
2933
{
@@ -136,6 +140,18 @@ public function rules(): array
136140
'string',
137141
'in:true,false',
138142
],
143+
// Rounding type defined where the end of each time entry should be rounded to. For example: nearest rounds the end to the nearest x minutes group. Rounding per time entry is activated if `rounding_type` and `rounding_minutes` is not null.
144+
'rounding_type' => [
145+
'nullable',
146+
'string',
147+
Rule::enum(TimeEntryRoundingType::class),
148+
],
149+
// Defines the length of the interval that the time entry rounding rounds to.
150+
'rounding_minutes' => [
151+
'nullable',
152+
'numeric',
153+
'integer',
154+
],
139155
];
140156
}
141157

@@ -153,4 +169,22 @@ public function getOffset(): int
153169
{
154170
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
155171
}
172+
173+
public function getRoundingType(): ?TimeEntryRoundingType
174+
{
175+
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
176+
return null;
177+
}
178+
179+
return TimeEntryRoundingType::from($this->validated('rounding_type'));
180+
}
181+
182+
public function getRoundingMinutes(): ?int
183+
{
184+
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
185+
return null;
186+
}
187+
188+
return (int) $this->validated('rounding_minutes');
189+
}
156190
}

0 commit comments

Comments
 (0)