mirror of
https://github.com/solidtime-io/solidtime.git
synced 2026-06-15 13:32:43 +01:00
Added tag endpoints
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ yarn-error.log
|
|||||||
/playwright-report/
|
/playwright-report/
|
||||||
/blob-report/
|
/blob-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
/coverage
|
||||||
|
|||||||
88
app/Http/Controllers/Api/V1/TagController.php
Normal file
88
app/Http/Controllers/Api/V1/TagController.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Http\Requests\V1\Tag\TagStoreRequest;
|
||||||
|
use App\Http\Requests\V1\Tag\TagUpdateRequest;
|
||||||
|
use App\Http\Resources\V1\Tag\TagCollection;
|
||||||
|
use App\Http\Resources\V1\Tag\TagResource;
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
class TagController extends Controller
|
||||||
|
{
|
||||||
|
protected function checkPermission(Organization $organization, string $permission, ?Tag $tag = null): void
|
||||||
|
{
|
||||||
|
parent::checkPermission($organization, $permission);
|
||||||
|
if ($tag !== null && $tag->organization_id !== $organization->getKey()) {
|
||||||
|
throw new AuthorizationException('Tag does not belong to organization');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get tags
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*/
|
||||||
|
public function index(Organization $organization): TagCollection
|
||||||
|
{
|
||||||
|
$this->checkPermission($organization, 'tags:view');
|
||||||
|
|
||||||
|
$tags = Tag::query()
|
||||||
|
->whereBelongsTo($organization, 'organization')
|
||||||
|
->orderBy('created_at', 'desc')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return new TagCollection($tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create tag
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*/
|
||||||
|
public function store(Organization $organization, TagStoreRequest $request): TagResource
|
||||||
|
{
|
||||||
|
$this->checkPermission($organization, 'tags:create');
|
||||||
|
|
||||||
|
$tag = new Tag();
|
||||||
|
$tag->name = $request->input('name');
|
||||||
|
$tag->organization()->associate($organization);
|
||||||
|
$tag->save();
|
||||||
|
|
||||||
|
return new TagResource($tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update tag
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*/
|
||||||
|
public function update(Organization $organization, Tag $tag, TagUpdateRequest $request): TagResource
|
||||||
|
{
|
||||||
|
$this->checkPermission($organization, 'tags:update', $tag);
|
||||||
|
|
||||||
|
$tag->name = $request->input('name');
|
||||||
|
$tag->save();
|
||||||
|
|
||||||
|
return new TagResource($tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete tag
|
||||||
|
*
|
||||||
|
* @throws AuthorizationException
|
||||||
|
*/
|
||||||
|
public function destroy(Organization $organization, Tag $tag): JsonResponse
|
||||||
|
{
|
||||||
|
$this->checkPermission($organization, 'tags:delete', $tag);
|
||||||
|
|
||||||
|
$tag->delete();
|
||||||
|
|
||||||
|
return response()->json(null, 204);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,11 +114,13 @@ class TimeEntryController extends Controller
|
|||||||
|
|
||||||
if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
|
if ($request->get('end') === null && TimeEntry::query()->where('user_id', $request->get('user_id'))->where('end', null)->exists()) {
|
||||||
// TODO: API documentation
|
// TODO: API documentation
|
||||||
|
// TODO: Create concept for api exceptions
|
||||||
throw new TimeEntryStillRunning('User already has an active time entry');
|
throw new TimeEntryStillRunning('User already has an active time entry');
|
||||||
}
|
}
|
||||||
|
|
||||||
$timeEntry = new TimeEntry();
|
$timeEntry = new TimeEntry();
|
||||||
$timeEntry->fill($request->validated());
|
$timeEntry->fill($request->validated());
|
||||||
|
$timeEntry->description = $request->get('description', '');
|
||||||
$timeEntry->organization()->associate($organization);
|
$timeEntry->organization()->associate($organization);
|
||||||
$timeEntry->save();
|
$timeEntry->save();
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ProjectStoreRequest extends FormRequest
|
|||||||
'name' => [
|
'name' => [
|
||||||
'required',
|
'required',
|
||||||
'string',
|
'string',
|
||||||
|
'min:1',
|
||||||
'max:255',
|
'max:255',
|
||||||
],
|
],
|
||||||
'color' => [
|
'color' => [
|
||||||
|
|||||||
28
app/Http/Requests/V1/Tag/TagStoreRequest.php
Normal file
28
app/Http/Requests/V1/Tag/TagStoreRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Tag;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TagStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, array<string|ValidationRule>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'min:1',
|
||||||
|
'max:255',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/V1/Tag/TagUpdateRequest.php
Normal file
28
app/Http/Requests/V1/Tag/TagUpdateRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\V1\Tag;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TagUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, array<string|ValidationRule>>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'min:1',
|
||||||
|
'max:255',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,13 +52,12 @@ class TimeEntryStoreRequest extends FormRequest
|
|||||||
// Start of time entry (ISO 8601 format, UTC timezone)
|
// Start of time entry (ISO 8601 format, UTC timezone)
|
||||||
'start' => [
|
'start' => [
|
||||||
'required',
|
'required',
|
||||||
'date', // TODO
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
],
|
],
|
||||||
// End of time entry (ISO 8601 format, UTC timezone)
|
// End of time entry (ISO 8601 format, UTC timezone)
|
||||||
'end' => [
|
'end' => [
|
||||||
'required',
|
|
||||||
'nullable',
|
'nullable',
|
||||||
'date', // TODO
|
'date_format:Y-m-d\TH:i:s\Z',
|
||||||
'after:start',
|
'after:start',
|
||||||
],
|
],
|
||||||
// Description of time entry
|
// Description of time entry
|
||||||
|
|||||||
17
app/Http/Resources/V1/Tag/TagCollection.php
Normal file
17
app/Http/Resources/V1/Tag/TagCollection.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Resources\V1\Tag;
|
||||||
|
|
||||||
|
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||||
|
|
||||||
|
class TagCollection extends ResourceCollection
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The resource that this resource collects.
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
public $collects = TagResource::class;
|
||||||
|
}
|
||||||
34
app/Http/Resources/V1/Tag/TagResource.php
Normal file
34
app/Http/Resources/V1/Tag/TagResource.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Resources\V1\Tag;
|
||||||
|
|
||||||
|
use App\Http\Resources\V1\BaseResource;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property Tag $resource
|
||||||
|
*/
|
||||||
|
class TagResource extends BaseResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Transform the resource into an array.
|
||||||
|
*
|
||||||
|
* @return array<string, string|bool|int|null>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
/** @var string $id ID */
|
||||||
|
'id' => $this->resource->id,
|
||||||
|
/** @var string $name Name */
|
||||||
|
'name' => $this->resource->name,
|
||||||
|
/** @var string $created_at When the tag was created */
|
||||||
|
'created_at' => $this->formatDateTime($this->resource->created_at),
|
||||||
|
/** @var string $updated_at When the tag was last updated */
|
||||||
|
'updated_at' => $this->formatDateTime($this->resource->updated_at),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,11 +9,14 @@ use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string $id
|
* @property string $id
|
||||||
* @property string $name
|
* @property string $name
|
||||||
* @property string $organization_id
|
* @property string $organization_id
|
||||||
|
* @property Carbon|null $created_at
|
||||||
|
* @property Carbon|null $updated_at
|
||||||
* @property-read Organization $organization
|
* @property-read Organization $organization
|
||||||
*
|
*
|
||||||
* @method static TagFactory factory()
|
* @method static TagFactory factory()
|
||||||
|
|||||||
@@ -64,6 +64,10 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'time-entries:create:own',
|
'time-entries:create:own',
|
||||||
'time-entries:update:own',
|
'time-entries:update:own',
|
||||||
'time-entries:delete:own',
|
'time-entries:delete:own',
|
||||||
|
'tags:view',
|
||||||
|
'tags:create',
|
||||||
|
'tags:update',
|
||||||
|
'tags:delete',
|
||||||
])->description('Administrator users can perform any action.');
|
])->description('Administrator users can perform any action.');
|
||||||
|
|
||||||
Jetstream::role('manager', 'Manager', [
|
Jetstream::role('manager', 'Manager', [
|
||||||
@@ -79,10 +83,15 @@ class JetstreamServiceProvider extends ServiceProvider
|
|||||||
'time-entries:create:own',
|
'time-entries:create:own',
|
||||||
'time-entries:update:own',
|
'time-entries:update:own',
|
||||||
'time-entries:delete:own',
|
'time-entries:delete:own',
|
||||||
|
'tags:view',
|
||||||
|
'tags:create',
|
||||||
|
'tags:update',
|
||||||
|
'tags:delete',
|
||||||
])->description('Editor users have the ability to read, create, and update.');
|
])->description('Editor users have the ability to read, create, and update.');
|
||||||
|
|
||||||
Jetstream::role('employee', 'Employee', [
|
Jetstream::role('employee', 'Employee', [
|
||||||
'projects:view',
|
'projects:view',
|
||||||
|
'tags:view',
|
||||||
'time-entries:view:own',
|
'time-entries:view:own',
|
||||||
'time-entries:create:own',
|
'time-entries:create:own',
|
||||||
'time-entries:update:own',
|
'time-entries:update:own',
|
||||||
|
|||||||
@@ -67,13 +67,16 @@
|
|||||||
"@php artisan model:typer > ./resources/js/types/models.ts"
|
"@php artisan model:typer > ./resources/js/types/models.ts"
|
||||||
],
|
],
|
||||||
"ptest": [
|
"ptest": [
|
||||||
"@php artisan test --parallel --colors=always --stop-on-failure"
|
"@php artisan test --parallel --stop-on-failure"
|
||||||
],
|
],
|
||||||
"test": [
|
"test": [
|
||||||
"@php artisan test --colors=always --stop-on-failure"
|
"@php artisan test --stop-on-failure"
|
||||||
],
|
],
|
||||||
"test:coverage": [
|
"test:coverage": [
|
||||||
"@php artisan test --coverage --colors=always --stop-on-failure"
|
"@php artisan test --coverage --stop-on-failure"
|
||||||
|
],
|
||||||
|
"test:coverage:report": [
|
||||||
|
"@php vendor/bin/phpunit --coverage-html=coverage"
|
||||||
],
|
],
|
||||||
"fix": [
|
"fix": [
|
||||||
"@php pint"
|
"@php pint"
|
||||||
|
|||||||
221
composer.lock
generated
221
composer.lock
generated
@@ -1414,16 +1414,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/actions",
|
"name": "filament/actions",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/actions.git",
|
"url": "https://github.com/filamentphp/actions.git",
|
||||||
"reference": "3e369b846363b990a12b70eb364a37c16100f3cb"
|
"reference": "dc87d0954eb05e04ffd7c94c2fec624fc0fb8cb5"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/actions/zipball/3e369b846363b990a12b70eb364a37c16100f3cb",
|
"url": "https://api.github.com/repos/filamentphp/actions/zipball/dc87d0954eb05e04ffd7c94c2fec624fc0fb8cb5",
|
||||||
"reference": "3e369b846363b990a12b70eb364a37c16100f3cb",
|
"reference": "dc87d0954eb05e04ffd7c94c2fec624fc0fb8cb5",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1432,9 +1432,9 @@
|
|||||||
"filament/infolists": "self.version",
|
"filament/infolists": "self.version",
|
||||||
"filament/notifications": "self.version",
|
"filament/notifications": "self.version",
|
||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/database": "^10.45",
|
"illuminate/database": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"league/csv": "^9.14",
|
"league/csv": "^9.14",
|
||||||
"openspout/openspout": "^4.23",
|
"openspout/openspout": "^4.23",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
@@ -1463,20 +1463,20 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-23T22:25:58+00:00"
|
"time": "2024-02-27T15:32:43+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/filament",
|
"name": "filament/filament",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/panels.git",
|
"url": "https://github.com/filamentphp/panels.git",
|
||||||
"reference": "038ccc8f9f9b7f2599799d35498571bb555b27cb"
|
"reference": "58b62998897e45017bab2beb1d3d27eba4e21cee"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/panels/zipball/038ccc8f9f9b7f2599799d35498571bb555b27cb",
|
"url": "https://api.github.com/repos/filamentphp/panels/zipball/58b62998897e45017bab2beb1d3d27eba4e21cee",
|
||||||
"reference": "038ccc8f9f9b7f2599799d35498571bb555b27cb",
|
"reference": "58b62998897e45017bab2beb1d3d27eba4e21cee",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -1488,16 +1488,16 @@
|
|||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"filament/tables": "self.version",
|
"filament/tables": "self.version",
|
||||||
"filament/widgets": "self.version",
|
"filament/widgets": "self.version",
|
||||||
"illuminate/auth": "^10.45",
|
"illuminate/auth": "^10.45|^11.0",
|
||||||
"illuminate/console": "^10.45",
|
"illuminate/console": "^10.45|^11.0",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/cookie": "^10.45",
|
"illuminate/cookie": "^10.45|^11.0",
|
||||||
"illuminate/database": "^10.45",
|
"illuminate/database": "^10.45|^11.0",
|
||||||
"illuminate/http": "^10.45",
|
"illuminate/http": "^10.45|^11.0",
|
||||||
"illuminate/routing": "^10.45",
|
"illuminate/routing": "^10.45|^11.0",
|
||||||
"illuminate/session": "^10.45",
|
"illuminate/session": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"illuminate/view": "^10.45",
|
"illuminate/view": "^10.45|^11.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9"
|
"spatie/laravel-package-tools": "^1.9"
|
||||||
},
|
},
|
||||||
@@ -1528,33 +1528,33 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-26T20:23:05+00:00"
|
"time": "2024-02-27T15:32:50+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/forms",
|
"name": "filament/forms",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/forms.git",
|
"url": "https://github.com/filamentphp/forms.git",
|
||||||
"reference": "dddd0e14ccd91e98168051aac80899069a27e4ad"
|
"reference": "cb06d6e6f26d7b1837825fce3a4e77764fd346ad"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/forms/zipball/dddd0e14ccd91e98168051aac80899069a27e4ad",
|
"url": "https://api.github.com/repos/filamentphp/forms/zipball/cb06d6e6f26d7b1837825fce3a4e77764fd346ad",
|
||||||
"reference": "dddd0e14ccd91e98168051aac80899069a27e4ad",
|
"reference": "cb06d6e6f26d7b1837825fce3a4e77764fd346ad",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"danharrin/date-format-converter": "^0.3",
|
"danharrin/date-format-converter": "^0.3",
|
||||||
"filament/actions": "self.version",
|
"filament/actions": "self.version",
|
||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"illuminate/console": "^10.45",
|
"illuminate/console": "^10.45|^11.0",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/database": "^10.45",
|
"illuminate/database": "^10.45|^11.0",
|
||||||
"illuminate/filesystem": "^10.45",
|
"illuminate/filesystem": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"illuminate/validation": "^10.45",
|
"illuminate/validation": "^10.45|^11.0",
|
||||||
"illuminate/view": "^10.45",
|
"illuminate/view": "^10.45|^11.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9"
|
"spatie/laravel-package-tools": "^1.9"
|
||||||
},
|
},
|
||||||
@@ -1584,31 +1584,31 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-26T20:23:03+00:00"
|
"time": "2024-02-27T15:32:42+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/infolists",
|
"name": "filament/infolists",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/infolists.git",
|
"url": "https://github.com/filamentphp/infolists.git",
|
||||||
"reference": "049e214811d0511a3b7649376d82683fd710ea1f"
|
"reference": "9c5748d4c5278c7854c53f283e16585002006920"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/049e214811d0511a3b7649376d82683fd710ea1f",
|
"url": "https://api.github.com/repos/filamentphp/infolists/zipball/9c5748d4c5278c7854c53f283e16585002006920",
|
||||||
"reference": "049e214811d0511a3b7649376d82683fd710ea1f",
|
"reference": "9c5748d4c5278c7854c53f283e16585002006920",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"filament/actions": "self.version",
|
"filament/actions": "self.version",
|
||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"illuminate/console": "^10.45",
|
"illuminate/console": "^10.45|^11.0",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/database": "^10.45",
|
"illuminate/database": "^10.45|^11.0",
|
||||||
"illuminate/filesystem": "^10.45",
|
"illuminate/filesystem": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"illuminate/view": "^10.45",
|
"illuminate/view": "^10.45|^11.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9"
|
"spatie/laravel-package-tools": "^1.9"
|
||||||
},
|
},
|
||||||
@@ -1635,29 +1635,29 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-26T20:22:57+00:00"
|
"time": "2024-02-27T15:32:44+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/notifications",
|
"name": "filament/notifications",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/notifications.git",
|
"url": "https://github.com/filamentphp/notifications.git",
|
||||||
"reference": "ae0f5c20df16f5ba28d524f3b2b286753db42cf7"
|
"reference": "e6809dd500ce6b061c3bfb7d19dc03248572611e"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/ae0f5c20df16f5ba28d524f3b2b286753db42cf7",
|
"url": "https://api.github.com/repos/filamentphp/notifications/zipball/e6809dd500ce6b061c3bfb7d19dc03248572611e",
|
||||||
"reference": "ae0f5c20df16f5ba28d524f3b2b286753db42cf7",
|
"reference": "e6809dd500ce6b061c3bfb7d19dc03248572611e",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"filament/actions": "self.version",
|
"filament/actions": "self.version",
|
||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/filesystem": "^10.45",
|
"illuminate/filesystem": "^10.45|^11.0",
|
||||||
"illuminate/notifications": "^10.45",
|
"illuminate/notifications": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9"
|
"spatie/laravel-package-tools": "^1.9"
|
||||||
},
|
},
|
||||||
@@ -1687,32 +1687,31 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-26T20:23:03+00:00"
|
"time": "2024-02-27T15:32:43+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/support",
|
"name": "filament/support",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/support.git",
|
"url": "https://github.com/filamentphp/support.git",
|
||||||
"reference": "2d2c788d36fea3c115628fac12d3638d52059cd6"
|
"reference": "9f375682575c9669ccd24a47617b50308cc1a684"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/support/zipball/2d2c788d36fea3c115628fac12d3638d52059cd6",
|
"url": "https://api.github.com/repos/filamentphp/support/zipball/9f375682575c9669ccd24a47617b50308cc1a684",
|
||||||
"reference": "2d2c788d36fea3c115628fac12d3638d52059cd6",
|
"reference": "9f375682575c9669ccd24a47617b50308cc1a684",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"blade-ui-kit/blade-heroicons": "^2.2.1",
|
"blade-ui-kit/blade-heroicons": "^2.2.1",
|
||||||
"doctrine/dbal": "^3.2",
|
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"illuminate/view": "^10.45",
|
"illuminate/view": "^10.45|^11.0",
|
||||||
"livewire/livewire": "^3.2.3",
|
"livewire/livewire": "^3.2.3",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"ryangjchandler/blade-capture-directive": "^0.2|^0.3",
|
"ryangjchandler/blade-capture-directive": "^0.2|^0.3|^1.0",
|
||||||
"spatie/color": "^1.5",
|
"spatie/color": "^1.5",
|
||||||
"spatie/invade": "^1.0|^2.0",
|
"spatie/invade": "^1.0|^2.0",
|
||||||
"spatie/laravel-package-tools": "^1.9",
|
"spatie/laravel-package-tools": "^1.9",
|
||||||
@@ -1744,32 +1743,32 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-26T20:23:16+00:00"
|
"time": "2024-02-27T15:33:06+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/tables",
|
"name": "filament/tables",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/tables.git",
|
"url": "https://github.com/filamentphp/tables.git",
|
||||||
"reference": "242a3d6e99bc095b225a145e362e07a3fb92fed1"
|
"reference": "2515119b88b68339b64fb4e256375dfb2af5dd4c"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/filamentphp/tables/zipball/242a3d6e99bc095b225a145e362e07a3fb92fed1",
|
"url": "https://api.github.com/repos/filamentphp/tables/zipball/2515119b88b68339b64fb4e256375dfb2af5dd4c",
|
||||||
"reference": "242a3d6e99bc095b225a145e362e07a3fb92fed1",
|
"reference": "2515119b88b68339b64fb4e256375dfb2af5dd4c",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"filament/actions": "self.version",
|
"filament/actions": "self.version",
|
||||||
"filament/forms": "self.version",
|
"filament/forms": "self.version",
|
||||||
"filament/support": "self.version",
|
"filament/support": "self.version",
|
||||||
"illuminate/console": "^10.45",
|
"illuminate/console": "^10.45|^11.0",
|
||||||
"illuminate/contracts": "^10.45",
|
"illuminate/contracts": "^10.45|^11.0",
|
||||||
"illuminate/database": "^10.45",
|
"illuminate/database": "^10.45|^11.0",
|
||||||
"illuminate/filesystem": "^10.45",
|
"illuminate/filesystem": "^10.45|^11.0",
|
||||||
"illuminate/support": "^10.45",
|
"illuminate/support": "^10.45|^11.0",
|
||||||
"illuminate/view": "^10.45",
|
"illuminate/view": "^10.45|^11.0",
|
||||||
"kirschbaum-development/eloquent-power-joins": "^3.0",
|
"kirschbaum-development/eloquent-power-joins": "^3.0",
|
||||||
"php": "^8.1",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9"
|
"spatie/laravel-package-tools": "^1.9"
|
||||||
@@ -1797,11 +1796,11 @@
|
|||||||
"issues": "https://github.com/filamentphp/filament/issues",
|
"issues": "https://github.com/filamentphp/filament/issues",
|
||||||
"source": "https://github.com/filamentphp/filament"
|
"source": "https://github.com/filamentphp/filament"
|
||||||
},
|
},
|
||||||
"time": "2024-02-23T22:26:31+00:00"
|
"time": "2024-02-27T15:33:09+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "filament/widgets",
|
"name": "filament/widgets",
|
||||||
"version": "v3.2.38",
|
"version": "v3.2.39",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/filamentphp/widgets.git",
|
"url": "https://github.com/filamentphp/widgets.git",
|
||||||
@@ -2714,16 +2713,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/framework",
|
"name": "laravel/framework",
|
||||||
"version": "v10.45.1",
|
"version": "v10.46.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/framework.git",
|
"url": "https://github.com/laravel/framework.git",
|
||||||
"reference": "dcf5d1d722b84ad38a5e053289130b6962f830bd"
|
"reference": "5e95946a8283a8d5c015035793f9c61c297e937f"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/framework/zipball/dcf5d1d722b84ad38a5e053289130b6962f830bd",
|
"url": "https://api.github.com/repos/laravel/framework/zipball/5e95946a8283a8d5c015035793f9c61c297e937f",
|
||||||
"reference": "dcf5d1d722b84ad38a5e053289130b6962f830bd",
|
"reference": "5e95946a8283a8d5c015035793f9c61c297e937f",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2916,20 +2915,20 @@
|
|||||||
"issues": "https://github.com/laravel/framework/issues",
|
"issues": "https://github.com/laravel/framework/issues",
|
||||||
"source": "https://github.com/laravel/framework"
|
"source": "https://github.com/laravel/framework"
|
||||||
},
|
},
|
||||||
"time": "2024-02-21T14:07:36+00:00"
|
"time": "2024-02-27T16:46:54+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/jetstream",
|
"name": "laravel/jetstream",
|
||||||
"version": "v4.2.2",
|
"version": "v4.3.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/jetstream.git",
|
"url": "https://github.com/laravel/jetstream.git",
|
||||||
"reference": "7a11a4fb1426855b7132900af5c113a684b820cc"
|
"reference": "c0e19cad88ec5e014746f860bb1559d50474f590"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/jetstream/zipball/7a11a4fb1426855b7132900af5c113a684b820cc",
|
"url": "https://api.github.com/repos/laravel/jetstream/zipball/c0e19cad88ec5e014746f860bb1559d50474f590",
|
||||||
"reference": "7a11a4fb1426855b7132900af5c113a684b820cc",
|
"reference": "c0e19cad88ec5e014746f860bb1559d50474f590",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -2985,7 +2984,7 @@
|
|||||||
"issues": "https://github.com/laravel/jetstream/issues",
|
"issues": "https://github.com/laravel/jetstream/issues",
|
||||||
"source": "https://github.com/laravel/jetstream"
|
"source": "https://github.com/laravel/jetstream"
|
||||||
},
|
},
|
||||||
"time": "2024-01-17T00:49:40+00:00"
|
"time": "2024-02-23T15:35:22+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/passport",
|
"name": "laravel/passport",
|
||||||
@@ -3067,16 +3066,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/prompts",
|
"name": "laravel/prompts",
|
||||||
"version": "v0.1.15",
|
"version": "v0.1.16",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/prompts.git",
|
"url": "https://github.com/laravel/prompts.git",
|
||||||
"reference": "d814a27514d99b03c85aa42b22cfd946568636c1"
|
"reference": "ca6872ab6aec3ab61db3a61f83a6caf764ec7781"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/prompts/zipball/d814a27514d99b03c85aa42b22cfd946568636c1",
|
"url": "https://api.github.com/repos/laravel/prompts/zipball/ca6872ab6aec3ab61db3a61f83a6caf764ec7781",
|
||||||
"reference": "d814a27514d99b03c85aa42b22cfd946568636c1",
|
"reference": "ca6872ab6aec3ab61db3a61f83a6caf764ec7781",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -3118,9 +3117,9 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/laravel/prompts/issues",
|
"issues": "https://github.com/laravel/prompts/issues",
|
||||||
"source": "https://github.com/laravel/prompts/tree/v0.1.15"
|
"source": "https://github.com/laravel/prompts/tree/v0.1.16"
|
||||||
},
|
},
|
||||||
"time": "2023-12-29T22:37:42+00:00"
|
"time": "2024-02-21T19:25:27+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/serializable-closure",
|
"name": "laravel/serializable-closure",
|
||||||
@@ -6289,33 +6288,33 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "ryangjchandler/blade-capture-directive",
|
"name": "ryangjchandler/blade-capture-directive",
|
||||||
"version": "v0.3.0",
|
"version": "v1.0.0",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/ryangjchandler/blade-capture-directive.git",
|
"url": "https://github.com/ryangjchandler/blade-capture-directive.git",
|
||||||
"reference": "62fd2ecb50b938a46025093bcb64fcaddd531f89"
|
"reference": "cb6f58663d97f17bece176295240b740835e14f1"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/62fd2ecb50b938a46025093bcb64fcaddd531f89",
|
"url": "https://api.github.com/repos/ryangjchandler/blade-capture-directive/zipball/cb6f58663d97f17bece176295240b740835e14f1",
|
||||||
"reference": "62fd2ecb50b938a46025093bcb64fcaddd531f89",
|
"reference": "cb6f58663d97f17bece176295240b740835e14f1",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
"illuminate/contracts": "^9.0|^10.0",
|
"illuminate/contracts": "^10.0|^11.0",
|
||||||
"php": "^8.0",
|
"php": "^8.1",
|
||||||
"spatie/laravel-package-tools": "^1.9.2"
|
"spatie/laravel-package-tools": "^1.9.2"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"nunomaduro/collision": "^6.0|^7.0",
|
"nunomaduro/collision": "^7.0|^8.0",
|
||||||
"nunomaduro/larastan": "^2.0",
|
"nunomaduro/larastan": "^2.0",
|
||||||
"orchestra/testbench": "^7.22|^8.0",
|
"orchestra/testbench": "^8.0|^9.0",
|
||||||
"pestphp/pest": "^1.21",
|
"pestphp/pest": "^2.0",
|
||||||
"pestphp/pest-plugin-laravel": "^1.1",
|
"pestphp/pest-plugin-laravel": "^2.0",
|
||||||
"phpstan/extension-installer": "^1.1",
|
"phpstan/extension-installer": "^1.1",
|
||||||
"phpstan/phpstan-deprecation-rules": "^1.0",
|
"phpstan/phpstan-deprecation-rules": "^1.0",
|
||||||
"phpstan/phpstan-phpunit": "^1.0",
|
"phpstan/phpstan-phpunit": "^1.0",
|
||||||
"phpunit/phpunit": "^9.5",
|
"phpunit/phpunit": "^10.0",
|
||||||
"spatie/laravel-ray": "^1.26"
|
"spatie/laravel-ray": "^1.26"
|
||||||
},
|
},
|
||||||
"type": "library",
|
"type": "library",
|
||||||
@@ -6355,7 +6354,7 @@
|
|||||||
],
|
],
|
||||||
"support": {
|
"support": {
|
||||||
"issues": "https://github.com/ryangjchandler/blade-capture-directive/issues",
|
"issues": "https://github.com/ryangjchandler/blade-capture-directive/issues",
|
||||||
"source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v0.3.0"
|
"source": "https://github.com/ryangjchandler/blade-capture-directive/tree/v1.0.0"
|
||||||
},
|
},
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -6363,7 +6362,7 @@
|
|||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"time": "2023-02-14T16:54:54+00:00"
|
"time": "2024-02-26T18:08:49+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "spatie/color",
|
"name": "spatie/color",
|
||||||
@@ -9962,16 +9961,16 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/sail",
|
"name": "laravel/sail",
|
||||||
"version": "v1.28.0",
|
"version": "v1.28.1",
|
||||||
"source": {
|
"source": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/laravel/sail.git",
|
"url": "https://github.com/laravel/sail.git",
|
||||||
"reference": "a05861ca9b04558b1ec1f36cff521a271a259b6c"
|
"reference": "f84e444a3dbc1811803cd2a050bdd54ff6f5eef8"
|
||||||
},
|
},
|
||||||
"dist": {
|
"dist": {
|
||||||
"type": "zip",
|
"type": "zip",
|
||||||
"url": "https://api.github.com/repos/laravel/sail/zipball/a05861ca9b04558b1ec1f36cff521a271a259b6c",
|
"url": "https://api.github.com/repos/laravel/sail/zipball/f84e444a3dbc1811803cd2a050bdd54ff6f5eef8",
|
||||||
"reference": "a05861ca9b04558b1ec1f36cff521a271a259b6c",
|
"reference": "f84e444a3dbc1811803cd2a050bdd54ff6f5eef8",
|
||||||
"shasum": ""
|
"shasum": ""
|
||||||
},
|
},
|
||||||
"require": {
|
"require": {
|
||||||
@@ -10020,7 +10019,7 @@
|
|||||||
"issues": "https://github.com/laravel/sail/issues",
|
"issues": "https://github.com/laravel/sail/issues",
|
||||||
"source": "https://github.com/laravel/sail"
|
"source": "https://github.com/laravel/sail"
|
||||||
},
|
},
|
||||||
"time": "2024-02-20T15:11:00+00:00"
|
"time": "2024-02-23T00:24:47+00:00"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "laravel/telescope",
|
"name": "laravel/telescope",
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ return new class extends Migration
|
|||||||
->cascadeOnUpdate()
|
->cascadeOnUpdate()
|
||||||
->restrictOnDelete();
|
->restrictOnDelete();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('created_at');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,10 @@ return new class extends Migration
|
|||||||
->restrictOnDelete();
|
->restrictOnDelete();
|
||||||
$table->jsonb('tags')->nullable();
|
$table->jsonb('tags')->nullable();
|
||||||
$table->timestamps();
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('start');
|
||||||
|
$table->index('end');
|
||||||
|
$table->index('billable');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Http\Controllers\Api\V1\ProjectController;
|
use App\Http\Controllers\Api\V1\ProjectController;
|
||||||
|
use App\Http\Controllers\Api\V1\TagController;
|
||||||
use App\Http\Controllers\Api\V1\TimeEntryController;
|
use App\Http\Controllers\Api\V1\TimeEntryController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@@ -19,6 +20,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
Route::middleware('auth:api')->prefix('v1')->name('v1.')->group(static function () {
|
Route::middleware('auth:api')->prefix('v1')->name('v1.')->group(static function () {
|
||||||
|
// Project routes
|
||||||
Route::name('projects.')->group(static function () {
|
Route::name('projects.')->group(static function () {
|
||||||
Route::get('/organization/{organization}/projects', [ProjectController::class, 'index'])->name('index');
|
Route::get('/organization/{organization}/projects', [ProjectController::class, 'index'])->name('index');
|
||||||
Route::get('/organization/{organization}/projects/{project}', [ProjectController::class, 'show'])->name('show');
|
Route::get('/organization/{organization}/projects/{project}', [ProjectController::class, 'show'])->name('show');
|
||||||
@@ -27,12 +29,21 @@ Route::middleware('auth:api')->prefix('v1')->name('v1.')->group(static function
|
|||||||
Route::delete('/organization/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy');
|
Route::delete('/organization/{organization}/projects/{project}', [ProjectController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Time entry routes
|
||||||
Route::name('time-entries.')->group(static function () {
|
Route::name('time-entries.')->group(static function () {
|
||||||
Route::get('/organization/{organization}/time-entries', [TimeEntryController::class, 'index'])->name('index');
|
Route::get('/organization/{organization}/time-entries', [TimeEntryController::class, 'index'])->name('index');
|
||||||
Route::post('/organization/{organization}/time-entries', [TimeEntryController::class, 'store'])->name('store');
|
Route::post('/organization/{organization}/time-entries', [TimeEntryController::class, 'store'])->name('store');
|
||||||
Route::put('/organization/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->name('update');
|
Route::put('/organization/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'update'])->name('update');
|
||||||
Route::delete('/organization/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->name('destroy');
|
Route::delete('/organization/{organization}/time-entries/{timeEntry}', [TimeEntryController::class, 'destroy'])->name('destroy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Tag routes
|
||||||
|
Route::name('tags.')->group(static function () {
|
||||||
|
Route::get('/organization/{organization}/tags', [TagController::class, 'index'])->name('index');
|
||||||
|
Route::post('/organization/{organization}/tags', [TagController::class, 'store'])->name('store');
|
||||||
|
Route::put('/organization/{organization}/tags/{tag}', [TagController::class, 'update'])->name('update');
|
||||||
|
Route::delete('/organization/{organization}/tags/{tag}', [TagController::class, 'destroy'])->name('destroy');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -243,6 +243,7 @@ class ProjectEndpointTest extends ApiEndpointTestAbstract
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$response->assertStatus(204);
|
$response->assertStatus(204);
|
||||||
|
$response->assertNoContent();
|
||||||
$this->assertDatabaseMissing(Project::class, [
|
$this->assertDatabaseMissing(Project::class, [
|
||||||
'id' => $project->getKey(),
|
'id' => $project->getKey(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
218
tests/Unit/Endpoint/Api/V1/TagEndpointTest.php
Normal file
218
tests/Unit/Endpoint/Api/V1/TagEndpointTest.php
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Tests\Unit\Endpoint\Api\V1;
|
||||||
|
|
||||||
|
use App\Models\Organization;
|
||||||
|
use App\Models\Tag;
|
||||||
|
use Illuminate\Testing\Fluent\AssertableJson;
|
||||||
|
use Laravel\Passport\Passport;
|
||||||
|
|
||||||
|
class TagEndpointTest extends ApiEndpointTestAbstract
|
||||||
|
{
|
||||||
|
public function test_index_endpoint_fails_if_user_has_no_permission_to_view_tags(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
]);
|
||||||
|
$tags = Tag::factory()->forOrganization($data->organization)->createMany(4);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tags.index', [$data->organization->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_returns_list_of_all_tags_of_organization_ordered_by_created_at_desc_per_default(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:view',
|
||||||
|
]);
|
||||||
|
$tags = Tag::factory()->forOrganization($data->organization)->createMany(4);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.tags.index', [$data->organization->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(4, 'data');
|
||||||
|
$response->assertJson(fn (AssertableJson $json) => $json
|
||||||
|
->has('data')
|
||||||
|
->count('data', 4)
|
||||||
|
->where('data.0.id', $tags->sortByDesc('created_at')->get(0)->getKey())
|
||||||
|
->where('data.1.id', $tags->sortByDesc('created_at')->get(1)->getKey())
|
||||||
|
->where('data.2.id', $tags->sortByDesc('created_at')->get(2)->getKey())
|
||||||
|
->where('data.3.id', $tags->sortByDesc('created_at')->get(3)->getKey())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_tags(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.tags.store', [$data->organization->getKey()]), [
|
||||||
|
'name' => 'Test Tag',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_store_endpoint_creates_new_tag(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:create',
|
||||||
|
]);
|
||||||
|
$tagFake = Tag::factory()->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.tags.store', [$data->organization->getKey()]), [
|
||||||
|
'name' => $tagFake->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$response->assertJson(fn (AssertableJson $json) => $json
|
||||||
|
->has('data')
|
||||||
|
->where('data.name', $tagFake->name)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_fails_if_user_has_no_permission_to_update_tags(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
]);
|
||||||
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
||||||
|
$tagFake = Tag::factory()->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.tags.update', [$data->organization->getKey(), $tag->getKey()]), [
|
||||||
|
'name' => $tagFake->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_fails_if_user_is_not_part_of_tag_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:update',
|
||||||
|
]);
|
||||||
|
$otherOrganization = Organization::factory()->create();
|
||||||
|
$tag = Tag::factory()->forOrganization($otherOrganization)->create();
|
||||||
|
$tagFake = Tag::factory()->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.tags.update', [$data->organization->getKey(), $tag->getKey()]), [
|
||||||
|
'name' => $tagFake->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$this->assertDatabaseHas(Tag::class, [
|
||||||
|
'id' => $tag->getKey(),
|
||||||
|
'name' => $tag->name,
|
||||||
|
'organization_id' => $otherOrganization->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_update_endpoint_updates_tag(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:update',
|
||||||
|
]);
|
||||||
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
||||||
|
$tagFake = Tag::factory()->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->putJson(route('api.v1.tags.update', [$data->organization->getKey(), $tag->getKey()]), [
|
||||||
|
'name' => $tagFake->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson(fn (AssertableJson $json) => $json
|
||||||
|
->has('data')
|
||||||
|
->where('data.name', $tagFake->name)
|
||||||
|
);
|
||||||
|
$this->assertDatabaseHas(Tag::class, [
|
||||||
|
'name' => $tagFake->name,
|
||||||
|
'organization_id' => $data->organization->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_destroy_endpoint_fails_if_user_has_no_permission_to_delete_tags(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
]);
|
||||||
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->deleteJson(route('api.v1.tags.destroy', [$data->organization->getKey(), $tag->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_destroy_endpoint_fails_if_user_is_not_part_of_tag_organization(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:delete',
|
||||||
|
]);
|
||||||
|
$otherOrganization = Organization::factory()->create();
|
||||||
|
$tag = Tag::factory()->forOrganization($otherOrganization)->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->deleteJson(route('api.v1.tags.destroy', [$data->organization->getKey(), $tag->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(403);
|
||||||
|
$this->assertDatabaseHas(Tag::class, [
|
||||||
|
'id' => $tag->getKey(),
|
||||||
|
'name' => $tag->name,
|
||||||
|
'organization_id' => $otherOrganization->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_destroy_endpoint_deletes_tag(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'tags:delete',
|
||||||
|
]);
|
||||||
|
$tag = Tag::factory()->forOrganization($data->organization)->create();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->deleteJson(route('api.v1.tags.destroy', [$data->organization->getKey(), $tag->getKey()]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(204);
|
||||||
|
$response->assertNoContent();
|
||||||
|
$this->assertDatabaseMissing(Tag::class, [
|
||||||
|
'id' => $tag->getKey(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,8 +6,12 @@ namespace Tests\Unit\Endpoint\Api\V1;
|
|||||||
|
|
||||||
use App\Models\TimeEntry;
|
use App\Models\TimeEntry;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
use Illuminate\Testing\Fluent\AssertableJson;
|
||||||
use Laravel\Passport\Passport;
|
use Laravel\Passport\Passport;
|
||||||
|
use TiMacDonald\Log\LogEntry;
|
||||||
|
|
||||||
class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
||||||
{
|
{
|
||||||
@@ -95,7 +99,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_index_endpoint_returns_time_entries_for_all_users_in_organization(): void
|
public function test_index_endpoint_returns_time_entries_for_all_users_in_organization_default_sort_by_start_date_desc(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
$data = $this->createUserWithPermission([
|
$data = $this->createUserWithPermission([
|
||||||
@@ -105,7 +109,15 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$data->organization->users()->attach($user, [
|
$data->organization->users()->attach($user, [
|
||||||
'role' => 'employee',
|
'role' => 'employee',
|
||||||
]);
|
]);
|
||||||
$timeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create();
|
$timeEntry1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create([
|
||||||
|
'start' => Carbon::now()->subDay(),
|
||||||
|
]);
|
||||||
|
$timeEntry2 = TimeEntry::factory()->forOrganization($data->organization)->forUser($user)->create([
|
||||||
|
'start' => Carbon::now()->subDays(2),
|
||||||
|
]);
|
||||||
|
$timeEntry3 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->create([
|
||||||
|
'start' => Carbon::now()->subDays(3),
|
||||||
|
]);
|
||||||
Passport::actingAs($data->user);
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
// Act
|
// Act
|
||||||
@@ -113,7 +125,182 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$response->assertStatus(200);
|
$response->assertStatus(200);
|
||||||
$response->assertJsonPath('data.0.id', $timeEntry->getKey());
|
$response->assertJsonPath('data.0.id', $timeEntry1->getKey());
|
||||||
|
$response->assertJsonPath('data.1.id', $timeEntry2->getKey());
|
||||||
|
$response->assertJsonPath('data.2.id', $timeEntry3->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_returns_only_active_time_entries(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->create();
|
||||||
|
$nonActiveTimeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'active' => true,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(1, 'data');
|
||||||
|
$response->assertJsonPath('data.0.id', $activeTimeEntry->getKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_less_time_entries_than_limit(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$timeEntries = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->createMany(3);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'only_full_dates' => true,
|
||||||
|
'limit' => 5,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(3, 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_more_time_entries_than_limit(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now()->subDay()->endOfDay())
|
||||||
|
->createMany(3);
|
||||||
|
$timeEntriesDay2 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDays(2)->startOfDay(), Carbon::now()->subDays(2)->endOfDay())
|
||||||
|
->createMany(3);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'only_full_dates' => true,
|
||||||
|
'limit' => 5,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(3, 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_filter_only_full_dates_returns_time_entries_for_the_whole_day_case_more_time_entries_in_latest_day_than_limit(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$timeEntriesDay1 = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now()->subDay()->endOfDay())
|
||||||
|
->createMany(7);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'only_full_dates' => true,
|
||||||
|
'limit' => 5,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Log::assertLogged(fn (LogEntry $log) => $log->level === 'warning'
|
||||||
|
&& $log->message === 'User has has more than 5 time entries on one date'
|
||||||
|
);
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonCount(7, 'data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_before_filter_returns_time_entries_before_date(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now())
|
||||||
|
->createMany(3);
|
||||||
|
$timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDays(2)->startOfDay(), Carbon::now()->subDays(2)->endOfDay())
|
||||||
|
->createMany(3);
|
||||||
|
$timeEntriesDirectlyBeforeLimit = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->create([
|
||||||
|
'start' => Carbon::now()->subDays(2)->endOfDay(),
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'before' => Carbon::now()->subDay()->startOfDay()->toIso8601ZuluString(),
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson(fn (AssertableJson $json) => $json
|
||||||
|
->has('data')
|
||||||
|
->count('data', 4)
|
||||||
|
->where('data.0.id', $timeEntriesDirectlyBeforeLimit->getKey())
|
||||||
|
->where('data.1.id', $timeEntriesBefore->sortByDesc('start')->get(0)->getKey())
|
||||||
|
->where('data.2.id', $timeEntriesBefore->sortByDesc('start')->get(1)->getKey())
|
||||||
|
->where('data.3.id', $timeEntriesBefore->sortByDesc('start')->get(2)->getKey())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_index_endpoint_after_filter_returns_time_entries_after_date(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:view:own',
|
||||||
|
]);
|
||||||
|
$timeEntriesAfter = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->startOfDay(), Carbon::now())
|
||||||
|
->createMany(3);
|
||||||
|
$timeEntriesBefore = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->startBetween(Carbon::now()->subDay()->startOfDay(), Carbon::now()->subDay()->endOfDay())
|
||||||
|
->createMany(3);
|
||||||
|
$timeEntriesDirectlyAfterLimit = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)
|
||||||
|
->create([
|
||||||
|
'start' => Carbon::now()->startOfDay(),
|
||||||
|
]);
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->getJson(route('api.v1.time-entries.index', [
|
||||||
|
$data->organization->getKey(),
|
||||||
|
'after' => Carbon::now()->subDay()->endOfDay()->toIso8601ZuluString(), // yesterday
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]));
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJson(fn (AssertableJson $json) => $json
|
||||||
|
->has('data')
|
||||||
|
->count('data', 4)
|
||||||
|
->where('data.0.id', $timeEntriesAfter->sortByDesc('start')->get(0)->getKey())
|
||||||
|
->where('data.1.id', $timeEntriesAfter->sortByDesc('start')->get(1)->getKey())
|
||||||
|
->where('data.2.id', $timeEntriesAfter->sortByDesc('start')->get(2)->getKey())
|
||||||
|
->where('data.3.id', $timeEntriesDirectlyAfterLimit->getKey())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_time_entries(): void
|
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_time_entries(): void
|
||||||
@@ -127,8 +314,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $data->user->getKey(),
|
'user_id' => $data->user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -138,6 +325,31 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
$response->assertStatus(403);
|
$response->assertStatus(403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_store_endpoint_fails_if_user_already_has_active_time_entry_and_tries_to_start_new_one(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:create:own',
|
||||||
|
]);
|
||||||
|
$activeTimeEntry = TimeEntry::factory()->forOrganization($data->organization)->forUser($data->user)->active()->create();
|
||||||
|
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->withTags($data->organization)->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
|
'description' => $timeEntryFake->description,
|
||||||
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
|
'end' => null,
|
||||||
|
'tags' => $timeEntryFake->tags,
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
'task_id' => $timeEntryFake->task_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(400);
|
||||||
|
$response->assertJsonPath('error', true);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_store_endpoint_creates_new_time_entry_for_current_user(): void
|
public function test_store_endpoint_creates_new_time_entry_for_current_user(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -150,8 +362,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $data->user->getKey(),
|
'user_id' => $data->user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -166,6 +378,30 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_store_endpoint_creates_new_time_entry_with_minimal_fields(): void
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
$data = $this->createUserWithPermission([
|
||||||
|
'time-entries:create:own',
|
||||||
|
]);
|
||||||
|
$timeEntryFake = TimeEntry::factory()->forOrganization($data->organization)->make();
|
||||||
|
Passport::actingAs($data->user);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
$response->assertStatus(201);
|
||||||
|
$this->assertDatabaseHas(TimeEntry::class, [
|
||||||
|
'id' => $response->json('data.id'),
|
||||||
|
'user_id' => $data->user->getKey(),
|
||||||
|
'task_id' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_time_entries_for_others(): void
|
public function test_store_endpoint_fails_if_user_has_no_permission_to_create_time_entries_for_others(): void
|
||||||
{
|
{
|
||||||
// Arrange
|
// Arrange
|
||||||
@@ -182,8 +418,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $otherUser->getKey(),
|
'user_id' => $otherUser->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -209,8 +445,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
$response = $this->postJson(route('api.v1.time-entries.store', [$data->organization->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $otherUser->getKey(),
|
'user_id' => $otherUser->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -237,8 +473,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $data->user->getKey(),
|
'user_id' => $data->user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -264,8 +500,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $data->user->getKey(),
|
'user_id' => $data->user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -292,8 +528,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $user->getKey(),
|
'user_id' => $user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -316,8 +552,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $data->user->getKey(),
|
'user_id' => $data->user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -349,8 +585,8 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
// Act
|
// Act
|
||||||
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
$response = $this->putJson(route('api.v1.time-entries.update', [$data->organization->getKey(), $timeEntry->getKey()]), [
|
||||||
'description' => $timeEntryFake->description,
|
'description' => $timeEntryFake->description,
|
||||||
'start' => $timeEntryFake->start->toIso8601String(),
|
'start' => $timeEntryFake->start->toIso8601ZuluString(),
|
||||||
'end' => $timeEntryFake->end->toIso8601String(),
|
'end' => $timeEntryFake->end->toIso8601ZuluString(),
|
||||||
'tags' => $timeEntryFake->tags,
|
'tags' => $timeEntryFake->tags,
|
||||||
'user_id' => $user->getKey(),
|
'user_id' => $user->getKey(),
|
||||||
'task_id' => $timeEntryFake->task_id,
|
'task_id' => $timeEntryFake->task_id,
|
||||||
@@ -448,6 +684,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$response->assertStatus(204);
|
$response->assertStatus(204);
|
||||||
|
$response->assertNoContent();
|
||||||
$this->assertDatabaseMissing(TimeEntry::class, [
|
$this->assertDatabaseMissing(TimeEntry::class, [
|
||||||
'id' => $timeEntry->getKey(),
|
'id' => $timeEntry->getKey(),
|
||||||
]);
|
]);
|
||||||
@@ -471,6 +708,7 @@ class TimeEntryEndpointTest extends ApiEndpointTestAbstract
|
|||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
$response->assertStatus(204);
|
$response->assertStatus(204);
|
||||||
|
$response->assertNoContent();
|
||||||
$this->assertDatabaseMissing(TimeEntry::class, [
|
$this->assertDatabaseMissing(TimeEntry::class, [
|
||||||
'id' => $timeEntry->getKey(),
|
'id' => $timeEntry->getKey(),
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user