first commit

This commit is contained in:
Noor E Ilahi
2026-01-09 12:54:53 +05:30
commit 7ccf44f7da
1070 changed files with 113036 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
<?php
namespace App\Services;
use App\Contracts\AttachmentAble;
use App\Enums\AttachmentStatus;
use App\Jobs\VerifyAttachmentHash;
use App\Models\Attachment;
use App\Models\Project;
use App\Models\Task;
use App\Models\TaskComment;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Http\UploadedFile;
use Storage;
class AttachmentService implements \App\Contracts\AttachmentService
{
public const DISK = 'attachments';
private readonly FilesystemAdapter $storage;
public function __construct()
{
$this->storage = Storage::disk($this::DISK);
}
public function storeFile(UploadedFile $file, Attachment $attachment): string|false
{
return $file->storeAs(
$this->getPath($attachment),
['disk' => $this::DISK]
);
}
public function handleProjectChange(Task $parent): void
{
$newProjectId = $parent->getProjectId();
$parent->attachmentsRelation()->lazyById()
->each(fn(Attachment $attachment) => $this->moveAttachment($attachment, $newProjectId));
$parent->comments()->lazyById()
->each(fn(TaskComment $comment) => $comment->attachmentsRelation()->lazyById()
->each(fn(Attachment $attachment) => $this->moveAttachment($attachment, $newProjectId)));
}
public function moveAttachment(Attachment $attachment, $newProjectId): void
{
if ($attachment->status === AttachmentStatus::NOT_ATTACHED) {
return;
}
$oldPath = $this->getPath($attachment);
$oldProjectId = $attachment->project_id;
$attachment->project_id = $newProjectId;
$attachment->saveQuietly();
$newPath = $this->getPath($attachment);
$fileExists = $this->storage->exists($oldPath);
if (!$fileExists || !$this->storage->move($oldPath, $newPath)) {
$attachment->project_id = $oldProjectId;
$attachment->saveQuietly();
\Log::error("Unable to move attachment`s file", [
'attachment_id' => $attachment->id,
'attachmentable_id' => $attachment->attachmentable_id,
'attachmentable_type' => $attachment->attachmentable_type,
'attachment_project_id' => $attachment->project_id,
'old_attachment_project_id' => $oldProjectId,
'old_path' => $oldPath,
'new_path' => $newPath
]);
}
}
public function deleteAttachment(Attachment $attachment): void
{
if (($fileExists = $this->fileExists($attachment)) && $this->deleteFile($attachment)) {
$attachment->deleteQuietly();
} elseif (!$fileExists) {
$attachment->deleteQuietly();
} else {
\Log::error("Unable to delete attachment`s file", [
'attachment_id' => $attachment->id,
'attachmentable_id' => $attachment->attachmentable_id,
'attachmentable_type' => $attachment->attachmentable_type,
'attachment_project_id' => $attachment->project_id,
'path' => $this->getPath($attachment)
]);
}
}
public function deleteAttachments(array $attachmentsIds): void
{
Attachment::whereIn('id', $attachmentsIds)
->each(fn (Attachment $attachment) => $this->deleteAttachment($attachment));
}
public function handleParentDeletion(AttachmentAble $parent): void
{
$parent->attachmentsRelation()->lazyById()
->each(fn (Attachment $attachment) => $this->deleteAttachment($attachment));
}
public function handleProjectDeletion(Project $project): void
{
Attachment::whereProjectId($project->id)->delete();
if ($this->storage->directoryExists($this->getProjectPath($project)) && !$this->storage->deleteDirectory($this->getProjectPath($project))) {
\Log::error("Unable to delete project's attachments directory", [
'project_id' => $project->id,
'path' => $this->getProjectPath($project)
]);
}
}
public function fileExists(Attachment $attachment): bool
{
return $this->storage->exists($this->getPath($attachment));
}
private function deleteFile(Attachment $attachment): bool
{
return $this->storage->delete($this->getPath($attachment));
}
public function attach(AttachmentAble $parent, array $idsToAttach): void
{
$projectId = $parent->getProjectId();
Attachment::whereIn('id', $idsToAttach)
->each(function (Attachment $attachment) use ($parent, $projectId) {
if ($attachment->status !== AttachmentStatus::NOT_ATTACHED) {
return;
}
$tmpPath = $this->getPath($attachment);
if ($this->storage->exists($tmpPath)) {
$attachment->attachmentAbleRelation()->associate($parent);
$attachment->status = AttachmentStatus::PROCESSING;
$attachment->project_id = $projectId;
$attachment->saveQuietly();
$newPath = $this->getPath($attachment);
$this->storage->move($tmpPath, $newPath);
VerifyAttachmentHash::dispatch($attachment)->afterCommit();
}
});
}
public function getProjectPath(Project $project): string
{
return "projects/{$project->id}";
}
public function getPath(Attachment $attachment): string
{
return match ($attachment->status) {
AttachmentStatus::NOT_ATTACHED => "users/{$attachment->user_id}/{$attachment->id}",
AttachmentStatus::PROCESSING,
AttachmentStatus::GOOD,
AttachmentStatus::BAD => "projects/{$attachment->project_id}/{$attachment->id}",
};
}
public function getFullPath(Attachment $attachment): string
{
return $this->storage->path($this->getPath($attachment));
}
public function readStream(Attachment $attachment)
{
return $this->storage->readStream($this->getPath($attachment));
}
public function getHashAlgo(): string
{
return $this->storage->getConfig()['checksum_algo'] ?? 'sha512/256';
}
public function getHashSum(Attachment|string $attachment): false|string
{
return $this->storage->checksum($this->callIfInstance($attachment, 'getPath'), ['checksum_algo' => $this->getHashAlgo()]);
}
public function getMimeType(Attachment|string $attachment): false|string
{
return $this->storage->mimeType($this->callIfInstance($attachment, 'getPath'));
}
private function callIfInstance(Attachment|string $attachment, $action)
{
return $attachment instanceof Attachment ? $this->{$action}($attachment) : $attachment;
}
public function verifyHash(Attachment $attachment): void
{
if ($attachment->status === AttachmentStatus::PROCESSING && $hash = $this->getHashSum($attachment)) {
$attachment->hash = $hash;
$attachment->status = AttachmentStatus::GOOD;
} elseif ($attachment->status === AttachmentStatus::GOOD && $attachment->hash !== $this->getHashSum($attachment)
) {
$attachment->status = AttachmentStatus::BAD;
}
$attachment->save();
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace App\Services;
use App\Models\Invitation;
use Exception;
use Str;
class InvitationService
{
/**
* The invitation expires in one months.
*/
protected const EXPIRATION_TIME_IN_DAYS = 30;
/**
* Create new invitation.
*
* @param array $user
* @return Invitation|null
* @throws Exception
*/
public static function create(array $user): ?Invitation
{
return Invitation::create([
'email' => $user['email'],
'key' => Str::uuid(),
'expires_at' => now()->addDays(self::EXPIRATION_TIME_IN_DAYS),
'role_id' => $user['role_id']
]);
}
/**
* Update invitation.
*
* @param int $id
* @return Invitation|null
* @throws Exception
*/
public static function update(int $id): ?Invitation
{
return tap(Invitation::find($id))->update([
'key' => Str::uuid(),
'expires_at' => now()->addDays(self::EXPIRATION_TIME_IN_DAYS)
]);
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace App\Services;
use Cache;
use Exception;
use JsonException;
use Nwidart\Modules\Contracts\ActivatorInterface;
use Nwidart\Modules\Module;
use App\Models\Module as ModuleModel;
class ModuleActivatorService implements ActivatorInterface
{
/**
* @inheritDoc
* @throws Exception
*/
public function enable(Module $module): void
{
$this->setActiveByName($module->getName(), true);
}
/**
* @inheritDoc
* @throws Exception
*/
public function disable(Module $module): void
{
$this->setActiveByName($module->getName(), false);
}
/**
* @inheritDoc
* @throws Exception
*/
public function hasStatus(Module $module, bool $status): bool
{
$moduleStatuses = Cache::store('octane')
->rememberForever(config('modules.activators.amazing.cache_key'), function () {
$configFile = config('modules.activators.amazing.file_name');
$databaseModules = [];
try {
foreach (ModuleModel::all()->toArray() as $module) {
$databaseModules[$module['name']] = $module['enabled'];
}
} catch (Exception) {
// We can't communicate with db - then do nothing
// This can happen on first install when we are trying to migrate over clear database
}
return array_merge(
$this->getFileConfig($configFile),
$this->getFileConfig("$configFile." . config('app.env')),
$this->getFileConfig("$configFile.local"),
$databaseModules,
);
});
if (isset($moduleStatuses[$module->getName()])) {
return $moduleStatuses[$module->getName()] ?? false === $status;
}
return (bool)$module->json()?->active;
}
/**
* @inheritDoc
* @throws Exception
*/
public function setActive(Module $module, bool $active): void
{
$this->setActiveByName($module->getName(), $active);
}
/**
* @inheritDoc
* @throws Exception
*/
public function setActiveByName(string $name, bool $active): void
{
ModuleModel::firstOrCreate(['name' => $name])->update(['enabled' => $active]);
$this->flushCache();
}
/**
* @inheritDoc
* @throws Exception
*/
public function delete(Module $module): void
{
ModuleModel::firstOrFail($module->getName())->delete();
$this->flushCache();
}
/**
* @inheritDoc
* @throws Exception
*/
public function reset(): void
{
ModuleModel::truncate();
$this->flushCache();
}
/**
* @param $name
* @return array
* @throws JsonException
*/
private function getFileConfig($name): array
{
$filePath = base_path("$name.json");
return file_exists($filePath) ? json_decode(file_get_contents($filePath), true, 512, JSON_THROW_ON_ERROR) : [];
}
/**
* @throws Exception
*/
private function flushCache(): void
{
Cache::store('octane')->forget(config('modules.activators.amazing.cache_key'));
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Services;
use App\Contracts\ScreenshotService as ScreenshotServiceContract;
use App\Models\TimeInterval;
class ProductionScreenshotService extends ScreenshotServiceContract
{
public function getScreenshotPath(TimeInterval|int $interval): string
{
return self::PARENT_FOLDER . hash('sha256', optional($interval)->id ?: $interval) . '.' . self::FILE_FORMAT;
}
public function getThumbPath(TimeInterval|int $interval): string
{
return self::PARENT_FOLDER . self::THUMBS_FOLDER . hash(
'sha256',
optional($interval)->id ?: $interval
) . '.' . self::FILE_FORMAT;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Services;
use App\Models\Project;
class ProjectMemberService
{
public static function getMembers(int $projectId): array
{
return Project::find($projectId, 'id')
->where('id', $projectId)
->with('users')
->first()
->only(['id', 'users']);
}
public static function syncMembers(int $projectId, array $users): array
{
return Project::findOrFail($projectId)->users()->sync($users);
}
}

View File

@@ -0,0 +1,168 @@
<?php
namespace App\Services;
use App\Contracts\SettingsProvider;
use App\Models\Setting;
use Cache;
use Exception;
use PDOException;
class SettingsProviderService implements SettingsProvider
{
protected string $scope = 'app';
public function __construct(private readonly Setting $model, private readonly bool $saveScope = true)
{
}
/**
* Sets scope for the next request to settings module
*
* @param string $moduleName
*
* @return SettingsProviderService
*/
public function scope(string $moduleName): SettingsProviderService
{
$this->scope = $moduleName;
return $this;
}
/**
* @inerhitDoc
*/
final public function all(): array
{
$scope = $this->scope;
if (!$this->saveScope) {
$this->scope = '';
}
$result = $this->model::whereModuleName($scope)
->get()
->map(static fn(Setting $item) => [$item->key => $item->value])
->collapse()
->toArray();
try {
Cache::store('octane')->forever("settings:$scope", $result);
} catch (Exception) {
// DO NOTHING
}
return $result;
}
/**
* @inerhitDoc
*/
final public function get(string $key = null, mixed $default = null): mixed
{
$scope = $this->scope;
if (!$this->saveScope) {
$this->scope = '';
}
try {
$cached = Cache::store('octane')->get("settings:$scope");
if (!isset($cached[$key])) {
$cached[$key] = optional(
$this->model::where([
'module_name' => $scope,
'key' => $key,
])->first()
)->value ?? $default;
Cache::store('octane')->put("settings:$scope", $cached);
}
return $cached[$key];
} catch (PDOException) {
return $default;
} catch (Exception) {
return optional(
$this->model::where([
'module_name' => $scope,
'key' => $key,
])->first()
)->value ?? $default;
}
}
/**
* @inerhitDoc
*/
final public function set(mixed $key, mixed $value = null, bool $onlyIfNotExists = false): void
{
$scope = $this->scope;
if (!$this->saveScope) {
$this->scope = '';
}
if (is_array($key)) {
foreach ($key as $_key => $_value) {
if ($onlyIfNotExists &&
$this->model::where([
'module_name' => $scope,
'key' => $_key,
])->exists()
) {
continue;
}
$this->model::updateOrCreate([
'module_name' => $scope,
'key' => $_key,
], [
'value' => $_value,
]);
}
} else {
if ($onlyIfNotExists &&
$this->model::where([
'module_name' => $scope,
'key' => $key,
])->exists()
) {
return;
}
$this->model::updateOrCreate([
'module_name' => $scope,
'key' => $key,
], [
'value' => $value,
]);
}
try {
Cache::store('octane')->forget("settings:$scope");
} catch (Exception) {
// DO NOTHING
}
}
/**
* @inerhitDoc
*/
final public function flush(): void
{
$scope = $this->scope;
if (!$this->saveScope) {
$this->scope = '';
}
try {
Cache::store('octane')->forget("settings:$scope");
} catch (Exception) {
// DO NOTHING
}
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Models\Task;
use App\Models\TimeInterval;
use App\Models\UniversalReport;
use Carbon\Carbon;
class UniversalReportServiceProject
{
private Carbon $startAt;
private Carbon $endAt;
private UniversalReport $report;
private array $periodDates;
public function __construct(Carbon $startAt, Carbon $endAt, UniversalReport $report, array $periodDates = [])
{
$this->startAt = $startAt;
$this->endAt = $endAt;
$this->report = $report;
$this->periodDates = $periodDates;
}
public function getProjectReportData()
{
$projectFields = ['id'];
foreach ($this->report->fields['base'] as $field) {
$projectFields[] = 'projects.' . $field;
}
$taskRelations = [];
$taskFields = ['id', 'tasks.project_id'];
foreach ($this->report->fields['tasks'] as $field) {
if ($field !== 'priority' && $field !== 'status') {
$taskFields[] = 'tasks.' . $field;
} else {
$taskRelations[] = 'tasks.' . $field;
$taskFields[] = 'tasks.' . $field . '_id';
}
}
$userFields = ['id'];
foreach ($this->report->fields['users'] as $field) {
$userFields[] = 'users.' . $field;
}
$projectsQuery = Project::with(['tasks' => function ($query) use ($taskFields) {
$query->select($taskFields);
}, 'users' => function ($query) use ($userFields) {
$query->select($userFields);
}])
->select(array_merge($projectFields))->whereIn('id', $this->report->data_objects);
if (!empty($taskRelations)) $projectsQuery = $projectsQuery->with($taskRelations);
$projects = $projectsQuery->get();
$tasksId = [];
foreach ($projects as $project) {
$tasksId[] = $project->tasks->pluck('id');
}
$taskQuery = Task::whereIn('project_id', $projects->pluck('id'));
$projectIdsIndexedByTaskIds = $taskQuery->pluck('project_id', 'id');
$tasksId = collect($tasksId)->flatten()->toArray();
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
$totalSpentTimeByUserAndDay = TimeInterval::whereIn('task_id', $tasksId)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id', 'task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_user_and_day')
->groupBy('user_id', 'date_at', 'task_id')->get();
$totalSpentTimeByDay = TimeInterval::whereIn('task_id', $tasksId)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day')
->groupBy('date_at', 'task_id')->get();
$intervalProjectId = null;
$workedTimeByDayUser = [];
$totalSpentTimeUser = [];
foreach ($totalSpentTimeByUserAndDay as $timeInterval) {
$intervalDate = $timeInterval['date_at'];
if (isset($projectIdsIndexedByTaskIds[$timeInterval->task_id])) {
$intervalProjectId = $projectIdsIndexedByTaskIds[$timeInterval->task_id];
}
$intervalUserId = $timeInterval->user_id;
$startDateTime = Carbon::parse($this->startAt);
$endDateTime = Carbon::parse($this->endAt);
if (!isset($workedTimeByDayUser[$intervalProjectId][$intervalUserId])) {
$workedTimeByDayUser[$intervalProjectId][$intervalUserId] = [];
}
if (!isset($workedTimeByDayUser[$intervalProjectId][$intervalUserId][$intervalDate])) {
$workedTimeByDayUser[$intervalProjectId][$intervalUserId][$intervalDate] = 0;
}
if (!isset($totalSpentTimeUser[$intervalProjectId][$intervalUserId])) {
$totalSpentTimeUser[$intervalProjectId][$intervalUserId] = 0;
}
$workedTimeByDayUser[$intervalProjectId][$intervalUserId][$intervalDate] += $timeInterval->total_spent_time_by_user_and_day;
$totalSpentTimeUser[$intervalProjectId][$intervalUserId] += $timeInterval->total_spent_time_by_user_and_day;
while ($startDateTime <= $endDateTime) {
$currentDate = $startDateTime->format('Y-m-d');
if ($currentDate !== $intervalDate) {
$workedTimeByDayUser[$intervalProjectId][$intervalUserId][$currentDate] = 0;
}
$startDateTime->modify('+1 day');
}
}
$workedTimeByDay = [];
foreach ($totalSpentTimeByDay as $timeInterval) {
$intervalDate = $timeInterval['date_at'];
$intervalProjectId = $projectIdsIndexedByTaskIds[$timeInterval->task_id];
$startDateTime = \Carbon\Carbon::parse($this->startAt);
$endDateTime = \Carbon\Carbon::parse($this->endAt);
while ($startDateTime <= $endDateTime) {
$currentDate = $startDateTime->format('Y-m-d');
if ($currentDate !== $intervalDate) {
$workedTimeByDay[$intervalProjectId][$currentDate] = 0;
}
$startDateTime->modify('+1 day');
}
if (!isset($workedTimeByDay[$intervalProjectId])) {
$workedTimeByDay[$intervalProjectId] = [];
}
if (!isset($workedTimeByDay[$intervalProjectId][$intervalDate])) {
$workedTimeByDay[$intervalProjectId][$intervalDate] = 0;
}
$workedTimeByDay[$intervalProjectId][$intervalDate] += $timeInterval->total_spent_time_by_day;
}
foreach ($projects as $project) {
if (isset($workedTimeByDay[$project->id]))
$project->worked_time_day = $workedTimeByDay[$project->id];
foreach ($project->users as $user) {
if (isset($workedTimeByDayUser[$project->id][$user->id]))
$user->workers_day = $workedTimeByDayUser[$project->id][$user->id];
if (isset($totalSpentTimeUser[$project->id][$user->id]))
$user->total_spent_time_by_user = $totalSpentTimeUser[$project->id][$user->id];
else
$user->total_spent_time_by_user = 0;
}
}
$projects = $projects->keyBy('id')->toArray();
foreach ($projects as &$project) {
if (isset($project['created_at'])) {
$date = Carbon::parse($project['created_at']);
$project['created_at'] = $date->format('Y-m-d H:i:s');
}
foreach ($project['tasks'] as &$task) {
if (isset($task['priority'])) $task['priority'] = $task['priority']['name'];
if (isset($task['status'])) $task['status'] = $task['status']['name'];
}
}
return $projects;
}
public function getProjectReportCharts()
{
$result = [];
if (count($this->report->charts) === 0) {
return $result;
}
$projects = Project::query()
->with(['tasks', 'users'])
->whereIn('id', $this->report->data_objects)->get();
$usersId = [];
$tasksId = [];
foreach ($projects as $project) {
$projectsName = $project->pluck('name', 'id');
$usersId[] = $project->users->pluck('id');
$tasksId[] = $project->tasks->pluck('id');
foreach ($project->users as $user) {
$userNames = $user->pluck('full_name', 'id');
}
}
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
$usersId = collect($usersId)->flatten()->toArray();
$tasksId = collect($tasksId)->flatten()->toArray();
if (in_array('total_spent_time_day', $this->report->charts)) {
$total_spent_time_day = [
'datasets' => []
];
$taskQuery = Task::whereIn('project_id', $projects->pluck('id'));
$projectIdsIndexedByTaskIds = $taskQuery->pluck('project_id', 'id');
TimeInterval::whereIn('task_id', $tasksId)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_day')
->groupBy('date_at', 'task_id')
->get()
->each(function ($timeInterval) use (&$total_spent_time_day, $userNames, $projectIdsIndexedByTaskIds, $projectsName) {
$time = 0;
$projectId = (int)$timeInterval->task->project_id;
foreach ($projectIdsIndexedByTaskIds as $taskId => $id) {
if ($projectId === $id) {
$time += $timeInterval->total_spent_time_day;
}
}
if (!isset($total_spent_time_day['datasets'][$projectId])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_day['datasets'][$projectId] = [
'label' => $projectsName[$projectId] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
}
$total_spent_time_day['datasets'][$projectId]['data'][$timeInterval->date_at] = $time;
});
foreach ($total_spent_time_day['datasets'] as $key => $item) {
$this->fillNullDatesAsZeroTime($total_spent_time_day['datasets'], 'data');
}
$result['total_spent_time_day'] = $total_spent_time_day;
}
if (in_array('total_spent_time_day_and_users_separately', $this->report->charts)) {
$total_spent_time_day_and_users_separately = [
'datasets' => [],
];
TimeInterval::whereIn('user_id', $usersId)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id', 'task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_day_and_users_separately')
->groupBy('user_id', 'date_at', 'task_id')
->get()
->each(function ($timeInterval) use (&$total_spent_time_day_and_users_separately, $userNames, $projectsName) {
$time = $timeInterval->total_spent_time_day_and_users_separately;
$projectId = (int)$timeInterval->task->project_id;
if (!isset($total_spent_time_day_and_users_separately['datasets'][$projectId])) {
$total_spent_time_day_and_users_separately['datasets'][$projectId] = [];
}
$userId = $timeInterval->user_id;
if (!isset($total_spent_time_day_and_users_separately['datasets'][$projectId][$userId])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_day_and_users_separately['datasets'][$projectId][$userId] = [
'label' => $userNames[$timeInterval->user_id] ?? ' ',
'projectLabel' => $projectsName[$projectId] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
}
$total_spent_time_day_and_users_separately['datasets'][$projectId][$userId]['data'][$timeInterval->date_at] = $time;
});
foreach ($total_spent_time_day_and_users_separately['datasets'] as $key => $item) {
$this->fillNullDatesAsZeroTime($total_spent_time_day_and_users_separately['datasets'][$key], 'data');
}
$result['total_spent_time_day_and_users_separately'] = $total_spent_time_day_and_users_separately;
}
return $result;
}
public function fillNullDatesAsZeroTime(array &$datesToFill, $key = null)
{
foreach ($this->periodDates as $date) {
if (is_null($key)) {
unset($datesToFill['']);
array_key_exists($date, $datesToFill) ? '' : $datesToFill[$date] = 0.0;
} else {
foreach ($datesToFill as $k => $item) {
unset($datesToFill[$k][$key]['']);
if (!array_key_exists($date, $item[$key])) {
$datesToFill[$k][$key][$date] = 0.0;
}
ksort($datesToFill[$k][$key]);
}
}
}
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace App\Services;
use App\Models\Task;
use App\Models\TimeInterval;
use App\Models\UniversalReport;
use App\Models\User;
use Carbon\Carbon;
class UniversalReportServiceTask
{
private Carbon $startAt;
private Carbon $endAt;
private UniversalReport $report;
private array $periodDates;
public function __construct(Carbon $startAt, Carbon $endAt, UniversalReport $report, array $periodDates = [])
{
$this->startAt = $startAt;
$this->endAt = $endAt;
$this->report = $report;
$this->periodDates = $periodDates;
}
public function getTaskReportData()
{
$projectFields = ['id'];
foreach ($this->report->fields['projects'] as $field) {
$projectFields[] = 'projects.' . $field;
}
$taskRelations = [];
$taskFields = ['id', 'tasks.project_id'];
foreach ($this->report->fields['base'] as $field) {
if ($field !== 'priority' && $field !== 'status') {
$taskFields[] = 'tasks.' . $field;
} else {
$taskRelations[] = $field;
$taskFields[] = 'tasks.' . $field . '_id';
}
}
$userFields = ['id'];
foreach ($this->report->fields['users'] as $field) {
$userFields[] = 'users.' . $field;
}
$tasksQuery = Task::query()
->with(['project' => function ($query) use ($projectFields) {
$query->select($projectFields);
}, 'users' => function ($query) use ($userFields) {
$query->select($userFields);
}])
->select(array_merge($taskFields))->whereIn('id', $this->report->data_objects);
if (!empty($taskRelations)) $tasksQuery = $tasksQuery->with($taskRelations);
$tasks = $tasksQuery->get();
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
$totalSpentTimeByUser = TimeInterval::whereIn('task_id', $tasks->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id', 'task_id')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_user')
->groupBy('user_id', 'task_id')
->get();
$totalSpentTimeByUserAndDay = TimeInterval::whereIn('task_id', $tasks->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id', 'task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_user_and_day')
->groupBy('user_id', 'date_at', 'task_id')->get();
$totalSpentTime = TimeInterval::whereIn('task_id', $tasks->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time')
->groupBy('task_id')->pluck('total_spent_time', 'task_id');
$totalSpentTimeByDay = TimeInterval::whereIn('task_id', $tasks->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day')
->groupBy('task_id', 'date_at')->get();
foreach ($tasks as $task) {
$worked_time_day = [];
$startDateTime = Carbon::parse($this->startAt);
$endDateTime = Carbon::parse($this->endAt);
$task->total_spent_time = $totalSpentTime[$task->id] ?? 0;
while ($startDateTime <= $endDateTime) {
$currentDate = $startDateTime->format('Y-m-d');
foreach ($totalSpentTimeByDay as $item) {
if (($item['date_at'] === $currentDate) && (int)$item['task_id'] === $task->id) {
$worked_time_day[$currentDate] = $item['total_spent_time_by_day'];
break;
}
}
if (!isset($worked_time_day[$currentDate])) {
$worked_time_day[$currentDate] = 0.0;
}
$startDateTime->modify('+1 day');
}
$task->worked_time_day = $worked_time_day;
foreach ($task->users as $user) {
$worked_time_day = [];
$startDateTime = Carbon::parse($this->startAt);
$endDateTime = Carbon::parse($this->endAt);
$user->total_spent_time_by_user = $totalSpentTimeByUser->where('user_id', $user->id)->where('task_id', $task->id)->first()->total_spent_time_by_user ?? 0;
while ($startDateTime <= $endDateTime) {
$currentDate = $startDateTime->format('Y-m-d');
foreach ($totalSpentTimeByUserAndDay as $item) {
if (($item['date_at'] === $currentDate) && ($user->id === (int)$item['user_id'] && (int)$item['task_id'] === $task->id)) {
$worked_time_day[$currentDate] = $item['total_spent_time_by_user_and_day'];
break;
}
}
if (!isset($worked_time_day[$currentDate])) {
$worked_time_day[$currentDate] = 0.0;
}
$startDateTime->modify('+1 day');
}
$user->workers_day = $worked_time_day;
}
}
$tasks = $tasks->keyBy('id')->toArray();
foreach ($tasks as &$task) {
if (isset($task['priority'])) $task['priority'] = $task['priority']['name'];
if (isset($task['status'])) $task['status'] = $task['status']['name'];
}
return $tasks;
}
public function getTasksReportCharts()
{
$result = [];
if (count($this->report->charts) === 0) {
return $result;
}
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
$tasks = Task::whereIn('id', $this->report->data_objects)->get();
if (in_array('total_spent_time_day', $this->report->charts)) {
$total_spent_time_day = [
'datasets' => []
];
$taskNames = $tasks->pluck('task_name', 'id');
TimeInterval::whereIn('task_id', $this->report->data_objects)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_day')
->groupBy('task_id', 'date_at')
->get()
->each(function ($timeInterval) use (&$total_spent_time_day, $taskNames) {
$time = sprintf("%02d.%02d", floor($timeInterval->total_spent_time_day / 3600), floor($timeInterval->total_spent_time_day / 60) % 60);
if (!array_key_exists($timeInterval->task_id, $total_spent_time_day['datasets'])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_day['datasets'][$timeInterval->task_id] = [
'label' => $taskNames[$timeInterval->task_id] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
}
$total_spent_time_day['datasets'][$timeInterval->task_id]['data'][$timeInterval->date_at] = $time;
});
$this->fillNullDatesAsZeroTime($total_spent_time_day['datasets'], 'data');
$result['total_spent_time_day'] = $total_spent_time_day;
}
if (in_array('total_spent_time_day_users_separately', $this->report->charts)) {
$total_spent_time_day_users_separately = [
'datasets' => [],
];
$userTasks = User::whereHas('tasks', function ($query) {
$query->whereIn('task_id', $this->report->data_objects);
})->get();
$userNames = $userTasks->pluck('full_name', 'id');
TimeInterval::whereIn('task_id', $this->report->data_objects)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('task_id', 'user_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_day_users_separately')
->groupBy('task_id', 'date_at', 'user_id')
->get()
->each(function ($timeInterval) use (&$total_spent_time_day_users_separately, $userNames) {
$time = $timeInterval->total_spent_time_day_users_separately;
$taskId = $timeInterval->task_id;
$userId = $timeInterval->user_id;
if (!isset($total_spent_time_day_users_separately['datasets'][$taskId])) {
$total_spent_time_day_users_separately['datasets'][$taskId] = [];
}
if (!isset($total_spent_time_day_users_separately['datasets'][$taskId][$userId])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_day_users_separately['datasets'][$taskId][$userId] = [
'label' => $userNames[$userId] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
}
$total_spent_time_day_users_separately['datasets'][$timeInterval->task_id][$timeInterval->user_id]['data'][$timeInterval->date_at] = $time;
});
foreach ($total_spent_time_day_users_separately['datasets'] as $key => $item) {
$this->fillNullDatesAsZeroTime($total_spent_time_day_users_separately['datasets'][$key], 'data');
}
$result['total_spent_time_day_users_separately'] = $total_spent_time_day_users_separately;
}
return $result;
}
public function fillNullDatesAsZeroTime(array &$datesToFill, $key = null)
{
foreach ($this->periodDates as $date) {
if (is_null($key)) {
unset($datesToFill['']);
array_key_exists($date, $datesToFill) ? '' : $datesToFill[$date] = 0.0;
} else {
foreach ($datesToFill as $k => $item) {
unset($datesToFill[$k][$key]['']);
if (!array_key_exists($date, $item[$key])) {
$datesToFill[$k][$key][$date] = 0.0;
}
ksort($datesToFill[$k][$key]);
}
}
}
}
}

View File

@@ -0,0 +1,263 @@
<?php
namespace App\Services;
use App\Models\Project;
use App\Models\Task;
use App\Models\TimeInterval;
use App\Models\UniversalReport;
use App\Models\User;
use Carbon\Carbon;
class UniversalReportServiceUser
{
private Carbon $startAt;
private Carbon $endAt;
private UniversalReport $report;
private array $periodDates;
public function __construct(Carbon $startAt, Carbon $endAt, UniversalReport $report, array $periodDates = [])
{
$this->startAt = $startAt;
$this->endAt = $endAt;
$this->report = $report;
$this->periodDates = $periodDates;
}
public function getUserReportData()
{
$projectFields = ['id'];
foreach ($this->report->fields['projects'] as $field) {
$projectFields[] = 'projects.' . $field;
}
$taskRelations = [];
$taskFields = ['tasks.id', 'tasks.project_id'];
foreach ($this->report->fields['tasks'] as $field) {
if ($field !== 'priority' && $field !== 'status') {
$taskFields[] = 'tasks.' . $field;
} else {
$taskRelations[] = 'tasks.' . $field;
$taskFields[] = 'tasks.' . $field . '_id';
}
}
$usersQuery = User::query()->with(['projects' => function ($query) use ($projectFields) {
$query->select($projectFields);
}])->with(['tasks' => function ($query) use ($taskFields) {
$query->select($taskFields);
}])->select(array_merge($this->report->fields['base'], ['id']))->whereIn('id', $this->report->data_objects);
if (!empty($taskRelations)) $usersQuery = $usersQuery->with($taskRelations);
$users = $usersQuery->get();
foreach ($users as $user) {
foreach ($user->projects as $project) {
$project->tasks = $user->tasks->where('project_id', $project->id)->toArray();
}
}
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
$totalSpentTime = TimeInterval::whereIn('user_id', $users->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time')
->groupBy('user_id')
->pluck('total_spent_time', 'user_id');
$totalSpentTimeDay = TimeInterval::whereIn('user_id', $users->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw(' SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day')
->groupBy('user_id', 'date_at')->get()->toArray();
foreach ($users as $user) {
$worked_time_day = [];
$startDateTime = Carbon::parse($this->startAt);
$endDateTime = Carbon::parse($this->endAt);
$user->total_spent_time = $totalSpentTime[$user->id] ?? 0;
while ($startDateTime <= $endDateTime) {
$currentDate = $startDateTime->format('Y-m-d');
foreach ($totalSpentTimeDay as $item) {
if (($item['date_at'] === $currentDate) && ($user->id === (int)$item['user_id'])) {
$worked_time_day[$currentDate] = $item['total_spent_time_by_day'];
break;
}
}
if (!isset($worked_time_day[$currentDate])) {
$worked_time_day[$currentDate] = 0;
}
$startDateTime->modify('+1 day');
}
$user->worked_time_day = $worked_time_day;
foreach ($user->projects as $project) {
if (!empty($project['tasks'])) {
$tasks = $project['tasks'];
foreach ($tasks as $key => $task) {
if (isset($tasks[$key]['priority'])) {
$tasks[$key]['priority'] = $tasks[$key]['priority']['name'];
}
if (isset($tasks[$key]['status'])) {
$tasks[$key]['status'] = $tasks[$key]['status']['name'];
}
}
$project['tasks'] = $tasks;
}
}
}
return $users->keyBy('id');
}
public function getUserReportCharts()
{
$result = [];
if (count($this->report->charts) === 0) {
return $result;
}
$endAt = clone $this->endAt;
$endAt = $endAt->endOfDay();
if (in_array('total_spent_time_day', $this->report->charts)) {
$total_spent_time_by_day = [
'datasets' => [],
];
$users = User::query()->whereIn('id', $this->report->data_objects)->get();
$userNames = $users->pluck('full_name', 'id');
TimeInterval::whereIn('user_id', $users->pluck('id'))
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->select('user_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw(' SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day')
->groupBy('user_id', 'date_at')
->get()
->each(function ($timeInterval) use (&$total_spent_time_by_day, $userNames) {
$time = sprintf("%02d.%02d", floor($timeInterval->total_spent_time_by_day / 3600), floor($timeInterval->total_spent_time_by_day / 60) % 60);
if (!array_key_exists($timeInterval->user_id, $total_spent_time_by_day['datasets'])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_by_day['datasets'][$timeInterval->user_id] = [
'label' => $userNames[$timeInterval->user_id] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
}
$total_spent_time_by_day['datasets'][$timeInterval->user_id]['data'][$timeInterval->date_at] = $time;
});
$this->fillNullDatesAsZeroTime($total_spent_time_by_day['datasets'], 'data');
$result['total_spent_time_day'] = $total_spent_time_by_day;
}
if (in_array('total_spent_time_day_and_tasks', $this->report->charts)) {
$total_spent_time_by_day_and_tasks = [
'datasets' => [],
];
$userTasks = Task::whereHas('users', function ($query) {
$query->whereIn('id', $this->report->data_objects);
})->get();
$taskNames = $userTasks->pluck('task_name', 'id');
TimeInterval::whereIn('user_id', $this->report->data_objects)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->whereIn('task_id', $userTasks->pluck('id'))
->select('task_id', 'user_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day_and_tasks')
->groupBy('task_id', 'date_at', 'user_id')
->get()
->each(function ($timeInterval) use (&$total_spent_time_by_day_and_tasks, $taskNames) {
$time = sprintf("%02d.%02d", floor($timeInterval->total_spent_time_by_day_and_tasks / 3600), floor($timeInterval->total_spent_time_by_day_and_tasks / 60) % 60);
if (!array_key_exists($timeInterval->user_id, $total_spent_time_by_day_and_tasks['datasets'])) {
$total_spent_time_by_day_and_tasks['datasets'][$timeInterval->user_id] = [];
}
if (!array_key_exists($timeInterval->task_id, $total_spent_time_by_day_and_tasks['datasets'][$timeInterval->user_id])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_by_day_and_tasks['datasets'][$timeInterval->user_id][$timeInterval->task_id] = [
'label' => $taskNames[$timeInterval->task_id] ?? ' ',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time],
];
} else {
$total_spent_time_by_day_and_tasks['datasets'][$timeInterval->user_id][$timeInterval->task_id]['data'][$timeInterval->date_at] = $time;
}
});
foreach ($total_spent_time_by_day_and_tasks['datasets'] as $key => $item) {
$this->fillNullDatesAsZeroTime($total_spent_time_by_day_and_tasks['datasets'][$key], 'data');
}
$result['total_spent_time_day_and_tasks'] = $total_spent_time_by_day_and_tasks;
}
if (in_array('total_spent_time_day_and_projects', $this->report->charts)) {
$total_spent_time_by_day_and_projects = [
'datasets' => [],
];
$userProjects = Project::whereHas('users', function ($query) {
$query->whereIn('id', $this->report->data_objects);
})->get();
$projectNames = $userProjects->pluck('name', 'id');
$userTasks = Task::whereHas('users', function ($query) {
$query->whereIn('id', $this->report->data_objects);
})->pluck('project_id', 'id');
$timeIntervals = TimeInterval::whereIn('user_id', $this->report->data_objects)
->where('start_at', '>=', $this->startAt->format('Y-m-d H:i:s'))
->where('end_at', '<=', $endAt->format('Y-m-d H:i:s'))
->whereIn('task_id', $userTasks->keys())
->select('task_id', 'user_id')
->selectRaw('DATE(start_at) as date_at')
->selectRaw('SUM(TIMESTAMPDIFF(SECOND, start_at, end_at)) as total_spent_time_by_day_and_projects')
->groupBy('task_id', 'date_at', 'user_id')
->get();
$time = [];
foreach ($timeIntervals as $timeInterval) {
$projectId = $userTasks[$timeInterval->task_id];
if (!isset($time[$timeInterval->date_at . '_' . $projectId . '_' . $timeInterval->user_id])) {
$time[$timeInterval->date_at . '_' . $projectId . '_' . $timeInterval->user_id] = 0;
}
$time[$timeInterval->date_at . '_' . $projectId . '_' . $timeInterval->user_id] += $timeInterval->total_spent_time_by_day_and_projects;
}
$timeIntervals->each(function ($timeInterval) use (&$total_spent_time_by_day_and_projects, $userTasks, $projectNames, $time) {
if (!array_key_exists($timeInterval->user_id, $total_spent_time_by_day_and_projects['datasets'])) {
$total_spent_time_by_day_and_projects['datasets'][$timeInterval->user_id] = [];
}
$projectId = $userTasks[$timeInterval->task_id];
if (!array_key_exists($projectId, $total_spent_time_by_day_and_projects['datasets'][$timeInterval->user_id])) {
$color = sprintf('#%02X%02X%02X', rand(0, 255), rand(0, 255), rand(0, 255));
$total_spent_time_by_day_and_projects['datasets'][$timeInterval->user_id][$projectId] = [
'label' => $projectNames[$projectId] ?? '',
'borderColor' => $color,
'backgroundColor' => $color,
'data' => [$timeInterval->date_at => $time[$timeInterval->date_at . '_' . $projectId . '_' . $timeInterval->user_id] / 3600],
];
}
$total_spent_time_by_day_and_projects['datasets'][$timeInterval->user_id][$projectId]['data'][$timeInterval->date_at] = $time[$timeInterval->date_at . '_' . $projectId . '_' . $timeInterval->user_id] / 3600;
});
foreach ($total_spent_time_by_day_and_projects['datasets'] as $key => $item) {
$this->fillNullDatesAsZeroTime($total_spent_time_by_day_and_projects['datasets'][$key], 'data');
}
$result['total_spent_time_day_and_projects'] = $total_spent_time_by_day_and_projects;
}
return $result;
}
public function fillNullDatesAsZeroTime(array &$datesToFill, $key = null)
{
foreach ($this->periodDates as $date) {
if (is_null($key)) {
unset($datesToFill['']);
array_key_exists($date, $datesToFill) ? '' : $datesToFill[$date] = 0.0;
} else {
foreach ($datesToFill as $k => $item) {
unset($datesToFill[$k][$key]['']);
if (!array_key_exists($date, $item[$key])) {
$datesToFill[$k][$key][$date] = 0.0;
}
ksort($datesToFill[$k][$key]);
}
}
}
}
}