Files
cattr/app/Http/Controllers/Api/IntervalController.php
Noor E Ilahi 7ccf44f7da first commit
2026-01-09 12:54:53 +05:30

1047 lines
35 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Http\Controllers\Api;
use App\Contracts\ScreenshotService;
use App\Enums\ScreenshotsState;
use App\Http\Requests\Interval\BulkDestroyTimeIntervalRequest;
use App\Http\Requests\Interval\BulkEditTimeIntervalRequest;
use App\Http\Requests\Interval\CreateTimeIntervalRequest;
use App\Http\Requests\Interval\DestroyTimeIntervalRequest;
use App\Http\Requests\Interval\EditTimeIntervalRequest;
use App\Http\Requests\Interval\IntervalTasksRequest;
use App\Http\Requests\Interval\IntervalTotalRequest;
use App\Http\Requests\Interval\ListIntervalRequest;
use App\Http\Requests\Interval\PutScreenshotRequest;
use App\Http\Requests\Interval\ScreenshotRequest;
use App\Http\Requests\Interval\ShowIntervalRequest;
use App\Http\Requests\Interval\TrackAppRequest;
use App\Http\Requests\Interval\UploadOfflineIntervalsRequest;
use App\Http\Requests\Interval\UploadOfflineScreenshotsRequest;
use App\Jobs\AssignAppsToTimeInterval;
use App\Models\Task;
use App\Models\TrackedApplication;
use App\Models\User;
use CatEvent;
use Filter;
use App\Models\TimeInterval;
use Carbon\Carbon;
use Exception;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Arr;
use MessagePack\MessagePack;
use phpseclib3\Crypt\RSA;
use Settings;
use Spatie\TemporaryDirectory\TemporaryDirectory;
use Storage;
use Str;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Throwable;
use Validator;
use ZipArchive;
class IntervalController extends ItemController
{
protected const MODEL = TimeInterval::class;
public function __construct(protected ScreenshotService $screenshotService)
{
}
/**
* @param ListIntervalRequest $request
*
* @return JsonResponse
* @throws Exception
*/
/**
* @api {post} /time-intervals/list List
* @apiDescription Get list of Time Intervals
*
* @apiVersion 4.0.0
* @apiName List
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_list
* @apiPermission time_intervals_full_access
* @apiUse TimeIntervalObject
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function index(ListIntervalRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($filters) use ($request) {
if ($request->get('project_id')) {
$filters['task.project_id'] = $request->get('project_id');
}
return $filters;
});
return $this->_index($request);
}
/**
* @param ShowIntervalRequest $request
*
* @return JsonResponse
* @throws Throwable
* @api {post} /time-intervals/show Show
* @apiDescription Show Time Interval
*
* @apiVersion 4.0.0
* @apiName Show
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_show
* @apiPermission time_intervals_full_access
*
* @apiParam {Integer} id Time Interval id
*
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiUse TimeIntervalObject
* @apiUse 400Error
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
* @apiUse ForbiddenError
* @apiUse ValidationError
*/
public function show(ShowIntervalRequest $request): JsonResponse
{
return $this->_show($request);
}
/**
* @apiDeprecated since 1.0.0
* @api {post} /time-intervals/bulk-create Bulk Create
* @apiDescription Create Time Intervals
*
* @apiVersion 1.0.0
* @apiName Bulk Create
* @apiGroup Time Interval
*/
/**
* @param EditTimeIntervalRequest $request
*
* @return JsonResponse
* @throws Throwable
*/
public function edit(EditTimeIntervalRequest $request): JsonResponse
{
Filter::listen(Filter::getRequestFilterName(), static function ($requestData) {
$requestData['start_at'] = Carbon::parse($requestData['start_at'])->setTimezone('UTC')->toDateTimeString();
$requestData['end_at'] = Carbon::parse($requestData['end_at'])->setTimezone('UTC')->toDateTimeString();
});
return $this->_edit($request);
}
/**
* @throws Exception
* @api {get,post} /time-intervals/count Count
* @apiDescription Count Time Intervals
*
* @apiVersion 1.0.0
* @apiName Count
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiSuccess {String} total Amount of users that we have
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "total": 2
* }
*
* @apiUse 400Error
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function count(ListIntervalRequest $request): JsonResponse
{
return $this->_count($request);
}
/**
* @api {post} /time-intervals/edit Edit
* @apiDescription Edit Time Interval
*
* @apiVersion 1.0.0
* @apiName Edit
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_edit
* @apiPermission time_intervals_full_access
*
* @apiParam {Integer} id Time Interval id
*
* @apiUse TimeIntervalParams
*
* @apiSuccess {Object} res TimeInterval
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "res": {
* "id":1,
* "task_id":1,
* "start_at":"2018-10-03 10:00:00",
* "end_at":"2018-10-03 10:00:00",
* "created_at":"2018-10-15 05:50:39",
* "updated_at":"2018-10-15 05:50:43",
* "deleted_at":null,
* "activity_fill": 42,
* "mouse_fill": 43,
* "keyboard_fill": 43,
* "user_id":1
* }
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ItemNotFoundError
*/
/**
* @throws Throwable
* @api {post} /time-intervals/remove Destroy
* @apiDescription Destroy Time Interval
*
* @apiVersion 4.0.0
* @apiName Destroy
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_remove
* @apiPermission time_intervals_full_access
*
* @apiParam {Integer} id ID of the target interval
*
* @apiParamExample {json} Request Example
* {
* "id": 1
* }
*
* @apiSuccess {String} message Destroy status
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*/
public function destroy(DestroyTimeIntervalRequest $request): JsonResponse
{
return $this->_destroy($request);
}
/**
* @param BulkEditTimeIntervalRequest $request
*
* @return JsonResponse
* @throws Exception
*/
/**
* @api {post} /time-intervals/bulk-edit Bulk Edit
* @apiDescription Multiple Edit TimeInterval to assign tasks to them
*
* @apiVersion 4.0.0
* @apiName Bulk Edit
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiParam {Object[]} intervals Time Intervals to edit
* @apiParam {Integer} intervals.id Time Interval ID
* @apiParam {Integer} intervals.task_id Task ID
*
* @apiParamExample {json} Request Example
* {
* "intervals": [
* {
* "id": 12,
* "task_id": 12
* },
* {
* "id": 13,
* "task_id": 16
* }
* ]
* }
*
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiSuccess {String} message Message from server
* @apiSuccess {Integer[]} updated Updated intervals
* @apiSuccess {Integer[]} not_found Not found intervals
*
* @apiSuccessExample {json} Not all intervals updated Response Example
* HTTP/1.1 207 Multi-Status
* {
* "message": "Some intervals have not been updated",
* "updated": [12, 123, 45],
* "not_found": [154, 77, 66]
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function bulkEdit(BulkEditTimeIntervalRequest $request): JsonResponse
{
$intervalsData = collect(
Filter::process(Filter::getRequestFilterName(), $request->validated())['intervals']
);
$intervals = $this->getQuery([
'where' => [
'id' => ['in', $intervalsData->pluck('id')->toArray()]
]
])->get()->toBase();
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$intervals, $request]);
$intervals->each(static fn(Model $item) => Filter::process(
Filter::getActionFilterName(),
$item->fill(
Arr::only(
$intervalsData->where('id', $item->id)->first() ?: [],
'task_id'
)
)
));
CatEvent::dispatch(Filter::getAfterActionEventName(), [$intervals, $request]);
$intervals->each(static function (Model $item) {
$item->save();
});
return responder()->success()->respond(204);
}
/**
* @throws Exception
* @api {post} /time-intervals/bulk-remove Bulk Destroy
* @apiDescription Multiple Destroy TimeInterval
*
* @apiVersion 4.0.0
* @apiName Bulk Destroy
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_bulk_remove
* @apiPermission time_intervals_full_access
*
* @apiParam {Integer[]} intervals Intervals ID to delete
*
* @apiParamExample {json} Request Example
* {
* "intervals": [1]
* }
*
* @apiSuccess {String} message Message from server
* @apiSuccess {Integer[]} removed Removed intervals
* @apiSuccess {Integer[]} not_found Not found intervals
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 204 No Content
* {
* }
*
* @apiSuccessExample {json} Not all intervals removed Response Example
* HTTP/1.1 207 Multi-Status
* {
* "message": "Some intervals have not been removed",
* "removed": [12, 123, 45],
* "not_found": [154, 77, 66]
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*
*/
public function bulkDestroy(BulkDestroyTimeIntervalRequest $request): JsonResponse
{
$intervalIds = Filter::process(Filter::getRequestFilterName(), $request->validated())['intervals'];
$itemsQuery = $this->getQuery(['where' => ['id' => ['in', $intervalIds]]]);
CatEvent::dispatch(Filter::getBeforeActionEventName(), [$intervalIds, $request]);
$itemsQuery->eachById(static function ($item) {
Filter::process(Filter::getActionFilterName(), $item)->delete();
});
CatEvent::dispatch(Filter::getAfterActionEventName(), [$intervalIds, $request]);
return responder()->success()->respond(204);
}
/**
* @api {put} /time-intervals/app Adds the current users ID
* @apiDescription Adds the current users ID
*
* @apiVersion 4.0.0
* @apiName Adds the current users ID
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiParam {Integer[]} intervals Intervals ID to delete
*
* @apiParamExample {json} Request Example
* {
* "executable": "1"
* }
*
* @apiSuccess {String} executable Indicates if the interval is executable.
* @apiSuccess {Integer[]} user_id ID of the user
* @apiSuccess {Integer[]} updated_at Last update timestamp
* @apiSuccess {Integer[]} created_at Creation timestamp
* @apiSuccess {Integer[]} id ID of the time interval
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200
* {
* "executable": "1",
* "user_id": 1,
* "updated_at": "2024-08-17T12:32:11.000000Z",
* "created_at": "2024-08-17T12:32:11.000000Z",
* "id": 1
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*
*/
public function trackApp(TrackAppRequest $request): JsonResponse
{
return responder()->success(
TrackedApplication::create(
array_merge(
$request->validated(),
['user_id' => auth()->user()->id]
)
)
)->respond();
}
/**
* @throws Throwable
* @api {post} /time-intervals/create Create
* @apiDescription Create Time Interval
*
* @apiVersion 4.0.0
* @apiName Create
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiPermission time_intervals_create
* @apiPermission time_intervals_full_access
*
* @apiParam {Integer} task_id Task id
* @apiParam {Integer} user_id User id
* @apiParam {String} start_at Interval time start
* @apiParam {String} end_at Interval time end
* @apiParam {String} timezone Interval time end
* @apiParam {Boolean} is_manual Indicates whether the time was logged manually (true) or automatically
* @apiParam {File} screenshot Image
*
* @apiParamExample {json} Request Example
* {
* "task_id": 59,
* "user_id": 7,
* "start_at": "2024-08-16T18:00:00.000Z",
* "end_at": "2024-08-16T18:10:00.000Z",
* "timezone": "Asia/Omsk",
* "is_manual": true
* }
*
* @apiSuccess {Object} interval Interval
*
* @apiUse TimeIntervalObject
*
* @apiSuccess {Integer} id ID of the time interval
* @apiSuccess {Integer} task_id ID of the task
* @apiSuccess {Integer} user_id ID of the user
* @apiSuccess {String} start_at Start timestamp of the time interval
* @apiSuccess {String} end_at End timestamp of the time interval
* @apiSuccess {String} created_at Creation timestamp
* @apiSuccess {String} updated_at Last update timestamp
* @apiSuccess {Boolean} is_manual Indicates whether the time was logged manually (true) or automatically
* @apiSuccess {Integer} has_screenshot Indicates if there is a screenshot for this interval
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "id": 22578,
* "task_id": 59,
* "user_id": 7,
* "start_at": "2024-08-16T18:00:00.000000Z",
* "end_at": "2024-08-16T18:10:00.000000Z",
* "created_at": "2024-08-16T20:07:14.000000Z",
* "updated_at": "2024-08-16T20:07:14.000000Z",
* "is_manual": true,
* "has_screenshot": true
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse UnauthorizedError
* @apiUse ForbiddenError
*/
public function create(CreateTimeIntervalRequest $request): JsonResponse
{
Filter::listen(
Filter::getRequestFilterName(),
static function (array $requestData) {
$timezone = Settings::scope('core')->get('timezone', 'UTC');
$requestData['start_at'] = Carbon::parse($requestData['start_at'])->setTimezone($timezone);
$requestData['end_at'] = Carbon::parse($requestData['end_at'])->setTimezone($timezone);
return $requestData;
}
);
$screenshotService = $this->screenshotService;
if ($request->hasFile('screenshot') && optional($request->file('screenshot'))->isValid()) {
$path = $request->file('screenshot')->store('tmp');
CatEvent::listen(
Filter::getAfterActionEventName(),
static function (TimeInterval $interval) use ($path, $screenshotService) {
$projScreenshotsState = $interval->task->project->screenshots_state;
$mustCapture = $projScreenshotsState === ScreenshotsState::REQUIRED;
$optionalCapture = $projScreenshotsState === ScreenshotsState::OPTIONAL;
if ($mustCapture
|| ($optionalCapture && $interval->user->screenshots_state === ScreenshotsState::REQUIRED)
) {
$screenshotService->saveScreenshot(Storage::path($path), $interval);
dispatch(static fn() => Storage::delete($path))->delay(now()->addMinute());
}
}
);
}
CatEvent::listen(
Filter::getAfterActionEventName(),
static function ($data) {
if (User::find($data['user_id'])->web_and_app_monitoring) {
AssignAppsToTimeInterval::dispatch($data);
}
}
);
return $this->_create($request);
}
public function showScreenshot(ScreenshotRequest $request, TimeInterval $interval): BinaryFileResponse
{
$path = $this->screenshotService->getScreenshotPath($interval);
if (!Storage::exists($path)) {
abort(404);
}
$fullPath = Storage::path($path);
return response()->file($fullPath);
}
public function showThumbnail(ScreenshotRequest $request, TimeInterval $interval): BinaryFileResponse
{
$path = $this->screenshotService->getThumbPath($interval);
if (!Storage::exists($path)) {
abort(404);
}
$fullPath = Storage::path($path);
return response()->file($fullPath);
}
public function putScreenshot(PutScreenshotRequest $request, TimeInterval $interval): JsonResponse
{
$data = $request->validated();
abort_if(
Storage::exists($this->screenshotService->getScreenshotPath($interval)),
409,
__('Screenshot for requested interval already exists')
);
$projScreenshotsState = $interval->task->project->screenshots_state;
$mustCapture = $projScreenshotsState === ScreenshotsState::REQUIRED;
$optionalCapture = $projScreenshotsState === ScreenshotsState::OPTIONAL;
if ($mustCapture
|| ($optionalCapture && $interval->user->screenshots_state === ScreenshotsState::REQUIRED)
) {
$this->screenshotService->saveScreenshot($data['screenshot'], $interval);
} else {
abort(
409,
__('Screenshots disabled for interval\'s project')
);
}
return responder()->success()->respond(204);
}
public function uploadOfflineIntervals(UploadOfflineIntervalsRequest $request): JsonResponse
{
/**
* @var UploadedFile $file
*/
$file = $request->validated()['file'];
$zip = new ZipArchive;
$zipOpenResult = $zip->open($file->path());
abort_if(
$zipOpenResult === false || (is_int($zipOpenResult) && $zipOpenResult > 0),
400,
__('Cannot open file.' . is_int($zipOpenResult) ? " ZipArchive error code: $zipOpenResult" : ""),
);
$temporaryDirectory = (new TemporaryDirectory())->deleteWhenDestroyed()->force()->create();
$zip->extractTo($temporaryDirectory->path());
$zip->close();
$privateKey = RSA::load(Settings::scope('core.offline-sync')->get('private_key'));
$intervalsContent = file_get_contents($temporaryDirectory->path('Intervals'));
$digestContent = file_get_contents($temporaryDirectory->path('EncryptedDigest'));
abort_if(
$intervalsContent === false || $digestContent === false,
400,
__('Unable to read content of Intervals or its EncryptedDigest')
);
$digest = $privateKey->withPadding(RSA::ENCRYPTION_OAEP)->withHash('sha256')->decrypt($digestContent);
$actualDigest = openssl_digest($intervalsContent, 'sha256');
abort_if(
$digest === false || $actualDigest === false || $digest !== $actualDigest,
400,
__('Unable to verify Intervals digest')
);
$intervals = MessagePack::unpack($intervalsContent);
$timezone = Settings::scope('core')->get('timezone', 'UTC');
$validatorClass = new CreateTimeIntervalRequest();
$creationResult = [];
abort_if(
count($intervals) === 0,
400,
__('File contains 0 intervals')
);
$user = User::whereId($intervals[0]['user_id'])->first(['id', 'email', 'full_name', 'screenshots_state']);
abort_if(
$user === null,
400,
__('User not found')
);
$canCreate = fn($interval) => $request->user()->can(
'create',
[
TimeInterval::class,
$interval['user_id'],
$interval['task_id'],
false,
],
);
$tasksScreenshotsState = Task::with('project:id,screenshots_state')
->whereIn('id', collect($intervals)->pluck('task_id'))
->select(['id', 'project_id'])
->get()
->mapWithKeys(fn($item)=>[$item->id => $item->project->screenshots_state])
->toArray();
$globalScreenshotsState = ScreenshotsState::withGlobalOverrides(null) ?? ScreenshotsState::OPTIONAL;
foreach ($intervals as $interval) {
$interval['user'] = $user;
if ($canCreate($interval) === false) {
$creationResult[] = [
'interval' => $interval,
'message' => __('validation.offline-sync.cannot_create_interval'),
'success' => false
];
continue;
}
$screenshotIdValidationRule = $interval['has_screenshot'] ? ['screenshot_id' => 'required|uuid'] : [];
if ($interval['has_screenshot']) {
$mustNotCapture = $globalScreenshotsState === ScreenshotsState::FORBIDDEN;
$optionalCapture = $globalScreenshotsState === ScreenshotsState::OPTIONAL
&& isset($tasksScreenshotsState[$interval['task_id']]);
if ($optionalCapture && $tasksScreenshotsState[$interval['task_id']] === ScreenshotsState::FORBIDDEN) {
$mustNotCapture = true;
} elseif ($optionalCapture
&& $tasksScreenshotsState[$interval['task_id']] === ScreenshotsState::OPTIONAL) {
$mustNotCapture = $user->screenshots_state === ScreenshotsState::FORBIDDEN;
}
if ($mustNotCapture) {
$screenshotIdValidationRule = [];
}
}
$intervalValidator = Validator::make(
$interval,
array_merge(
$validatorClass->getRules($interval['user_id'], $interval['start_at'], $interval['end_at']),
$screenshotIdValidationRule
)
);
if ($intervalValidator->fails()) {
$creationResult[] = [
'interval' => $interval,
'message' => $intervalValidator->errors(),
'success' => false
];
continue;
}
$requestData = $intervalValidator->validated();
$requestData['start_at'] = Carbon::parse($requestData['start_at'])->setTimezone($timezone);
$requestData['end_at'] = Carbon::parse($requestData['end_at'])->setTimezone($timezone);
TimeInterval::create($requestData);
$creationResult[] = [
'interval' => $interval,
'message' => __('validation.offline-sync.time_interval_added'),
'success' => true
];
}
return responder()->success($creationResult)->respond();
}
public function uploadOfflineScreenshots(UploadOfflineScreenshotsRequest $request): JsonResponse
{
/**
* @var UploadedFile $file
*/
$file = $request->validated()['file'];
$zip = new ZipArchive;
$zipOpenResult = $zip->open($file->path());
abort_if(
$zipOpenResult === false || (is_int($zipOpenResult) && $zipOpenResult > 0),
400,
__('Cannot open file.' . is_int($zipOpenResult) ? " ZipArchive error code: $zipOpenResult" : ""),
);
$temporaryDirectory = (new TemporaryDirectory())
->location(Storage::disk('local')->path('tmp'))->force()->create();
$zip->extractTo($temporaryDirectory->path());
$zip->close();
$dirPath = Str::of($temporaryDirectory->path())->match('/tmp.+/');
dispatch(static fn() => $temporaryDirectory->delete())->delay(now()->addHour());
$allScreenshots = Storage::disk('local')->files($dirPath);
$creationResult = [];
$screenshotService = $this->screenshotService;
foreach ($allScreenshots as $screenshotPath) {
$pathArr = Str::of($screenshotPath)->match('/\d_.+/')->split('/_/');
abort_if(
count($pathArr) !== 2 || (count($pathArr) === 2 && !Str::isUuid($pathArr[1])),
400,
__('Wrong screenshot file name')
);
[$userId, $screenshotId] = $pathArr;
$interval = TimeInterval::where('user_id', $userId)->where('screenshot_id', $screenshotId)->first();
if ($interval === null) {
$creationResult[] = [
'interval' => $interval,
'user_id' => $userId,
'screenshot_id' => $screenshotId,
'message' => __('validation.offline-sync.cannot_find_interval'),
'success' => false
];
continue;
}
try {
dispatch(static function () use ($screenshotService, $interval, $screenshotPath) {
$screenshotService->saveScreenshot(Storage::path($screenshotPath), $interval);
$interval->screenshot_id = null;
$interval->save();
});
$creationResult[] = [
'interval' => $interval,
'user_id' => $userId,
'screenshot_id' => $screenshotId,
'message' => __('validation.offline-sync.screenshot_attached'),
'success' => true
];
} catch (\Exception $e) {
$creationResult[] = [
'interval' => $interval,
'user_id' => $userId,
'screenshot_id' => $screenshotId,
'message' => __('validation.offline-sync.screenshot_not_attached'),
'success' => false
];
\Log::error($e);
}
}
return responder()->success($creationResult)->respond();
}
/**
* Display a total of time
* @param IntervalTotalRequest $request
* @return JsonResponse
* @throws Exception
*/
/**
* @api {any} /time/total Calculate total time interval
* @apiDescription Calculate the total time spent within the specified start and end intervals for a given user.
*
* @apiVersion 4.0.0
* @apiName CalculateTotalTime
* @apiGroup Time Interval
*
* @apiUse AuthHeader
* @apiUse ParamTimeInterval
*
* @apiSuccess {Integer} time Total time in seconds calculated between the intervals.
* @apiSuccess {String} start The earliest start datetime.
* @apiSuccess {String} end The latest end datetime.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "time": 1200,
* "start": "2024-08-16 18:00:00",
* "end": "2024-08-17 11:10:00"
* }
*
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*
*/
public function total(IntervalTotalRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$timezone = Settings::scope('core')->get('timezone', 'UTC');
$start_at = Carbon::parse($requestData['start_at'])->setTimezone($timezone);
$end_at = Carbon::parse($requestData['end_at'])->setTimezone($timezone);
$filters = [
'where' => [
'start_at' => ['>=', $start_at],
'end_at' => ['<=', $end_at],
'user_id' => ['=', $requestData['user_id']],
],
];
$itemsQuery = $this->getQuery($filters);
CatEvent::dispatch(Filter::getBeforeActionEventName(), $filters);
$timeIntervals = Filter::process(Filter::getActionFilterName(), $itemsQuery->get());
CatEvent::dispatch(Filter::getAfterActionEventName(), [$timeIntervals, $filters]);
$totalTime = $timeIntervals->sum(static fn($el) => Carbon::parse($el->end_at)->diffInSeconds($el->start_at));
return responder()->success([
'time' => $totalTime,
'start' => $timeIntervals->min('start_at'),
'end' => $timeIntervals->max('end_at'),
])->respond();
}
/**
* @throws Exception
*/
/**
* @api {any} /time/tasks Calculate total time interval
* @apiDescription Calculate the total time spent within the specified start and end intervals for a given user.
*
* @apiVersion 4.0.0
* @apiName CalculateTotalTime
* @apiGroup Time Interval
*
* @apiUse AuthHeader
*
* @apiUse ParamTimeInterval
*
* @apiSuccess {Integer} tasks.id The ID of the task.
* @apiSuccess {Integer} tasks.user_id The ID of the user.
* @apiSuccess {Integer} tasks.project_id The latest end datetime.
* @apiSuccess {Integer} tasks.time Total time in seconds calculated between the intervals.
* @apiSuccess {String} tasks.start The earliest start datetime.
* @apiSuccess {String} tasks.end The latest end datetime.
* @apiSuccess {Integer} total.time Total time in seconds calculated between the intervals.
* @apiSuccess {String} total.start The earliest start datetime.
* @apiSuccess {String} total.end The latest end datetime.
*
* @apiSuccessExample {json} Response Example
* HTTP/1.1 200 OK
* {
* "tasks": [
* {
* "id": 59,
* "user_id": 7,
* "project_id": 2,
* "time": 1200,
* "start": "2024-08-16T18:00:00.000000Z",
* "end": "2024-08-17T11:10:00.000000Z"
* }
* ],
* "total": {
* "time": 1200,
* "start": "2024-08-16T18:00:00.000000Z",
* "end": "2024-08-17T11:10:00.000000Z"
* }
* }
* @apiUse 400Error
* @apiUse ValidationError
* @apiUse ForbiddenError
* @apiUse UnauthorizedError
*
*/
public function tasks(IntervalTasksRequest $request): JsonResponse
{
$requestData = Filter::process(Filter::getRequestFilterName(), $request->validated());
$timezone = Settings::scope('core')->get('timezone', 'UTC');
$filters = [];
if (isset($requestData['start_at'])) {
$filters['start_at'] = ['>=', Carbon::parse($requestData['start_at'])->setTimezone($timezone)];
}
if (isset($requestData['end_at'])) {
$filters['end_at'] = ['<=', Carbon::parse($requestData['end_at'])->setTimezone($timezone)];
}
if (isset($requestData['project_id'])) {
$filters['task.project_id'] = $requestData['project_id'];
}
if (isset($requestData['task_id'])) {
$filters['task_id'] = ['in', $requestData['task_id']];
}
if (isset($requestData['user_id'])) {
$filters['user_id'] = $requestData['user_id'];
}
$itemsQuery = $this->getQuery($filters ? ['where' => $filters] : []);
$tasks = $itemsQuery
->with('task')
->get()
->groupBy(['task_id', 'user_id'])
->map(static function ($taskIntervals, $taskId) use (&$totalTime) {
$task = [];
foreach ($taskIntervals as $userId => $userIntervals) {
$taskTime = 0;
foreach ($userIntervals as $interval) {
$taskTime += Carbon::parse($interval->end_at)->diffInSeconds($interval->start_at);
}
$firstUserInterval = $userIntervals->first();
$lastUserInterval = $userIntervals->last();
$task = [
'id' => $taskId,
'user_id' => $userId,
'project_id' => $userIntervals[0]['task']['project_id'],
'time' => $taskTime,
'start' => Carbon::parse($firstUserInterval->start_at)->toISOString(),
'end' => Carbon::parse($lastUserInterval->end_at)->toISOString()
];
$totalTime += $taskTime;
}
return $task;
})
->values();
$first = $itemsQuery->get()->first();
$last = $itemsQuery->get()->last();
return responder()->success([
'tasks' => $tasks,
'total' => [
'time' => $totalTime,
'start' => $first ? Carbon::parse($first->start_at)->toISOString() : null,
'end' => $last ? Carbon::parse($last->end_at)->toISOString() : null,
]
])->respond();
}
}