Skip to content

Commit

Permalink
Added shareable reports
Browse files Browse the repository at this point in the history
  • Loading branch information
korridor committed Nov 8, 2024
1 parent 070246f commit 5e8510a
Show file tree
Hide file tree
Showing 22 changed files with 511 additions and 56 deletions.
3 changes: 3 additions & 0 deletions app/Enums/Weekday.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace App\Enums;

use Datomatic\LaravelEnumHelper\LaravelEnumHelper;
use Illuminate\Support\Carbon;

enum Weekday: string
{
use LaravelEnumHelper;

case Monday = 'monday';
case Tuesday = 'tuesday';
case Wednesday = 'wednesday';
Expand Down
4 changes: 4 additions & 0 deletions app/Http/Controllers/Api/V1/ProjectController.php
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public function store(Organization $organization, ProjectStoreRequest $request):
$project->is_billable = (bool) $request->input('is_billable');
$project->billable_rate = $request->getBillableRate();
$project->client_id = $request->input('client_id');
$project->is_public = $request->getIsPublic();
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$project->estimated_time = $request->getEstimatedTime();
}
Expand All @@ -127,6 +128,9 @@ public function update(Organization $organization, Project $project, ProjectUpda
if ($request->has('is_archived')) {
$project->archived_at = $request->getIsArchived() ? Carbon::now() : null;
}
if ($request->has('is_public')) {
$project->is_public = $request->boolean('is_public');

Check warning on line 132 in app/Http/Controllers/Api/V1/ProjectController.php

View check run for this annotation

Codecov / codecov/patch

app/Http/Controllers/Api/V1/ProjectController.php#L132

Added line #L132 was not covered by tests
}
if ($this->canAccessPremiumFeatures($organization) && $request->has('estimated_time')) {
$project->estimated_time = $request->getEstimatedTime();
}
Expand Down
50 changes: 47 additions & 3 deletions app/Http/Controllers/Api/V1/Public/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

namespace App\Http\Controllers\Api\V1\Public;

use App\Enums\TimeEntryAggregationType;
use App\Http\Controllers\Api\V1\Controller;
use App\Http\Resources\V1\Report\DetailedReportResource;
use App\Http\Resources\V1\Report\DetailedWithDataReportResource;
use App\Models\Report;
use App\Models\TimeEntry;
use App\Service\TimeEntryAggregationService;
use App\Service\TimeEntryFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
Expand All @@ -22,14 +26,17 @@ class ReportController extends Controller
*
* @operationId getPublicReport
*/
public function show(Request $request): DetailedReportResource
public function show(Request $request, TimeEntryAggregationService $timeEntryAggregationService): DetailedWithDataReportResource
{
$shareSecret = $request->header('X-Api-Key');
if (! is_string($shareSecret)) {
throw new ModelNotFoundException;
}

$report = Report::query()
->with([
'organization',
])
->where('share_secret', '=', $shareSecret)
->where('is_public', '=', true)
->where(function (Builder $builder): void {
Expand All @@ -38,7 +45,44 @@ public function show(Request $request): DetailedReportResource
->orWhere('public_until', '>', now());
})
->firstOrFail();
$properties = $report->properties;

return new DetailedReportResource($report);
$timeEntriesQuery = TimeEntry::query()
->whereBelongsTo($report->organization, 'organization');

$filter = new TimeEntryFilter($timeEntriesQuery);
$filter->addStart($properties->start);
$filter->addEnd($properties->end);
$filter->addActive($properties->active);
$filter->addBillable($properties->billable);
$filter->addMemberIdsFilter($request->input('member_ids'));
$filter->addProjectIdsFilter($request->input('project_ids'));
$filter->addTagIdsFilter($request->input('tag_ids'));
$filter->addTaskIdsFilter($request->input('task_ids'));
$filter->addClientIdsFilter($request->input('client_ids'));
$timeEntriesQuery = $filter->get();

$data = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
$timeEntriesQuery->clone(),
$report->properties->group,
$report->properties->subGroup,
$report->properties->timezone,
$report->properties->weekStart,
false,
$report->properties->start,
$report->properties->end,
);
$historyData = $timeEntryAggregationService->getAggregatedTimeEntriesWithDescriptions(
$timeEntriesQuery->clone(),
TimeEntryAggregationType::fromInterval($report->properties->historyGroup),
null,
$report->properties->timezone,
$report->properties->weekStart,
true,
$report->properties->start,
$report->properties->end,
);

return new DetailedWithDataReportResource($report, $data, $historyData);
}
}
30 changes: 29 additions & 1 deletion app/Http/Controllers/Api/V1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
namespace App\Http\Controllers\Api\V1;

use App\Enums\TimeEntryAggregationType;
use App\Enums\TimeEntryAggregationTypeInterval;
use App\Enums\Weekday;
use App\Http\Requests\V1\Report\ReportStoreRequest;
use App\Http\Requests\V1\Report\ReportUpdateRequest;
use App\Http\Resources\V1\Report\DetailedReportResource;
use App\Http\Resources\V1\Report\ReportCollection;
use App\Http\Resources\V1\Report\ReportResource;
use App\Models\Organization;
use App\Models\Report;
use App\Service\Dto\ReportPropertiesDto;
use App\Service\ReportService;
use App\Service\TimezoneService;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Http\JsonResponse;

Expand All @@ -32,6 +36,8 @@ protected function checkPermission(Organization $organization, string $permissio
/**
* Get reports
*
* @return ReportCollection<ReportResource>
*
* @throws AuthorizationException
*
* @operationId getReports
Expand Down Expand Up @@ -69,9 +75,10 @@ public function show(Organization $organization, Report $report): DetailedReport
*
* @operationId createReport
*/
public function store(Organization $organization, ReportStoreRequest $request): DetailedReportResource
public function store(Organization $organization, ReportStoreRequest $request, TimezoneService $timezoneService): DetailedReportResource
{
$this->checkPermission($organization, 'reports:create');
$user = $this->user();

$report = new Report;
$report->name = $request->getName();
Expand All @@ -81,6 +88,27 @@ public function store(Organization $organization, ReportStoreRequest $request):
$properties = new ReportPropertiesDto;
$properties->group = TimeEntryAggregationType::from($request->input('properties.group'));
$properties->subGroup = TimeEntryAggregationType::from($request->input('properties.sub_group'));
$properties->historyGroup = TimeEntryAggregationTypeInterval::from($request->input('properties.history_group'));
$properties->start = $request->getPropertyStart();
$properties->end = $request->getPropertyEnd();
$properties->active = $request->boolean('properties.active', null);

Check failure on line 94 in app/Http/Controllers/Api/V1/ReportController.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #2 $default of method Illuminate\Http\Request::boolean() expects bool, null given.
$properties->setMemberIds($request->input('properties.member_ids', null));
$properties->billable = $request->boolean('properties.billable', null);

Check failure on line 96 in app/Http/Controllers/Api/V1/ReportController.php

View workflow job for this annotation

GitHub Actions / phpstan

Parameter #2 $default of method Illuminate\Http\Request::boolean() expects bool, null given.
$properties->setClientIds($request->input('properties.client_ids', null));
$properties->setProjectIds($request->input('properties.project_ids', null));
$properties->setTagIds($request->input('properties.tag_ids', null));
$properties->setTaskIds($request->input('properties.task_ids', null));
$properties->weekStart = $request->has('properties.week_start') ? Weekday::from($request->input('properties.week_start')) : $user->week_start;
$timezone = $user->timezone;
if ($request->has('properties.timezone')) {
if ($timezoneService->isValid($request->input('properties.timezone'))) {
$timezone = $request->input('properties.timezone');
}
if ($timezoneService->mapLegacyTimezone($request->input('properties.timezone')) !== null) {
$timezone = $timezoneService->mapLegacyTimezone($request->input('properties.timezone'));

Check warning on line 108 in app/Http/Controllers/Api/V1/ReportController.php

View check run for this annotation

Codecov / codecov/patch

app/Http/Controllers/Api/V1/ReportController.php#L108

Added line #L108 was not covered by tests
}
}
$properties->timezone = $timezone;
$report->properties = $properties;
if ($isPublic) {
$report->share_secret = app(ReportService::class)->generateSecret();
Expand Down
9 changes: 9 additions & 0 deletions app/Http/Requests/V1/Project/ProjectStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,18 @@ public function rules(): array
'min:0',
'max:2147483647',
],
// Whether the project is public
'is_public' => [
'boolean',
],
];
}

public function getIsPublic(): bool
{
return $this->has('is_public') && $this->boolean('is_public');
}

public function getBillableRate(): ?int
{
$input = $this->input('billable_rate');
Expand Down
3 changes: 3 additions & 0 deletions app/Http/Requests/V1/Project/ProjectUpdateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ public function rules(): array
'is_archived' => [
'boolean',
],
'is_public' => [
'boolean',
],
'client_id' => [
'nullable',
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
Expand Down
31 changes: 30 additions & 1 deletion app/Http/Requests/V1/Report/ReportStoreRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
namespace App\Http\Requests\V1\Report;

use App\Enums\TimeEntryAggregationType;
use App\Enums\TimeEntryAggregationTypeInterval;
use App\Enums\Weekday;
use App\Models\Organization;
use Illuminate\Contracts\Validation\Rule as LegacyValidationRule;
use Illuminate\Contracts\Validation\ValidationRule;
Expand Down Expand Up @@ -80,6 +82,7 @@ public function rules(): array
'string',
'uuid',
],
// Filter by project IDs, project IDs are OR combined
'properties.project_ids' => [
'nullable',
'array',
Expand All @@ -88,6 +91,7 @@ public function rules(): array
'string',
'uuid',
],
// Filter by tag IDs, tag IDs are OR combined
'properties.tag_ids' => [
'nullable',
'array',
Expand All @@ -108,11 +112,22 @@ public function rules(): array
'required',
Rule::enum(TimeEntryAggregationType::class),
],

'properties.sub_group' => [
'required',
Rule::enum(TimeEntryAggregationType::class),
],
'properties.history_group' => [
'required',
Rule::enum(TimeEntryAggregationTypeInterval::class),
],
'properties.week_start' => [
'nullable',
Rule::enum(Weekday::class),
],
'properties.timezone' => [
'nullable',
'timezone:all',
],
];
}

Expand All @@ -137,4 +152,18 @@ public function getPublicUntil(): ?Carbon

return $publicUntil === null ? null : Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $publicUntil);
}

public function getPropertyStart(): ?Carbon
{
$start = $this->input('properties.start');

return $start === null ? null : Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $start);
}

public function getPropertyEnd(): ?Carbon
{
$end = $this->input('properties.end');

return $end === null ? null : Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $end);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,23 @@ class TimeEntryAggregateExportRequest extends FormRequest
public function rules(): array
{
return [
// Data format of the export
'format' => [
'required',
'string',
Rule::enum(ExportFormat::class),
],
// Type of first grouping
'group' => [
'required',
Rule::enum(TimeEntryAggregationType::class),
],

// Type of second grouping
'sub_group' => [
'required',
Rule::enum(TimeEntryAggregationType::class),
],

// Type of grouping of the historic aggregation (time chart)
'history_group' => [
'required',
'nullable',
Expand Down
3 changes: 2 additions & 1 deletion app/Http/Requests/V1/TimeEntry/TimeEntryAggregateRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ class TimeEntryAggregateRequest extends FormRequest
public function rules(): array
{
return [
// Type of first grouping
'group' => [
'nullable',
'required_with:sub_group',
Rule::enum(TimeEntryAggregationType::class),
],

// Type of second grouping
'sub_group' => [
'nullable',
Rule::enum(TimeEntryAggregationType::class),
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Resources/V1/Member/PersonalMembershipResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public function toArray(Request $request): array
'id' => $this->resource->organization->id,
/** @var string $name Name of organization */
'name' => $this->resource->organization->name,
/** @var string $currency Currency code (ISO 4217) of organization */
'currency' => $this->resource->organization->currency,
],
/** @var string $role Role */
'role' => $this->resource->role,
Expand Down
2 changes: 2 additions & 0 deletions app/Http/Resources/V1/Organization/OrganizationResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ public function toArray(Request $request): array
'billable_rate' => $this->showBillableRate ? $this->resource->billable_rate : null,
/** @var bool $employees_can_see_billable_rates Can members of the organization with role "employee" see the billable rates */
'employees_can_see_billable_rates' => $this->resource->employees_can_see_billable_rates,
/** @var string $currency Currency code (ISO 4217) */
'currency' => $this->resource->currency,
];
}
}
2 changes: 2 additions & 0 deletions app/Http/Resources/V1/Project/ProjectResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ public function toArray(Request $request): array
'estimated_time' => $this->resource->estimated_time,
/** @var int $spent_time Spent time on this project in seconds (sum of the duration of all associated time entries, excl. still running time entries) */
'spent_time' => $this->resource->spent_time,
/** @var bool $is_public Whether the project is public */
'is_public' => $this->resource->is_public,
];
}
}
14 changes: 14 additions & 0 deletions app/Http/Resources/V1/Report/DetailedReportResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,35 @@ public function toArray(Request $request): array
/** @var string|null $shareable_link Get link to access the report externally, not set if the report is private */
'shareable_link' => $this->resource->getShareableLink(),
'properties' => [
/** @var string $group Type of first grouping */
'group' => $this->resource->properties->group->value,
/** @var string $sub_group Type of second grouping */
'sub_group' => $this->resource->properties->subGroup->value,
/** @var string $history_group Type of grouping of the historic aggregation (time chart) */
'history_group' => $this->resource->properties->historyGroup->value,
/** @var string|null $start Start date of the report */
'start' => $this->resource->properties->start?->toIso8601ZuluString(),
/** @var string|null $end End date of the report */
'end' => $this->resource->properties->end?->toIso8601ZuluString(),
/** @var bool|null $active Whether the report is active */
'active' => $this->resource->properties->active,
/** @var array<string> $member_ids Filter by multiple member IDs, member IDs are OR combined */
'member_ids' => $this->resource->properties->memberIds?->toArray(),
/** @var bool $billable Filter by billable status */
'billable' => $this->resource->properties->billable,
/** @var array<string> $client_ids Filter by client IDs, client IDs are OR combined */
'client_ids' => $this->resource->properties->clientIds?->toArray(),
/** @var array<string> $project_ids Filter by project IDs, project IDs are OR combined */
'project_ids' => $this->resource->properties->projectIds?->toArray(),
/** @var array<string> $tags_ids Filter by tag IDs, tag IDs are OR combined */
'tag_ids' => $this->resource->properties->tagIds?->toArray(),
/** @var array<string> $task_ids Filter by task IDs, task IDs are OR combined */
'task_ids' => $this->resource->properties->taskIds?->toArray(),
],
/** @var string $created_at Date when the report was created */
'created_at' => $this->resource->created_at?->toIso8601ZuluString(),
/** @var string $updated_at Date when the report was last updated */
'updated_at' => $this->resource->updated_at?->toIso8601ZuluString(),
];
}
}
Loading

0 comments on commit 5e8510a

Please sign in to comment.