Add report exports

This commit is contained in:
Constantin Graf
2024-10-16 13:29:54 +02:00
committed by Constantin Graf
parent e54df74d5d
commit 64535ceea6
12 changed files with 1452 additions and 17 deletions
+35
View 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
*
@@ -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'));
}
}
+13
View File
@@ -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');
}
}
+16 -1
View File
@@ -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
View 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');
}
}
}
@@ -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,
];
}
}
@@ -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(', '),
];
}
}
+3
View File
@@ -20,12 +20,15 @@
"laravel/octane": "^2.3",
"laravel/passport": "^12.0",
"laravel/tinker": "^2.8",
"league/csv": "^9.16.0",
"league/flysystem-aws-s3-v3": "^3.0",
"maatwebsite/excel": "^3.1",
"novadaemon/filament-pretty-json": "^2.2",
"nwidart/laravel-modules": "^11.0.11",
"owen-it/laravel-auditing": "^13.6",
"pxlrbt/filament-environment-indicator": "^2.0",
"spatie/temporary-directory": "^2.2",
"staudenmeir/eloquent-json-relations": "^1.1",
"stechstudio/filament-impersonate": "^3.8",
"tightenco/ziggy": "^2.1.0",
"tpetry/laravel-postgresql-enhanced": "^2.0.0",
Generated
+535 -1
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "84e9d436af2f46e57ecc42a117e94259",
"content-hash": "7b901c08f4d2a3f90c4d667bd1470dcb",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -2093,6 +2093,67 @@
],
"time": "2023-10-06T06:47:41+00:00"
},
{
"name": "ezyang/htmlpurifier",
"version": "v4.17.0",
"source": {
"type": "git",
"url": "https://github.com/ezyang/htmlpurifier.git",
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/bbc513d79acf6691fa9cf10f192c90dd2957f18c",
"reference": "bbc513d79acf6691fa9cf10f192c90dd2957f18c",
"shasum": ""
},
"require": {
"php": "~5.6.0 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0 || ~7.4.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0"
},
"require-dev": {
"cerdic/css-tidy": "^1.7 || ^2.0",
"simpletest/simpletest": "dev-master"
},
"suggest": {
"cerdic/css-tidy": "If you want to use the filter 'Filter.ExtractStyleBlocks'.",
"ext-bcmath": "Used for unit conversion and imagecrash protection",
"ext-iconv": "Converts text to and from non-UTF-8 encodings",
"ext-tidy": "Used for pretty-printing HTML"
},
"type": "library",
"autoload": {
"files": [
"library/HTMLPurifier.composer.php"
],
"psr-0": {
"HTMLPurifier": "library/"
},
"exclude-from-classmap": [
"/library/HTMLPurifier/Language/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"LGPL-2.1-or-later"
],
"authors": [
{
"name": "Edward Z. Yang",
"email": "admin@htmlpurifier.org",
"homepage": "http://ezyang.com"
}
],
"description": "Standards compliant HTML filter written in PHP",
"homepage": "http://htmlpurifier.org/",
"keywords": [
"html"
],
"support": {
"issues": "https://github.com/ezyang/htmlpurifier/issues",
"source": "https://github.com/ezyang/htmlpurifier/tree/v4.17.0"
},
"time": "2023-11-17T15:01:25+00:00"
},
{
"name": "filament/actions",
"version": "v3.2.115",
@@ -5435,6 +5496,271 @@
],
"time": "2024-07-15T18:27:32+00:00"
},
{
"name": "maatwebsite/excel",
"version": "3.1.58",
"source": {
"type": "git",
"url": "https://github.com/SpartnerNL/Laravel-Excel.git",
"reference": "18495a71b112f43af8ffab35111a58b4e4ba4a4d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/18495a71b112f43af8ffab35111a58b4e4ba4a4d",
"reference": "18495a71b112f43af8ffab35111a58b4e4ba4a4d",
"shasum": ""
},
"require": {
"composer/semver": "^3.3",
"ext-json": "*",
"illuminate/support": "5.8.*||^6.0||^7.0||^8.0||^9.0||^10.0||^11.0",
"php": "^7.0||^8.0",
"phpoffice/phpspreadsheet": "^1.29.1",
"psr/simple-cache": "^1.0||^2.0||^3.0"
},
"require-dev": {
"laravel/scout": "^7.0||^8.0||^9.0||^10.0",
"orchestra/testbench": "^6.0||^7.0||^8.0||^9.0",
"predis/predis": "^1.1"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Maatwebsite\\Excel\\ExcelServiceProvider"
],
"aliases": {
"Excel": "Maatwebsite\\Excel\\Facades\\Excel"
}
}
},
"autoload": {
"psr-4": {
"Maatwebsite\\Excel\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Patrick Brouwers",
"email": "patrick@spartner.nl"
}
],
"description": "Supercharged Excel exports and imports in Laravel",
"keywords": [
"PHPExcel",
"batch",
"csv",
"excel",
"export",
"import",
"laravel",
"php",
"phpspreadsheet"
],
"support": {
"issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
"source": "https://github.com/SpartnerNL/Laravel-Excel/tree/3.1.58"
},
"funding": [
{
"url": "https://laravel-excel.com/commercial-support",
"type": "custom"
},
{
"url": "https://github.com/patrickbrouwers",
"type": "github"
}
],
"time": "2024-09-07T13:53:36+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "3.1.1",
"source": {
"type": "git",
"url": "https://github.com/maennchen/ZipStream-PHP.git",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9",
"reference": "6187e9cc4493da94b9b63eb2315821552015fca9",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"ext-zlib": "*",
"php-64bit": "^8.1"
},
"require-dev": {
"ext-zip": "*",
"friendsofphp/php-cs-fixer": "^3.16",
"guzzlehttp/guzzle": "^7.5",
"mikey179/vfsstream": "^1.6",
"php-coveralls/php-coveralls": "^2.5",
"phpunit/phpunit": "^10.0",
"vimeo/psalm": "^5.0"
},
"suggest": {
"guzzlehttp/psr7": "^2.4",
"psr/http-message": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"ZipStream\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paul Duncan",
"email": "pabs@pablotron.org"
},
{
"name": "Jonatan Männchen",
"email": "jonatan@maennchen.ch"
},
{
"name": "Jesse Donat",
"email": "donatj@gmail.com"
},
{
"name": "András Kolesár",
"email": "kolesar@kolesar.hu"
}
],
"description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
"keywords": [
"stream",
"zip"
],
"support": {
"issues": "https://github.com/maennchen/ZipStream-PHP/issues",
"source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1"
},
"funding": [
{
"url": "https://github.com/maennchen",
"type": "github"
}
],
"time": "2024-10-10T12:33:01+00:00"
},
{
"name": "markbaker/complex",
"version": "3.0.2",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPComplex.git",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
"shasum": ""
},
"require": {
"php": "^7.2 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Complex\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@lange.demon.co.uk"
}
],
"description": "PHP Class for working with complex numbers",
"homepage": "https://github.com/MarkBaker/PHPComplex",
"keywords": [
"complex",
"mathematics"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPComplex/issues",
"source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
},
"time": "2022-12-06T16:21:08+00:00"
},
{
"name": "markbaker/matrix",
"version": "3.0.1",
"source": {
"type": "git",
"url": "https://github.com/MarkBaker/PHPMatrix.git",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
"reference": "728434227fe21be27ff6d86621a1b13107a2562c",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-master",
"phpcompatibility/php-compatibility": "^9.3",
"phpdocumentor/phpdocumentor": "2.*",
"phploc/phploc": "^4.0",
"phpmd/phpmd": "2.*",
"phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
"sebastian/phpcpd": "^4.0",
"squizlabs/php_codesniffer": "^3.7"
},
"type": "library",
"autoload": {
"psr-4": {
"Matrix\\": "classes/src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Baker",
"email": "mark@demon-angel.eu"
}
],
"description": "PHP Class for working with matrices",
"homepage": "https://github.com/MarkBaker/PHPMatrix",
"keywords": [
"mathematics",
"matrix",
"vector"
],
"support": {
"issues": "https://github.com/MarkBaker/PHPMatrix/issues",
"source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
},
"time": "2022-12-02T22:17:43+00:00"
},
{
"name": "masterminds/html5",
"version": "2.9.0",
@@ -6670,6 +6996,111 @@
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.2",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f",
"reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f",
"shasum": ""
},
"require": {
"ext-ctype": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-iconv": "*",
"ext-libxml": "*",
"ext-mbstring": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"ext-xmlreader": "*",
"ext-xmlwriter": "*",
"ext-zip": "*",
"ext-zlib": "*",
"ezyang/htmlpurifier": "^4.15",
"maennchen/zipstream-php": "^2.1 || ^3.0",
"markbaker/complex": "^3.0",
"markbaker/matrix": "^3.0",
"php": "^7.4 || ^8.0",
"psr/http-client": "^1.0",
"psr/http-factory": "^1.0",
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
},
"require-dev": {
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
"dompdf/dompdf": "^1.0 || ^2.0",
"friendsofphp/php-cs-fixer": "^3.2",
"mitoteam/jpgraph": "^10.3",
"mpdf/mpdf": "^8.1.1",
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
"suggest": {
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
"ext-intl": "PHP Internationalization Functions",
"mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
},
"type": "library",
"autoload": {
"psr-4": {
"PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Maarten Balliauw",
"homepage": "https://blog.maartenballiauw.be"
},
{
"name": "Mark Baker",
"homepage": "https://markbakeruk.net"
},
{
"name": "Franck Lefevre",
"homepage": "https://rootslabs.net"
},
{
"name": "Erik Tilt"
},
{
"name": "Adrien Crivelli"
}
],
"description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
"homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
"keywords": [
"OpenXML",
"excel",
"gnumeric",
"ods",
"php",
"spreadsheet",
"xls",
"xlsx"
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.2"
},
"time": "2024-09-29T07:04:47+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
@@ -8407,6 +8838,109 @@
],
"time": "2023-12-25T11:46:58+00:00"
},
{
"name": "staudenmeir/eloquent-has-many-deep-contracts",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts.git",
"reference": "3ad76c6eeda60042f262d113bf471dcce584d88b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/staudenmeir/eloquent-has-many-deep-contracts/zipball/3ad76c6eeda60042f262d113bf471dcce584d88b",
"reference": "3ad76c6eeda60042f262d113bf471dcce584d88b",
"shasum": ""
},
"require": {
"illuminate/database": "^11.0",
"php": "^8.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Staudenmeir\\EloquentHasManyDeepContracts\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonas Staudenmeir",
"email": "mail@jonas-staudenmeir.de"
}
],
"description": "Contracts for staudenmeir/eloquent-has-many-deep",
"support": {
"issues": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts/issues",
"source": "https://github.com/staudenmeir/eloquent-has-many-deep-contracts/tree/v1.2.1"
},
"time": "2024-09-25T18:24:22+00:00"
},
{
"name": "staudenmeir/eloquent-json-relations",
"version": "v1.13.1",
"source": {
"type": "git",
"url": "https://github.com/staudenmeir/eloquent-json-relations.git",
"reference": "65533e304061ee649c0bcfd0e0da9376712e8b0e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/staudenmeir/eloquent-json-relations/zipball/65533e304061ee649c0bcfd0e0da9376712e8b0e",
"reference": "65533e304061ee649c0bcfd0e0da9376712e8b0e",
"shasum": ""
},
"require": {
"illuminate/database": "^11.0",
"php": "^8.2",
"staudenmeir/eloquent-has-many-deep-contracts": "^1.2"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^3.0",
"larastan/larastan": "^2.9",
"orchestra/testbench": "^9.0",
"phpunit/phpunit": "^11.0",
"staudenmeir/eloquent-has-many-deep": "^1.20"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Staudenmeir\\EloquentJsonRelations\\IdeHelperServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Staudenmeir\\EloquentJsonRelations\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jonas Staudenmeir",
"email": "mail@jonas-staudenmeir.de"
}
],
"description": "Laravel Eloquent relationships with JSON keys",
"support": {
"issues": "https://github.com/staudenmeir/eloquent-json-relations/issues",
"source": "https://github.com/staudenmeir/eloquent-json-relations/tree/v1.13.1"
},
"funding": [
{
"url": "https://paypal.me/JonasStaudenmeir",
"type": "custom"
}
],
"time": "2024-10-06T19:12:12+00:00"
},
{
"name": "stechstudio/filament-impersonate",
"version": "3.14",
+382
View File
@@ -0,0 +1,382 @@
<?php
declare(strict_types=1);
use Maatwebsite\Excel\Excel;
use PhpOffice\PhpSpreadsheet\Reader\Csv;
return [
'exports' => [
/*
|--------------------------------------------------------------------------
| Chunk size
|--------------------------------------------------------------------------
|
| When using FromQuery, the query is automatically chunked.
| Here you can specify how big the chunk should be.
|
*/
'chunk_size' => 1000,
/*
|--------------------------------------------------------------------------
| Pre-calculate formulas during export
|--------------------------------------------------------------------------
*/
'pre_calculate_formulas' => false,
/*
|--------------------------------------------------------------------------
| Enable strict null comparison
|--------------------------------------------------------------------------
|
| When enabling strict null comparison empty cells ('') will
| be added to the sheet.
*/
'strict_null_comparison' => false,
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV exports.
|
*/
'csv' => [
'delimiter' => ',',
'enclosure' => '"',
'line_ending' => PHP_EOL,
'use_bom' => false,
'include_separator_line' => false,
'excel_compatibility' => false,
'output_encoding' => '',
'test_auto_detect' => true,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
],
'imports' => [
/*
|--------------------------------------------------------------------------
| Read Only
|--------------------------------------------------------------------------
|
| When dealing with imports, you might only be interested in the
| data that the sheet exists. By default we ignore all styles,
| however if you want to do some logic based on style data
| you can enable it by setting read_only to false.
|
*/
'read_only' => true,
/*
|--------------------------------------------------------------------------
| Ignore Empty
|--------------------------------------------------------------------------
|
| When dealing with imports, you might be interested in ignoring
| rows that have null values or empty strings. By default rows
| containing empty strings or empty values are not ignored but can be
| ignored by enabling the setting ignore_empty to true.
|
*/
'ignore_empty' => false,
/*
|--------------------------------------------------------------------------
| Heading Row Formatter
|--------------------------------------------------------------------------
|
| Configure the heading row formatter.
| Available options: none|slug|custom
|
*/
'heading_row' => [
'formatter' => 'slug',
],
/*
|--------------------------------------------------------------------------
| CSV Settings
|--------------------------------------------------------------------------
|
| Configure e.g. delimiter, enclosure and line ending for CSV imports.
|
*/
'csv' => [
'delimiter' => null,
'enclosure' => '"',
'escape_character' => '\\',
'contiguous' => false,
'input_encoding' => Csv::GUESS_ENCODING,
],
/*
|--------------------------------------------------------------------------
| Worksheet properties
|--------------------------------------------------------------------------
|
| Configure e.g. default title, creator, subject,...
|
*/
'properties' => [
'creator' => '',
'lastModifiedBy' => '',
'title' => '',
'description' => '',
'subject' => '',
'keywords' => '',
'category' => '',
'manager' => '',
'company' => '',
],
/*
|--------------------------------------------------------------------------
| Cell Middleware
|--------------------------------------------------------------------------
|
| Configure middleware that is executed on getting a cell value
|
*/
'cells' => [
'middleware' => [
//\Maatwebsite\Excel\Middleware\TrimCellValue::class,
//\Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
],
],
],
/*
|--------------------------------------------------------------------------
| Extension detector
|--------------------------------------------------------------------------
|
| Configure here which writer/reader type should be used when the package
| needs to guess the correct type based on the extension alone.
|
*/
'extension_detector' => [
'xlsx' => Excel::XLSX,
'xlsm' => Excel::XLSX,
'xltx' => Excel::XLSX,
'xltm' => Excel::XLSX,
'xls' => Excel::XLS,
'xlt' => Excel::XLS,
'ods' => Excel::ODS,
'ots' => Excel::ODS,
'slk' => Excel::SLK,
'xml' => Excel::XML,
'gnumeric' => Excel::GNUMERIC,
'htm' => Excel::HTML,
'html' => Excel::HTML,
'csv' => Excel::CSV,
'tsv' => Excel::TSV,
/*
|--------------------------------------------------------------------------
| PDF Extension
|--------------------------------------------------------------------------
|
| Configure here which Pdf driver should be used by default.
| Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
|
*/
'pdf' => Excel::DOMPDF,
],
/*
|--------------------------------------------------------------------------
| Value Binder
|--------------------------------------------------------------------------
|
| PhpSpreadsheet offers a way to hook into the process of a value being
| written to a cell. In there some assumptions are made on how the
| value should be formatted. If you want to change those defaults,
| you can implement your own default value binder.
|
| Possible value binders:
|
| [x] Maatwebsite\Excel\DefaultValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
| [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
|
*/
'value_binder' => [
'default' => Maatwebsite\Excel\DefaultValueBinder::class,
],
'cache' => [
/*
|--------------------------------------------------------------------------
| Default cell caching driver
|--------------------------------------------------------------------------
|
| By default PhpSpreadsheet keeps all cell values in memory, however when
| dealing with large files, this might result into memory issues. If you
| want to mitigate that, you can configure a cell caching driver here.
| When using the illuminate driver, it will store each value in the
| cache store. This can slow down the process, because it needs to
| store each value. You can use the "batch" store if you want to
| only persist to the store when the memory limit is reached.
|
| Drivers: memory|illuminate|batch
|
*/
'driver' => 'memory',
/*
|--------------------------------------------------------------------------
| Batch memory caching
|--------------------------------------------------------------------------
|
| When dealing with the "batch" caching driver, it will only
| persist to the store when the memory limit is reached.
| Here you can tweak the memory limit to your liking.
|
*/
'batch' => [
'memory_limit' => 60000,
],
/*
|--------------------------------------------------------------------------
| Illuminate cache
|--------------------------------------------------------------------------
|
| When using the "illuminate" caching driver, it will automatically use
| your default cache store. However if you prefer to have the cell
| cache on a separate store, you can configure the store name here.
| You can use any store defined in your cache config. When leaving
| at "null" it will use the default store.
|
*/
'illuminate' => [
'store' => null,
],
/*
|--------------------------------------------------------------------------
| Cache Time-to-live (TTL)
|--------------------------------------------------------------------------
|
| The TTL of items written to cache. If you want to keep the items cached
| indefinitely, set this to null. Otherwise, set a number of seconds,
| a \DateInterval, or a callable.
|
| Allowable types: callable|\DateInterval|int|null
|
*/
'default_ttl' => 10800,
],
/*
|--------------------------------------------------------------------------
| Transaction Handler
|--------------------------------------------------------------------------
|
| By default the import is wrapped in a transaction. This is useful
| for when an import may fail and you want to retry it. With the
| transactions, the previous import gets rolled-back.
|
| You can disable the transaction handler by setting this to null.
| Or you can choose a custom made transaction handler here.
|
| Supported handlers: null|db
|
*/
'transactions' => [
'handler' => 'db',
'db' => [
'connection' => null,
],
],
'temporary_files' => [
/*
|--------------------------------------------------------------------------
| Local Temporary Path
|--------------------------------------------------------------------------
|
| When exporting and importing files, we use a temporary file, before
| storing reading or downloading. Here you can customize that path.
| permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
|
*/
'local_path' => storage_path('framework/cache/laravel-excel'),
/*
|--------------------------------------------------------------------------
| Local Temporary Path Permissions
|--------------------------------------------------------------------------
|
| Permissions is an array with the permission flags for the directory (dir)
| and the create file (file).
| If omitted the default permissions of the filesystem will be used.
|
*/
'local_permissions' => [
// 'dir' => 0755,
// 'file' => 0644,
],
/*
|--------------------------------------------------------------------------
| Remote Temporary Disk
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup with queues in which you
| cannot rely on having a shared local temporary path, you might
| want to store the temporary file on a shared disk. During the
| queue executing, we'll retrieve the temporary file from that
| location instead. When left to null, it will always use
| the local path. This setting only has effect when using
| in conjunction with queued imports and exports.
|
*/
'remote_disk' => null,
'remote_prefix' => null,
/*
|--------------------------------------------------------------------------
| Force Resync
|--------------------------------------------------------------------------
|
| When dealing with a multi server setup as above, it's possible
| for the clean up that occurs after entire queue has been run to only
| cleanup the server that the last AfterImportJob runs on. The rest of the server
| would still have the local temporary file stored on it. In this case your
| local storage limits can be exceeded and future imports won't be processed.
| To mitigate this you can set this config value to be true, so that after every
| queued chunk is processed the local temporary file is deleted on the server that
| processed it.
|
*/
'force_resync_remote' => null,
],
];
+1
View File
@@ -87,6 +87,7 @@ Route::middleware([
// Time entry routes
Route::name('time-entries.')->group(static function (): void {
Route::get('/organizations/{organization}/time-entries', [TimeEntryController::class, 'index'])->name('index');
Route::get('/organizations/{organization}/time-entries/export', [TimeEntryController::class, 'indexExport'])->name('index-export');
Route::get('/organizations/{organization}/time-entries/aggregate', [TimeEntryController::class, 'aggregate'])->name('aggregate');
Route::post('/organizations/{organization}/time-entries', [TimeEntryController::class, 'store'])->name('store')->middleware('check-organization-blocked');
Route::put('/organizations/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->name('update')->middleware('check-organization-blocked');