mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-14 11:12:16 +01:00
Add report exports
This commit is contained in:
committed by
Constantin Graf
parent
e54df74d5d
commit
64535ceea6
35
app/Enums/ExportFormat.php
Normal file
35
app/Enums/ExportFormat.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Maatwebsite\Excel\Excel;
|
||||
|
||||
enum ExportFormat: string
|
||||
{
|
||||
case CSV = 'csv';
|
||||
case PDF = 'pdf';
|
||||
case XLSX = 'xlsx';
|
||||
case ODS = 'ods';
|
||||
|
||||
public function getFileExtension(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::CSV => 'csv',
|
||||
self::PDF => 'pdf',
|
||||
self::XLSX => 'xlsx',
|
||||
self::ODS => 'ods',
|
||||
};
|
||||
}
|
||||
|
||||
public function getExportPackageType(): string
|
||||
{
|
||||
return match ($this) {
|
||||
self::CSV => Excel::CSV,
|
||||
self::PDF => Excel::MPDF,
|
||||
self::XLSX => Excel::XLSX,
|
||||
self::ODS => Excel::ODS,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryDestroyMultipleRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexExportRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryStoreRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryUpdateMultipleRequest;
|
||||
@@ -21,15 +23,20 @@ use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Task;
|
||||
use App\Models\TimeEntry;
|
||||
use App\Service\ReportExport\TimeEntriesDetailedCsvExport;
|
||||
use App\Service\ReportExport\TimeEntriesDetailedExport;
|
||||
use App\Service\TimeEntryAggregationService;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use App\Service\TimezoneService;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
|
||||
class TimeEntryController extends Controller
|
||||
{
|
||||
@@ -63,21 +70,7 @@ class TimeEntryController extends Controller
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
}
|
||||
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->orderBy('start', 'desc');
|
||||
|
||||
$filter = new TimeEntryFilter($timeEntriesQuery);
|
||||
$filter->addStartFilter($request->input('start'));
|
||||
$filter->addEndFilter($request->input('end'));
|
||||
$filter->addActiveFilter($request->input('active'));
|
||||
$filter->addMemberIdFilter($member);
|
||||
$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'));
|
||||
$filter->addBillableFilter($request->input('billable'));
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||
|
||||
$totalCount = $timeEntriesQuery->count();
|
||||
|
||||
@@ -128,6 +121,76 @@ class TimeEntryController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<TimeEntry>
|
||||
*/
|
||||
private function getTimeEntriesQuery(Organization $organization, TimeEntryIndexRequest|TimeEntryIndexExportRequest $request, ?Member $member): Builder
|
||||
{
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization')
|
||||
->orderBy('start', 'desc');
|
||||
|
||||
$filter = new TimeEntryFilter($timeEntriesQuery);
|
||||
$filter->addStartFilter($request->input('start'));
|
||||
$filter->addEndFilter($request->input('end'));
|
||||
$filter->addActiveFilter($request->input('active'));
|
||||
$filter->addMemberIdFilter($member);
|
||||
$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'));
|
||||
$filter->addBillableFilter($request->input('billable'));
|
||||
|
||||
return $filter->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function indexExport(Organization $organization, TimeEntryIndexExportRequest $request): JsonResponse
|
||||
{
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
if ($member !== null && $member->user_id === Auth::id()) {
|
||||
$this->checkPermission($organization, 'time-entries:view:own');
|
||||
} else {
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
}
|
||||
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||
$timeEntriesQuery->with([
|
||||
'task',
|
||||
'project' => [
|
||||
'client',
|
||||
],
|
||||
'user',
|
||||
'tagsRelation',
|
||||
]);
|
||||
$format = $request->getFormatValue();
|
||||
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||
$path = 'exports/'.$filename;
|
||||
if ($format === ExportFormat::CSV) {
|
||||
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $filename, $timeEntriesQuery, 1000);
|
||||
$export->export();
|
||||
} else {
|
||||
Excel::store(
|
||||
new TimeEntriesDetailedExport($timeEntriesQuery),
|
||||
$path,
|
||||
config('filesystems.private'),
|
||||
$format->getExportPackageType(),
|
||||
[
|
||||
'visibility' => 'private',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'download_url' => Storage::disk(config('filesystems.private'))
|
||||
->temporaryUrl($path, now()->addMinutes(5)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get aggregated time entries in organization
|
||||
*
|
||||
|
||||
143
app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php
Normal file
143
app/Http/Requests/V1/TimeEntry/TimeEntryIndexExportRequest.php
Normal file
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
* @property Organization $organization
|
||||
*/
|
||||
class TimeEntryIndexExportRequest extends TimeEntryIndexRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array<string, array<string|ValidationRule|\Illuminate\Contracts\Validation\Rule>>
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'format' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::enum(ExportFormat::class),
|
||||
],
|
||||
// Filter by member ID
|
||||
'member_id' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by multiple member IDs, member IDs are OR combined, but AND combined with the member_id parameter
|
||||
'member_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'member_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by project IDs, project IDs are OR combined
|
||||
'project_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'project_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Project::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Project> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by tag IDs, tag IDs are AND combined
|
||||
'tag_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'tag_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Tag::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Tag> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter by task IDs, task IDs are OR combined
|
||||
'task_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'task_ids.*' => [
|
||||
'string',
|
||||
'uuid',
|
||||
new ExistsEloquent(Task::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Task> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
}),
|
||||
],
|
||||
// Filter only time entries that have a start date after the given timestamp in UTC (example: 2021-01-01T00:00:00Z)
|
||||
'start' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
'before:end',
|
||||
],
|
||||
// Filter only time entries that have a start date before the given timestamp in UTC (example: 2021-01-01T00:00:00Z)
|
||||
'end' => [
|
||||
'nullable',
|
||||
'string',
|
||||
'date_format:Y-m-d\TH:i:s\Z',
|
||||
],
|
||||
// Filter by active status (active means has no end date, is still running)
|
||||
'active' => [
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// Filter by billable status
|
||||
'billable' => [
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
// Limit the number of returned time entries (default: 150)
|
||||
'limit' => [
|
||||
'integer',
|
||||
'min:1',
|
||||
'max:500',
|
||||
],
|
||||
// Filter makes sure that only time entries of a whole date are returned
|
||||
'only_full_dates' => [
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getOnlyFullDates(): bool
|
||||
{
|
||||
return $this->input('only_full_dates', 'false') === 'true';
|
||||
}
|
||||
|
||||
public function getFormatValue(): ExportFormat
|
||||
{
|
||||
return ExportFormat::from($this->validated('format'));
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Carbon;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
use Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
|
||||
use Staudenmeir\EloquentJsonRelations\Relations\HasManyJson;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
@@ -30,6 +32,7 @@ class Tag extends Model implements AuditableContract
|
||||
/** @use HasFactory<TagFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasJsonRelationships;
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
@@ -48,4 +51,14 @@ class Tag extends Model implements AuditableContract
|
||||
{
|
||||
return $this->belongsTo(Organization::class, 'organization_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: This relation based on a JSON column. Please make sure that there are no performance issues, before using it.
|
||||
*
|
||||
* @return HasManyJson<TimeEntry, $this>
|
||||
*/
|
||||
public function timeEntries(): HasManyJson
|
||||
{
|
||||
return $this->hasManyJson(TimeEntry::class, 'tags');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Service\BillableRateService;
|
||||
use Carbon\CarbonInterval;
|
||||
use Database\Factories\TimeEntryFactory;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -17,6 +18,8 @@ use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Korridor\LaravelComputedAttributes\ComputedAttributes;
|
||||
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
use Staudenmeir\EloquentJsonRelations\HasJsonRelationships;
|
||||
use Staudenmeir\EloquentJsonRelations\Relations\BelongsToJson;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
@@ -42,6 +45,7 @@ use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
|
||||
* @property-read Client|null $client
|
||||
* @property string|null $task_id
|
||||
* @property-read Task|null $task
|
||||
* @property-read Collection<Tag> $tagsRelation
|
||||
*
|
||||
* @method Builder<TimeEntry> hasTag(Tag $tag)
|
||||
* @method static TimeEntryFactory factory()
|
||||
@@ -50,10 +54,11 @@ class TimeEntry extends Model implements AuditableContract
|
||||
{
|
||||
use ComputedAttributes;
|
||||
use CustomAuditable;
|
||||
|
||||
/** @use HasFactory<TimeEntryFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasJsonRelationships;
|
||||
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
@@ -197,4 +202,14 @@ class TimeEntry extends Model implements AuditableContract
|
||||
{
|
||||
return $this->belongsTo(Client::class, 'client_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning: This relation based on a JSON column. Please make sure that there are no performance issues, before using it.
|
||||
*
|
||||
* @return BelongsToJson<Tag, $this>
|
||||
*/
|
||||
public function tagsRelation(): BelongsToJson
|
||||
{
|
||||
return $this->belongsToJson(Tag::class, 'tags');
|
||||
}
|
||||
}
|
||||
|
||||
99
app/Service/ReportExport/CsvExport.php
Normal file
99
app/Service/ReportExport/CsvExport.php
Normal file
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\ReportExport;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Csv\Writer;
|
||||
|
||||
/**
|
||||
* @template T of Model
|
||||
*/
|
||||
abstract class CsvExport
|
||||
{
|
||||
private string $disk;
|
||||
|
||||
private string $filename;
|
||||
|
||||
private int $chunk;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public const array HEADER = [];
|
||||
|
||||
/**
|
||||
* @var Builder<T>
|
||||
*/
|
||||
private Builder $builder;
|
||||
|
||||
/**
|
||||
* @param Builder<T> $builder
|
||||
*/
|
||||
public function __construct(string $disk, string $filename, Builder $builder, int $chunk)
|
||||
{
|
||||
|
||||
$this->disk = $disk;
|
||||
$this->filename = $filename;
|
||||
$this->chunk = $chunk;
|
||||
$this->builder = $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param T $model
|
||||
* @return array<string, string|Carbon|null>
|
||||
*/
|
||||
abstract public function mapRow(Model $model): array;
|
||||
|
||||
public function export(): void
|
||||
{
|
||||
$writer = Writer::createFromPath(Storage::disk($this->disk)->path($this->filename), 'w+');
|
||||
$writer->insertOne(static::HEADER);
|
||||
|
||||
$this->builder->chunk($this->chunk, function ($models) use ($writer): void {
|
||||
foreach ($models as $model) {
|
||||
$data = $this->mapRow($model);
|
||||
$row = $this->convertRow($data);
|
||||
$this->validateRow($row);
|
||||
|
||||
$writer->insertOne(array_values($row));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string|Carbon|null> $data
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function convertRow(array $data): array
|
||||
{
|
||||
$convertedRow = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if ($value instanceof Carbon) {
|
||||
$convertedRow[$key] = $value->toIso8601String();
|
||||
} elseif ($value === null) {
|
||||
$convertedRow[$key] = '';
|
||||
} else {
|
||||
$convertedRow[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $convertedRow;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $row
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function validateRow(array $row): void
|
||||
{
|
||||
if (array_keys($row) !== self::HEADER) {
|
||||
throw new \Exception('Invalid row');
|
||||
}
|
||||
}
|
||||
}
|
||||
46
app/Service/ReportExport/TimeEntriesDetailedCsvExport.php
Normal file
46
app/Service/ReportExport/TimeEntriesDetailedCsvExport.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\ReportExport;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* @extends CsvExport<TimeEntry>
|
||||
*/
|
||||
class TimeEntriesDetailedCsvExport extends CsvExport
|
||||
{
|
||||
public const array HEADER = [
|
||||
'id',
|
||||
'user_id',
|
||||
'project_id',
|
||||
'task_id',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'duration',
|
||||
'description',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* @param TimeEntry $model
|
||||
*/
|
||||
public function mapRow(Model $model): array
|
||||
{
|
||||
return [
|
||||
'id' => $model->id,
|
||||
'user_id' => $model->user_id,
|
||||
'project_id' => $model->project_id,
|
||||
'task_id' => $model->task_id,
|
||||
'start_time' => $model->start,
|
||||
'end_time' => $model->end,
|
||||
'description' => $model->description,
|
||||
'created_at' => $model->created_at,
|
||||
'updated_at' => $model->updated_at,
|
||||
];
|
||||
}
|
||||
}
|
||||
101
app/Service/ReportExport/TimeEntriesDetailedExport.php
Normal file
101
app/Service/ReportExport/TimeEntriesDetailedExport.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service\ReportExport;
|
||||
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Maatwebsite\Excel\Concerns\Exportable;
|
||||
use Maatwebsite\Excel\Concerns\FromQuery;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomCsvSettings;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
|
||||
/**
|
||||
* @implements WithMapping<TimeEntry>
|
||||
*/
|
||||
class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, WithHeadings, WithMapping
|
||||
{
|
||||
use Exportable;
|
||||
|
||||
/**
|
||||
* @var Builder<TimeEntry>
|
||||
*/
|
||||
private Builder $builder;
|
||||
|
||||
/**
|
||||
* @param Builder<TimeEntry> $builder
|
||||
*/
|
||||
public function __construct(Builder $builder)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Builder<TimeEntry>
|
||||
*/
|
||||
public function query(): Builder
|
||||
{
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
*/
|
||||
public function getCsvSettings(): array
|
||||
{
|
||||
return [
|
||||
'delimiter' => ',',
|
||||
'use_bom' => false,
|
||||
'output_encoding' => 'ISO-8859-1',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'Description',
|
||||
'Task',
|
||||
'Project',
|
||||
'Client',
|
||||
'User',
|
||||
'Start date',
|
||||
'Start time',
|
||||
'End date',
|
||||
'End time',
|
||||
'Duration',
|
||||
'Duration (decimal)',
|
||||
'Billable',
|
||||
'Tags',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TimeEntry $model
|
||||
* @return array<int, string|float|null>
|
||||
*/
|
||||
public function map($model): array
|
||||
{
|
||||
$duration = $model->getDuration();
|
||||
|
||||
return [
|
||||
$model->description,
|
||||
$model->task?->name,
|
||||
$model->project?->name,
|
||||
$model->project?->client?->name,
|
||||
$model->user->name,
|
||||
$model->start->format('Y-m-d'),
|
||||
$model->start->format('H:i:s'),
|
||||
$model->end?->format('Y-m-d'),
|
||||
$model->end?->format('H:i:s'),
|
||||
$duration !== null ? (int) floor($duration->totalHours).':'.$duration->format('%I:%S') : null,
|
||||
$duration?->totalHours,
|
||||
$model->billable ? 'Yes' : 'No',
|
||||
$model->tagsRelation->pluck('name')->implode(', '),
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user