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 15, 2024
1 parent 070246f commit e2ac8f6
Show file tree
Hide file tree
Showing 26 changed files with 986 additions and 107 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use Illuminate\Console\Command;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
use LogicException;

class ReportSetExpiredToPrivateCommand extends Command
{
Expand Down Expand Up @@ -44,7 +45,12 @@ public function handle(): int
->chunk(500, function (Collection $reports) use ($dryRun, &$resetReports): void {
/** @var Collection<int, Report> $reports */
foreach ($reports as $report) {
$this->info('Make report "'.$report->name.'" ('.$report->getKey().') private, expired: '.$report->public_until->toIso8601ZuluString().' ('.$report->public_until->diffForHumans().')');
$publicUntil = $report->public_until;
if ($publicUntil === null) {
throw new LogicException('public_until should not be null');

Check warning on line 50 in app/Console/Commands/Report/ReportSetExpiredToPrivateCommand.php

View check run for this annotation

Codecov / codecov/patch

app/Console/Commands/Report/ReportSetExpiredToPrivateCommand.php#L50

Added line #L50 was not covered by tests
}
$this->info('Make report "'.$report->name.'" ('.$report->getKey().') private, expired: '.
$publicUntil->toIso8601ZuluString().' ('.$publicUntil->diffForHumans().')');
$resetReports++;
if (! $dryRun) {
$report->is_public = false;
Expand Down
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
52 changes: 49 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,14 @@

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\Dto\ReportPropertiesDto;
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 +27,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 +46,45 @@ public function show(Request $request): DetailedReportResource
->orWhere('public_until', '>', now());
})
->firstOrFail();
/** @var ReportPropertiesDto $properties */
$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($properties->memberIds?->toArray());
$filter->addProjectIdsFilter($properties->projectIds?->toArray());
$filter->addTagIdsFilter($properties->tagIds?->toArray());
$filter->addTaskIdsFilter($properties->taskIds?->toArray());
$filter->addClientIdsFilter($properties->clientIds?->toArray());
$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);
}
}
40 changes: 33 additions & 7 deletions app/Http/Controllers/Api/V1/ReportController.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@

namespace App\Http\Controllers\Api\V1;

use App\Enums\TimeEntryAggregationType;
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 +34,8 @@ protected function checkPermission(Organization $organization, string $permissio
/**
* Get reports
*
* @return ReportCollection<ReportResource>
*
* @throws AuthorizationException
*
* @operationId getReports
Expand Down Expand Up @@ -69,21 +73,43 @@ 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, ReportService $reportService): DetailedReportResource
{
$this->checkPermission($organization, 'reports:create');
$user = $this->user();

$report = new Report;
$report->name = $request->getName();
$report->description = $request->getDescription();
$isPublic = $request->getIsPublic();
$report->is_public = $isPublic;
$properties = new ReportPropertiesDto;
$properties->group = TimeEntryAggregationType::from($request->input('properties.group'));
$properties->subGroup = TimeEntryAggregationType::from($request->input('properties.sub_group'));
$properties->group = $request->getPropertyGroup();
$properties->subGroup = $request->getPropertySubGroup();
$properties->historyGroup = $request->getPropertyHistoryGroup();
$properties->start = $request->getPropertyStart();
$properties->end = $request->getPropertyEnd();
$properties->active = $request->getPropertyActive();
$properties->setMemberIds($request->input('properties.member_ids', null));
$properties->billable = $request->getPropertyBillable();
$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 106 in app/Http/Controllers/Api/V1/ReportController.php

View check run for this annotation

Codecov / codecov/patch

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

Added line #L106 was not covered by tests
}
}
$properties->timezone = $timezone;
$report->properties = $properties;
if ($isPublic) {
$report->share_secret = app(ReportService::class)->generateSecret();
$report->share_secret = $reportService->generateSecret();
$report->public_until = $request->getPublicUntil();
} else {
$report->share_secret = null;
Expand All @@ -102,7 +128,7 @@ public function store(Organization $organization, ReportStoreRequest $request):
*
* @operationId updateReport
*/
public function update(Organization $organization, Report $report, ReportUpdateRequest $request): DetailedReportResource
public function update(Organization $organization, Report $report, ReportUpdateRequest $request, ReportService $reportService): DetailedReportResource
{
$this->checkPermission($organization, 'reports:update', $report);

Expand All @@ -116,7 +142,7 @@ public function update(Organization $organization, Report $report, ReportUpdateR
$isPublic = $request->getIsPublic();
$report->is_public = $isPublic;
if ($isPublic) {
$report->share_secret = app(ReportService::class)->generateSecret();
$report->share_secret = $reportService->generateSecret();
$report->public_until = $request->getPublicUntil();
} else {
$report->share_secret = null;
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
74 changes: 71 additions & 3 deletions 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 @@ -49,11 +51,11 @@ public function rules(): array
'array',
],
'properties.start' => [
'nullable',
'required',
'date_format:Y-m-d\TH:i:s\Z',
],
'properties.end' => [
'nullable',
'required',
'date_format:Y-m-d\TH:i:s\Z',
],
'properties.active' => [
Expand All @@ -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,57 @@ public function getPublicUntil(): ?Carbon

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

public function getPropertyStart(): Carbon
{
$start = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.start'));
if ($start === null) {
throw new \LogicException('Start date validation is not working');

Check warning on line 160 in app/Http/Requests/V1/Report/ReportStoreRequest.php

View check run for this annotation

Codecov / codecov/patch

app/Http/Requests/V1/Report/ReportStoreRequest.php#L160

Added line #L160 was not covered by tests
}

return $start;
}

public function getPropertyEnd(): Carbon
{
$end = Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('properties.end'));
if ($end === null) {
throw new \LogicException('End date validation is not working');

Check warning on line 170 in app/Http/Requests/V1/Report/ReportStoreRequest.php

View check run for this annotation

Codecov / codecov/patch

app/Http/Requests/V1/Report/ReportStoreRequest.php#L170

Added line #L170 was not covered by tests
}

return $end;
}

public function getPropertyActive(): ?bool
{
if ($this->has('properties.active') && $this->input('properties.active') !== null) {
return (bool) $this->input('properties.active');
}

return null;
}

public function getPropertyBillable(): ?bool
{
if ($this->has('properties.billable') && $this->input('properties.billable') !== null) {
return (bool) $this->input('properties.billable');
}

return null;
}

public function getPropertyGroup(): TimeEntryAggregationType
{
return TimeEntryAggregationType::from($this->input('properties.group'));
}

public function getPropertySubGroup(): TimeEntryAggregationType
{
return TimeEntryAggregationType::from($this->input('properties.sub_group'));
}

public function getPropertyHistoryGroup(): TimeEntryAggregationTypeInterval
{
return TimeEntryAggregationTypeInterval::from($this->input('properties.history_group'));
}
}
Loading

0 comments on commit e2ac8f6

Please sign in to comment.