mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-11 09:42:15 +01:00
Add rounding feature
This commit is contained in:
@@ -73,7 +73,9 @@ class ReportController extends Controller
|
||||
false,
|
||||
$report->properties->start,
|
||||
$report->properties->end,
|
||||
true
|
||||
true,
|
||||
$report->properties->roundingType,
|
||||
$report->properties->roundingMinutes,
|
||||
);
|
||||
$historyData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
||||
$timeEntriesQuery->clone(),
|
||||
@@ -84,7 +86,9 @@ class ReportController extends Controller
|
||||
true,
|
||||
$report->properties->start,
|
||||
$report->properties->end,
|
||||
true
|
||||
true,
|
||||
$report->properties->roundingType,
|
||||
$report->properties->roundingMinutes,
|
||||
);
|
||||
|
||||
return new DetailedWithDataReportResource($report, $data, $historyData);
|
||||
|
||||
@@ -33,6 +33,7 @@ use App\Service\ReportExport\TimeEntriesDetailedExport;
|
||||
use App\Service\ReportExport\TimeEntriesReportExport;
|
||||
use App\Service\TimeEntryAggregationService;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use App\Service\TimeEntryService;
|
||||
use App\Service\TimezoneService;
|
||||
use Gotenberg\Exceptions\GotenbergApiErrored;
|
||||
use Gotenberg\Exceptions\NoOutputFileInResponse;
|
||||
@@ -47,6 +48,7 @@ use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
@@ -140,8 +142,15 @@ class TimeEntryController extends Controller
|
||||
*/
|
||||
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
||||
{
|
||||
$select = TimeEntry::SELECT_COLUMNS;
|
||||
if ($request->getRoundingType() !== null && $request->getRoundingMinutes() !== null) {
|
||||
$select = array_diff($select, ['start', 'end']);
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getStartSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as start');
|
||||
$select[] = DB::raw(app(TimeEntryService::class)->getEndSelectRawForRounding($request->getRoundingType(), $request->getRoundingMinutes()).' as end');
|
||||
}
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->select($select)
|
||||
->orderBy('start', 'desc');
|
||||
|
||||
$filter = new TimeEntryFilter($timeEntriesQuery);
|
||||
@@ -183,6 +192,8 @@ class TimeEntryController extends Controller
|
||||
$user = $this->user();
|
||||
$timezone = $user->timezone;
|
||||
$showBillableRate = $this->member($organization)->role !== Role::Employee->value || $organization->employees_can_see_billable_rates;
|
||||
$roundingType = $request->getRoundingType();
|
||||
$roundingMinutes = $request->getRoundingMinutes();
|
||||
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||
$timeEntriesQuery->with([
|
||||
@@ -207,8 +218,9 @@ class TimeEntryController extends Controller
|
||||
if ($viewFile === false) {
|
||||
throw new \LogicException('View file not found');
|
||||
}
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||
$timeEntriesQuery->clone()->reorder()->withOnly([]),
|
||||
$timeEntriesAggregateQuery,
|
||||
null,
|
||||
null,
|
||||
$user->timezone,
|
||||
@@ -216,7 +228,9 @@ class TimeEntryController extends Controller
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
$showBillableRate
|
||||
$showBillableRate,
|
||||
$roundingType,
|
||||
$roundingMinutes,
|
||||
);
|
||||
$html = Blade::render($viewFile, [
|
||||
'timeEntries' => $timeEntriesQuery->get(),
|
||||
@@ -324,6 +338,8 @@ class TimeEntryController extends Controller
|
||||
$group1Type = $request->getGroup();
|
||||
$group2Type = $request->getSubGroup();
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$roundingType = $request->getRoundingType();
|
||||
$roundingMinutes = $request->getRoundingMinutes();
|
||||
|
||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||
$timeEntriesAggregateQuery,
|
||||
@@ -334,7 +350,9 @@ class TimeEntryController extends Controller
|
||||
$request->getFillGapsInTimeGroups(),
|
||||
$request->getStart(),
|
||||
$request->getEnd(),
|
||||
$showBillableRate
|
||||
$showBillableRate,
|
||||
$roundingType,
|
||||
$roundingMinutes
|
||||
);
|
||||
|
||||
return [
|
||||
@@ -373,6 +391,8 @@ class TimeEntryController extends Controller
|
||||
$group = $request->getGroup();
|
||||
$subGroup = $request->getSubGroup();
|
||||
$timeEntriesAggregateQuery = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
$roundingType = $request->getRoundingType();
|
||||
$roundingMinutes = $request->getRoundingMinutes();
|
||||
|
||||
$aggregatedData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
|
||||
$timeEntriesAggregateQuery->clone(),
|
||||
@@ -383,7 +403,9 @@ class TimeEntryController extends Controller
|
||||
false,
|
||||
$request->getStart(),
|
||||
$request->getEnd(),
|
||||
$showBillableRate
|
||||
$showBillableRate,
|
||||
$roundingType,
|
||||
$roundingMinutes
|
||||
);
|
||||
$dataHistoryChart = $timeEntryAggregationService->getAggregatedTimeEntries(
|
||||
$timeEntriesAggregateQuery->clone(),
|
||||
@@ -394,7 +416,9 @@ class TimeEntryController extends Controller
|
||||
true,
|
||||
$request->getStart(),
|
||||
$request->getEnd(),
|
||||
$showBillableRate
|
||||
$showBillableRate,
|
||||
$roundingType,
|
||||
$roundingMinutes
|
||||
);
|
||||
$currency = $organization->currency;
|
||||
$timezone = app(TimezoneService::class)->getTimezoneFromUser($this->user());
|
||||
@@ -477,7 +501,7 @@ class TimeEntryController extends Controller
|
||||
/**
|
||||
* @return Builder<TimeEntry>
|
||||
*/
|
||||
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest $request, ?Member $member): Builder
|
||||
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
||||
{
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization');
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace App\Http\Requests\V1\TimeEntry;
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Enums\TimeEntryAggregationType;
|
||||
use App\Enums\TimeEntryAggregationTypeInterval;
|
||||
use App\Enums\TimeEntryRoundingType;
|
||||
use App\Http\Requests\V1\BaseFormRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
@@ -164,6 +165,18 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// 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.
|
||||
'rounding_type' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::enum(TimeEntryRoundingType::class),
|
||||
],
|
||||
// Defines the length of the interval that the time entry rounding rounds to.
|
||||
'rounding_minutes' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'integer',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -211,4 +224,22 @@ class TimeEntryAggregateExportRequest extends BaseFormRequest
|
||||
{
|
||||
return ExportFormat::from($this->validated('format'));
|
||||
}
|
||||
|
||||
public function getRoundingType(): ?TimeEntryRoundingType
|
||||
{
|
||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||
}
|
||||
|
||||
public function getRoundingMinutes(): ?int
|
||||
{
|
||||
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $this->validated('rounding_minutes');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Enums\TimeEntryAggregationType;
|
||||
use App\Enums\TimeEntryRoundingType;
|
||||
use App\Http\Requests\V1\BaseFormRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
@@ -146,6 +147,18 @@ class TimeEntryAggregateRequest extends BaseFormRequest
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// 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.
|
||||
'rounding_type' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::enum(TimeEntryRoundingType::class),
|
||||
],
|
||||
// Defines the length of the interval that the time entry rounding rounds to.
|
||||
'rounding_minutes' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'integer',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -173,4 +186,22 @@ class TimeEntryAggregateRequest extends BaseFormRequest
|
||||
{
|
||||
return $this->input('end') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC') : null;
|
||||
}
|
||||
|
||||
public function getRoundingType(): ?TimeEntryRoundingType
|
||||
{
|
||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||
}
|
||||
|
||||
public function getRoundingMinutes(): ?int
|
||||
{
|
||||
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $this->validated('rounding_minutes');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Enums\TimeEntryRoundingType;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
@@ -133,6 +134,18 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// 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.
|
||||
'rounding_type' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::enum(TimeEntryRoundingType::class),
|
||||
],
|
||||
// Defines the length of the interval that the time entry rounding rounds to.
|
||||
'rounding_minutes' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'integer',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -170,4 +183,22 @@ class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
||||
{
|
||||
return ExportFormat::from($this->validated('format'));
|
||||
}
|
||||
|
||||
public function getRoundingType(): ?TimeEntryRoundingType
|
||||
{
|
||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||
}
|
||||
|
||||
public function getRoundingMinutes(): ?int
|
||||
{
|
||||
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $this->validated('rounding_minutes');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Enums\TimeEntryRoundingType;
|
||||
use App\Http\Requests\V1\BaseFormRequest;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
@@ -11,8 +12,11 @@ use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Contracts\Validation\Rule as RuleContract;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
@@ -23,7 +27,7 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule>>
|
||||
* @return array<string, array<string|ValidationRule|RuleContract>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
@@ -136,6 +140,18 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// 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.
|
||||
'rounding_type' => [
|
||||
'nullable',
|
||||
'string',
|
||||
Rule::enum(TimeEntryRoundingType::class),
|
||||
],
|
||||
// Defines the length of the interval that the time entry rounding rounds to.
|
||||
'rounding_minutes' => [
|
||||
'nullable',
|
||||
'numeric',
|
||||
'integer',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -153,4 +169,22 @@ class TimeEntryIndexRequest extends BaseFormRequest
|
||||
{
|
||||
return $this->has('offset') ? (int) $this->validated('offset', 0) : 0;
|
||||
}
|
||||
|
||||
public function getRoundingType(): ?TimeEntryRoundingType
|
||||
{
|
||||
if (! $this->has('rounding_type') || $this->validated('rounding_type') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TimeEntryRoundingType::from($this->validated('rounding_type'));
|
||||
}
|
||||
|
||||
public function getRoundingMinutes(): ?int
|
||||
{
|
||||
if (! $this->has('rounding_minutes') || $this->validated('rounding_minutes') === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (int) $this->validated('rounding_minutes');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user