mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-08-15 19:52:15 +01:00
Add pdf detailed report and placeholder for aggregate endpoint
This commit is contained in:
committed by
Constantin Graf
parent
5593d141ea
commit
b0bcc4f330
10
app/Exceptions/Api/PdfRendererIsNotConfiguredException.php
Normal file
10
app/Exceptions/Api/PdfRendererIsNotConfiguredException.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Exceptions\Api;
|
||||
|
||||
class PdfRendererIsNotConfiguredException extends ApiException
|
||||
{
|
||||
public const string KEY = 'pdf_renderer_is_not_configured';
|
||||
}
|
||||
@@ -5,8 +5,10 @@ declare(strict_types=1);
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Exceptions\Api\PdfRendererIsNotConfiguredException;
|
||||
use App\Exceptions\Api\TimeEntryCanNotBeRestartedApiException;
|
||||
use App\Exceptions\Api\TimeEntryStillRunningApiException;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateExportRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryAggregateRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryDestroyMultipleRequest;
|
||||
use App\Http\Requests\V1\TimeEntry\TimeEntryIndexExportRequest;
|
||||
@@ -28,15 +30,20 @@ use App\Service\ReportExport\TimeEntriesDetailedExport;
|
||||
use App\Service\TimeEntryAggregationService;
|
||||
use App\Service\TimeEntryFilter;
|
||||
use App\Service\TimezoneService;
|
||||
use Gotenberg\Gotenberg;
|
||||
use Gotenberg\Stream;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
class TimeEntryController extends Controller
|
||||
{
|
||||
@@ -49,7 +56,7 @@ class TimeEntryController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all time entries in organization
|
||||
* Get time entries in organization
|
||||
*
|
||||
* If you only need time entries for a specific user, you can filter by `user_id`.
|
||||
* Users with the permission `time-entries:view:own` can only use this endpoint with their own user ID in the user_id filter.
|
||||
@@ -146,7 +153,9 @@ class TimeEntryController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws AuthorizationException
|
||||
* Export time entries in organization
|
||||
*
|
||||
* @throws AuthorizationException|PdfRendererIsNotConfiguredException
|
||||
*
|
||||
* @operationId exportTimeEntries
|
||||
*/
|
||||
@@ -163,21 +172,40 @@ class TimeEntryController extends Controller
|
||||
$timeEntriesQuery = $this->getTimeEntriesQuery($organization, $request, $member);
|
||||
$timeEntriesQuery->with([
|
||||
'task',
|
||||
'project' => [
|
||||
'client',
|
||||
],
|
||||
'client',
|
||||
'project',
|
||||
'user',
|
||||
'tagsRelation',
|
||||
]);
|
||||
$format = $request->getFormatValue();
|
||||
//$format = ExportFormat::PDF;
|
||||
$filename = 'time-entries-export-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||
$path = 'exports/'.$filename;
|
||||
$folderPath = 'exports';
|
||||
$path = $folderPath.'/'.$filename;
|
||||
if ($format === ExportFormat::CSV) {
|
||||
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $filename, $timeEntriesQuery, 1000);
|
||||
$export = new TimeEntriesDetailedCsvExport(config('filesystems.private'), $folderPath, $filename, $timeEntriesQuery, 1000);
|
||||
$export->export();
|
||||
} elseif ($format === ExportFormat::PDF) {
|
||||
if (config('services.gotenberg.url') === null) {
|
||||
throw new PdfRendererIsNotConfiguredException;
|
||||
}
|
||||
$viewFile = file_get_contents(resource_path('views/reports/time-entry-index.blade.php'));
|
||||
$html = Blade::render($viewFile, ['timeEntries' => $timeEntriesQuery->get()]);
|
||||
$footerViewFile = file_get_contents(resource_path('views/reports/time-entry-index-footer.blade.php'));
|
||||
$footerHtml = Blade::render($footerViewFile);
|
||||
$request = Gotenberg::chromium(config('services.gotenberg.url'))
|
||||
->pdf()
|
||||
->pdfa('PDF/A-3b')
|
||||
->paperSize('8.27', '11.7') // A4
|
||||
->footer(Stream::string('footer', $footerHtml))
|
||||
->html(Stream::string('body', $html));
|
||||
$tempFolder = TemporaryDirectory::make();
|
||||
$filenameTemp = Gotenberg::save($request, $tempFolder->path());
|
||||
Storage::disk(config('filesystems.private'))
|
||||
->putFileAs($folderPath, new File($tempFolder->path($filenameTemp)), $filename);
|
||||
} else {
|
||||
Excel::store(
|
||||
new TimeEntriesDetailedExport($timeEntriesQuery),
|
||||
new TimeEntriesDetailedExport($timeEntriesQuery, $format),
|
||||
$path,
|
||||
config('filesystems.private'),
|
||||
$format->getExportPackageType(),
|
||||
@@ -225,7 +253,7 @@ class TimeEntryController extends Controller
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request, TimeEntryAggregationService $aggregationService): array
|
||||
public function aggregate(Organization $organization, TimeEntryAggregateRequest $request): array
|
||||
{
|
||||
/** @var Member|null $member */
|
||||
$member = $request->has('member_id') ? Member::query()->findOrFail($request->input('member_id')) : null;
|
||||
@@ -235,6 +263,75 @@ class TimeEntryController extends Controller
|
||||
$this->checkPermission($organization, 'time-entries:view:all');
|
||||
}
|
||||
|
||||
$aggregatedData = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
|
||||
return [
|
||||
'data' => $aggregatedData,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Export aggregated time entries in organization
|
||||
*
|
||||
* @throws AuthorizationException
|
||||
*/
|
||||
public function aggregateExport(Organization $organization, TimeEntryAggregateExportRequest $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');
|
||||
}
|
||||
|
||||
$aggregatedData = $this->getTimeEntriesAggregateQuery($organization, $request, $member);
|
||||
|
||||
$format = $request->getFormatValue();
|
||||
//$format = ExportFormat::PDF;
|
||||
$filename = 'time-entries-report-'.now()->format('Y-m-d_H-i-s').'.'.$format->getFileExtension();
|
||||
$folderPath = 'exports';
|
||||
$path = $folderPath.'/'.$filename;
|
||||
|
||||
if ($format === ExportFormat::CSV) {
|
||||
// TODO
|
||||
} elseif ($format === ExportFormat::PDF) {
|
||||
if (config('services.gotenberg.url') === null) {
|
||||
throw new PdfRendererIsNotConfiguredException;
|
||||
}
|
||||
// TODO
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'download_url' => Storage::disk(config('filesystems.private'))
|
||||
->temporaryUrl($path, now()->addMinutes(5)),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* grouped_type: string|null,
|
||||
* grouped_data: null|array<array{
|
||||
* key: string|null,
|
||||
* seconds: int,
|
||||
* cost: int,
|
||||
* grouped_type: string|null,
|
||||
* grouped_data: null|array<array{
|
||||
* key: string|null,
|
||||
* seconds: int,
|
||||
* cost: int,
|
||||
* grouped_type: null,
|
||||
* grouped_data: null
|
||||
* }>
|
||||
* }>,
|
||||
* seconds: int,
|
||||
* cost: int
|
||||
* }
|
||||
*/
|
||||
private function getTimeEntriesAggregateQuery(Organization $organization, TimeEntryAggregateRequest|TimeEntryAggregateExportRequest $request, ?Member $member): array
|
||||
{
|
||||
$timeEntriesQuery = TimeEntry::query()
|
||||
->whereBelongsTo($organization, 'organization');
|
||||
|
||||
@@ -256,7 +353,7 @@ class TimeEntryController extends Controller
|
||||
$group1Type = $request->getGroup();
|
||||
$group2Type = $request->getSubGroup();
|
||||
|
||||
$aggregatedData = $aggregationService->getAggregatedTimeEntries(
|
||||
$aggregatedData = app(TimeEntryAggregationService::class)->getAggregatedTimeEntries(
|
||||
$timeEntriesQuery,
|
||||
$group1Type,
|
||||
$group2Type,
|
||||
@@ -267,9 +364,7 @@ class TimeEntryController extends Controller
|
||||
$request->getEnd()
|
||||
);
|
||||
|
||||
return [
|
||||
'data' => $aggregatedData,
|
||||
];
|
||||
return $aggregatedData;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Requests\V1\TimeEntry;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Enums\TimeEntryAggregationType;
|
||||
use App\Models\Client;
|
||||
use App\Models\Member;
|
||||
use App\Models\Organization;
|
||||
use App\Models\Project;
|
||||
use App\Models\Tag;
|
||||
use App\Models\Task;
|
||||
use App\Models\User;
|
||||
use Illuminate\Contracts\Validation\ValidationRule;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Korridor\LaravelModelValidationRules\Rules\ExistsEloquent;
|
||||
|
||||
/**
|
||||
* @property Organization $organization
|
||||
*/
|
||||
class TimeEntryAggregateExportRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return ValidationRule
|
||||
*/
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'format' => [
|
||||
'required',
|
||||
'string',
|
||||
Rule::enum(ExportFormat::class),
|
||||
],
|
||||
'group' => [
|
||||
'nullable',
|
||||
'required_with:group_2',
|
||||
Rule::enum(TimeEntryAggregationType::class),
|
||||
],
|
||||
|
||||
'sub_group' => [
|
||||
'nullable',
|
||||
Rule::enum(TimeEntryAggregationType::class),
|
||||
],
|
||||
// Filter by member ID
|
||||
'member_id' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// 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',
|
||||
ExistsEloquent::make(Member::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Member> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
|
||||
// Filter by user ID
|
||||
'user_id' => [
|
||||
'string',
|
||||
ExistsEloquent::make(User::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<User> $builder */
|
||||
return $builder->belongsToOrganization($this->organization);
|
||||
})->uuid(),
|
||||
],
|
||||
// Filter by project IDs, project IDs are OR combined
|
||||
'project_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'project_ids.*' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Project::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Project> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// Filter by client IDs, client IDs are OR combined
|
||||
'client_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'client_ids.*' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Client::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Client> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// Filter by tag IDs, tag IDs are AND combined
|
||||
'tag_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'tag_ids.*' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Tag::class, null, function (Builder $builder): Builder {
|
||||
/** @var Builder<Tag> $builder */
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// Filter by task IDs, task IDs are OR combined
|
||||
'task_ids' => [
|
||||
'array',
|
||||
'min:1',
|
||||
],
|
||||
'task_ids.*' => [
|
||||
'string',
|
||||
ExistsEloquent::make(Task::class, null, function (Builder $builder): Builder {
|
||||
return $builder->whereBelongsTo($this->organization, 'organization');
|
||||
})->uuid(),
|
||||
],
|
||||
// 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',
|
||||
],
|
||||
'fill_gaps_in_time_groups' => [
|
||||
'string',
|
||||
'in:true,false',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function getGroup(): ?TimeEntryAggregationType
|
||||
{
|
||||
return $this->input('group') !== null ? TimeEntryAggregationType::from($this->input('group')) : null;
|
||||
}
|
||||
|
||||
public function getSubGroup(): ?TimeEntryAggregationType
|
||||
{
|
||||
return $this->input('sub_group') !== null ? TimeEntryAggregationType::from($this->input('sub_group')) : null;
|
||||
}
|
||||
|
||||
public function getFillGapsInTimeGroups(): bool
|
||||
{
|
||||
return $this->has('fill_gaps_in_time_groups') && $this->input('fill_gaps_in_time_groups') === 'true';
|
||||
}
|
||||
|
||||
public function getStart(): ?Carbon
|
||||
{
|
||||
return $this->input('start') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('start'), 'UTC') : null;
|
||||
}
|
||||
|
||||
public function getEnd(): ?Carbon
|
||||
{
|
||||
return $this->input('end') !== null ? Carbon::createFromFormat('Y-m-d\TH:i:s\Z', $this->input('end'), 'UTC') : null;
|
||||
}
|
||||
|
||||
public function getFormatValue(): ExportFormat
|
||||
{
|
||||
return ExportFormat::from($this->validated('format'));
|
||||
}
|
||||
}
|
||||
@@ -54,11 +54,11 @@ class TimeEntry extends Model implements AuditableContract
|
||||
{
|
||||
use ComputedAttributes;
|
||||
use CustomAuditable;
|
||||
|
||||
/** @use HasFactory<TimeEntryFactory> */
|
||||
use HasFactory;
|
||||
|
||||
use HasJsonRelationships;
|
||||
|
||||
use HasUuids;
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,10 +5,13 @@ declare(strict_types=1);
|
||||
namespace App\Service\ReportExport;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Http\File;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use League\Csv\Writer;
|
||||
use Spatie\TemporaryDirectory\TemporaryDirectory;
|
||||
|
||||
/**
|
||||
* @template T of Model
|
||||
@@ -31,16 +34,19 @@ abstract class CsvExport
|
||||
*/
|
||||
private Builder $builder;
|
||||
|
||||
private string $folderPath;
|
||||
|
||||
/**
|
||||
* @param Builder<T> $builder
|
||||
*/
|
||||
public function __construct(string $disk, string $filename, Builder $builder, int $chunk)
|
||||
public function __construct(string $disk, string $folderPath, string $filename, Builder $builder, int $chunk)
|
||||
{
|
||||
|
||||
$this->disk = $disk;
|
||||
$this->filename = $filename;
|
||||
$this->chunk = $chunk;
|
||||
$this->builder = $builder;
|
||||
$this->folderPath = $folderPath;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,12 +55,21 @@ abstract class CsvExport
|
||||
*/
|
||||
abstract public function mapRow(Model $model): array;
|
||||
|
||||
/**
|
||||
* @throws \League\Csv\CannotInsertRecord
|
||||
* @throws \League\Csv\Exception
|
||||
* @throws \League\Csv\UnavailableStream
|
||||
*/
|
||||
public function export(): void
|
||||
{
|
||||
$writer = Writer::createFromPath(Storage::disk($this->disk)->path($this->filename), 'w+');
|
||||
$tempDirectory = TemporaryDirectory::make();
|
||||
$writer = Writer::createFromPath($tempDirectory->path($this->filename), 'w+');
|
||||
$writer->setDelimiter(',');
|
||||
$writer->setEnclosure('"');
|
||||
$writer->setEscape('');
|
||||
$writer->insertOne(static::HEADER);
|
||||
|
||||
$this->builder->chunk($this->chunk, function ($models) use ($writer): void {
|
||||
$this->builder->chunk($this->chunk, function (Collection $models) use ($writer): void {
|
||||
foreach ($models as $model) {
|
||||
$data = $this->mapRow($model);
|
||||
$row = $this->convertRow($data);
|
||||
@@ -63,6 +78,8 @@ abstract class CsvExport
|
||||
$writer->insertOne(array_values($row));
|
||||
}
|
||||
});
|
||||
Storage::disk($this->disk)->putFileAs($this->folderPath, new File($tempDirectory->path($this->filename)), $this->filename);
|
||||
$tempDirectory->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,13 +104,11 @@ abstract class CsvExport
|
||||
|
||||
/**
|
||||
* @param array<string, string> $row
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function validateRow(array $row): void
|
||||
{
|
||||
if (array_keys($row) !== self::HEADER) {
|
||||
throw new \Exception('Invalid row');
|
||||
if (array_keys($row) !== static::HEADER) {
|
||||
throw new \LogicException('Invalid row');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,17 +13,17 @@ use Illuminate\Database\Eloquent\Model;
|
||||
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',
|
||||
'Description',
|
||||
'Task',
|
||||
'Project',
|
||||
'Client',
|
||||
'User',
|
||||
'Start',
|
||||
'End',
|
||||
'Duration',
|
||||
'Duration (decimal)',
|
||||
'Billable',
|
||||
'Tags',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -31,16 +31,20 @@ class TimeEntriesDetailedCsvExport extends CsvExport
|
||||
*/
|
||||
public function mapRow(Model $model): array
|
||||
{
|
||||
$duration = $model->getDuration();
|
||||
|
||||
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,
|
||||
'Description' => $model->description,
|
||||
'Task' => $model->task?->name,
|
||||
'Project' => $model->project?->name,
|
||||
'Client' => $model->client?->name,
|
||||
'User' => $model->user->name,
|
||||
'Start' => $model->start->format('Y-m-d H:i:s'),
|
||||
'End' => $model->end?->format('Y-m-d H:i:s'),
|
||||
'Duration' => $duration !== null ? (int) floor($duration->totalHours).':'.$duration->format('%I:%S') : null,
|
||||
'Duration (decimal)' => $duration?->totalHours,
|
||||
'Billable' => $model->billable ? 'Yes' : 'No',
|
||||
'Tags' => $model->tagsRelation->pluck('name')->implode(', '),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,18 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Service\ReportExport;
|
||||
|
||||
use App\Enums\ExportFormat;
|
||||
use App\Models\TimeEntry;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use LogicException;
|
||||
use Maatwebsite\Excel\Concerns\Exportable;
|
||||
use Maatwebsite\Excel\Concerns\FromQuery;
|
||||
use Maatwebsite\Excel\Concerns\WithCustomCsvSettings;
|
||||
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||
use Maatwebsite\Excel\Concerns\WithDefaultStyles;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\WithStyles;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
|
||||
/**
|
||||
* @implements WithMapping<TimeEntry>
|
||||
*/
|
||||
class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, WithHeadings, WithMapping
|
||||
class TimeEntriesDetailedExport implements FromQuery, ShouldAutoSize, WithColumnFormatting, WithDefaultStyles, WithHeadings, WithMapping, WithStyles
|
||||
{
|
||||
use Exportable;
|
||||
|
||||
@@ -24,12 +33,15 @@ class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, Wit
|
||||
*/
|
||||
private Builder $builder;
|
||||
|
||||
private ExportFormat $exportFormat;
|
||||
|
||||
/**
|
||||
* @param Builder<TimeEntry> $builder
|
||||
*/
|
||||
public function __construct(Builder $builder)
|
||||
public function __construct(Builder $builder, ExportFormat $exportFormat)
|
||||
{
|
||||
$this->builder = $builder;
|
||||
$this->exportFormat = $exportFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,18 +52,41 @@ class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, Wit
|
||||
return $this->builder;
|
||||
}
|
||||
|
||||
public function columnFormats(): array
|
||||
{
|
||||
if ($this->exportFormat === ExportFormat::XLSX) {
|
||||
return [
|
||||
'F' => 'yyyy-mm-dd hh:mm:ss',
|
||||
'G' => 'yyyy-mm-dd hh:mm:ss',
|
||||
'I' => NumberFormat::FORMAT_NUMBER_00,
|
||||
];
|
||||
} elseif ($this->exportFormat === ExportFormat::ODS) {
|
||||
return [
|
||||
'I' => NumberFormat::FORMAT_NUMBER_00,
|
||||
];
|
||||
} else {
|
||||
throw new LogicException('Unsupported export format.');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|bool>
|
||||
* @return array<int|string, array<string, array<string, bool>>>
|
||||
*/
|
||||
public function getCsvSettings(): array
|
||||
public function styles(Worksheet $sheet): array
|
||||
{
|
||||
return [
|
||||
'delimiter' => ',',
|
||||
'use_bom' => false,
|
||||
'output_encoding' => 'ISO-8859-1',
|
||||
// Style the first row as bold text.
|
||||
1 => ['font' => ['bold' => true]],
|
||||
];
|
||||
}
|
||||
|
||||
public function defaultStyles(Style $defaultStyle)
|
||||
{
|
||||
// Configure the default styles
|
||||
return $defaultStyle->getFill(); //->setFillType(Fill::FILL_SOLID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
@@ -63,10 +98,8 @@ class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, Wit
|
||||
'Project',
|
||||
'Client',
|
||||
'User',
|
||||
'Start date',
|
||||
'Start time',
|
||||
'End date',
|
||||
'End time',
|
||||
'Start',
|
||||
'End',
|
||||
'Duration',
|
||||
'Duration (decimal)',
|
||||
'Billable',
|
||||
@@ -82,20 +115,36 @@ class TimeEntriesDetailedExport implements FromQuery, WithCustomCsvSettings, Wit
|
||||
{
|
||||
$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(', '),
|
||||
];
|
||||
if ($this->exportFormat === ExportFormat::XLSX) {
|
||||
return [
|
||||
$model->description,
|
||||
$model->task?->name,
|
||||
$model->project?->name,
|
||||
$model->client?->name,
|
||||
$model->user->name,
|
||||
Date::dateTimeToExcel($model->start),
|
||||
$model->end !== null ? Date::dateTimeToExcel($model->end) : null,
|
||||
$duration !== null ? (int) floor($duration->totalHours).':'.$duration->format('%I:%S') : null,
|
||||
$duration?->totalHours,
|
||||
$model->billable ? 'Yes' : 'No',
|
||||
$model->tagsRelation->pluck('name')->implode(', '),
|
||||
];
|
||||
} elseif ($this->exportFormat === ExportFormat::ODS) {
|
||||
return [
|
||||
$model->description,
|
||||
$model->task?->name,
|
||||
$model->project?->name,
|
||||
$model->client?->name,
|
||||
$model->user->name,
|
||||
$model->start->format('Y-m-d H:i:s'),
|
||||
$model->end?->format('Y-m-d 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(', '),
|
||||
];
|
||||
} else {
|
||||
throw new LogicException('Unsupported export format.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user