Improve tasks UI, dark theme, performance fixes, and admin pages polish

This commit is contained in:
2026-06-02 18:45:01 +03:30
parent 54aa8f40dd
commit 300575930f
18 changed files with 2598 additions and 780 deletions
+277 -46
View File
@@ -101,6 +101,236 @@ class TaskController extends Controller
return implode("\n", $lines);
}
private function sumDurationSeconds($durations, ?int $userId = null): int
{
$totalSeconds = 0;
foreach ($durations as $item) {
if ($userId !== null && (int)$item->user_id !== $userId) {
continue;
}
if ($item->duration === null) {
continue;
}
try {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
} catch (\Throwable) {
continue;
}
}
return $totalSeconds;
}
private function resolveAssigneeNames(Tasks $task, array $usersById = []): array
{
$assignees = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$assignees = is_array($assignees) ? array_map('strval', $assignees) : [];
if (empty($assignees)) {
return [];
}
if (!empty($usersById)) {
$names = [];
foreach ($assignees as $id) {
if (!empty($usersById[$id])) {
$names[] = $usersById[$id];
}
}
return $names;
}
return User::whereIn('id', $assignees)->pluck('name')->all();
}
private function buildTasksPayload($tasks, array $usersById = []): array
{
$payload = [];
foreach ($tasks as $task) {
$payload[$task->id] = [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'start_date' => $task->start_date,
'target_date' => $task->target_date,
'priority' => $task->priority,
'state' => $task->state,
'app' => $task->app,
'active' => (bool)$task->active,
'creator_name' => $task->creator_name,
'created_at' => $task->created_at,
'assignee_names' => $task->assignee_names ?? $this->resolveAssigneeNames($task, $usersById),
'total_duration' => $task->total_duration ?? '00:00:00',
'comments' => [],
'duration' => [],
];
}
return $payload;
}
private function userCanAccessTask(Tasks $task, User $user): bool
{
if ($user->is_admin == 1) {
return true;
}
$assignees = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$assignees = is_array($assignees) ? array_map('strval', $assignees) : [];
return (int)$task->created_by === (int)$user->id
|| in_array((string)$user->id, $assignees, true);
}
private function buildTaskDetailPayload(Tasks $task, bool $isAdmin, User $user): array
{
$activeQuery = TaskDuration::query()
->where('task_id', $task->id)
->whereNull('end');
if (!$isAdmin) {
$activeQuery->where('user_id', $user->id);
}
return [
'id' => $task->id,
'title' => $task->title,
'description' => $task->description,
'start_date' => $task->start_date,
'target_date' => $task->target_date,
'priority' => $task->priority,
'state' => $task->state,
'app' => $task->app,
'active' => $activeQuery->exists(),
'creator_name' => $task->creator?->name ?? 'نامشخص',
'created_at' => $task->created_at,
'assignee_names' => $this->resolveAssigneeNames($task),
'duration' => $task->duration->map(fn ($d) => [
'id' => $d->id,
'task_id' => $d->task_id,
'user_id' => $d->user_id,
'start' => $d->start,
'end' => $d->end,
'duration' => $d->duration,
])->values()->all(),
'comments' => $task->comments->map(fn ($c) => [
'id' => $c->id,
'body' => $c->body,
'created_at' => $c->created_at,
'user' => $c->user ? ['name' => $c->user->name] : null,
])->values()->all(),
];
}
private function loadTasksPageData(User $user): array
{
$isAdmin = $user->is_admin == 1;
$query = Tasks::query()
->select([
'id',
'title',
'description',
'start_date',
'target_date',
'priority',
'state',
'app',
'created_by',
'assignees',
'created_at',
'updated_at',
])
->with([
'appName:id,name_fa,type',
'creator:id,name',
])
->orderByRaw('target_date IS NULL')
->orderBy('target_date', 'asc')
->orderBy('updated_at', 'desc');
if (!$isAdmin) {
$query->where(function ($q) use ($user) {
$q->where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id);
});
}
$tasks = $query->get();
$taskIds = $tasks->pluck('id');
$assigneeIds = [];
foreach ($tasks as $task) {
$ids = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
if (is_array($ids)) {
$assigneeIds = array_merge($assigneeIds, array_map('strval', $ids));
}
}
$usersById = !empty($assigneeIds)
? User::whereIn('id', array_unique($assigneeIds))
->get()
->mapWithKeys(fn ($user) => [(string)$user->id => $user->name])
->all()
: [];
if ($taskIds->isEmpty()) {
return [
'tasks' => $tasks,
'tasksPayload' => [],
'assigneeFilterOptions' => [],
];
}
$activeQuery = TaskDuration::query()
->select(['task_id'])
->whereIn('task_id', $taskIds)
->whereNull('end');
if (!$isAdmin) {
$activeQuery->where('user_id', $user->id);
}
$activeTaskIds = $activeQuery->pluck('task_id')->flip();
$durationQuery = TaskDuration::query()
->select(['task_id', 'user_id', 'duration'])
->whereIn('task_id', $taskIds)
->whereNotNull('duration');
if (!$isAdmin) {
$durationQuery->where('user_id', $user->id);
}
$durationsByTaskId = $durationQuery->get()->groupBy('task_id');
foreach ($tasks as $task) {
$task->active = $activeTaskIds->has($task->id);
$task->creator_name = $task->creator?->name ?? 'نامشخص';
$task->assignee_names = $this->resolveAssigneeNames($task, $usersById);
$ids = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees;
$task->assignee_ids = is_array($ids) ? array_map('strval', $ids) : [];
$totalSeconds = $this->sumDurationSeconds(
$durationsByTaskId->get($task->id, collect()),
$isAdmin ? null : $user->id
);
$task->total_duration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
}
$assigneeFilterOptions = collect($usersById)
->map(fn ($name, $id) => ['id' => (string)$id, 'name' => $name])
->sortBy('name', SORT_NATURAL)
->values()
->all();
return [
'tasks' => $tasks,
'tasksPayload' => $this->buildTasksPayload($tasks, $usersById),
'assigneeFilterOptions' => $assigneeFilterOptions,
];
}
// public function tasks()
// {
// $user = User::where('user_name', $_COOKIE['user_identity'])->first();
@@ -135,61 +365,27 @@ class TaskController extends Controller
public function tasks()
{
if (!isset($_COOKIE['user_identity'])) {
return redirect('/')->with('message', [
'color' => 'red',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
if ($user) {
if ($user->is_admin == 1) {
$tasks = Tasks::with(['duration', 'comments.user', 'appName'])
->orderByRaw('target_date IS NULL')
->orderBy('target_date', 'asc')
->orderBy('updated_at', 'desc')
->get();
// dd($tasks);
foreach ($tasks as $task) {
$totalSeconds = 0;
foreach ($task->duration as $item) {
if ($item->duration !== null) {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
}
}
$task->totalDuration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
$active_task = TaskDuration::where('task_id', $task->id)->orderBy('start', 'desc')->first();
$task->active = !empty($active_task) && $active_task->end == null;
}
} else {
$tasks = Tasks::with(['duration', 'comments.user'])->where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id)
// ->orderByRaw('target_date IS NULL')
// ->orderBy('target_date', 'asc')
->orderBy('updated_at', 'desc')
->get();
// dd($tasks);
foreach ($tasks as $task) {
$totalSeconds = 0;
foreach ($task->duration as $item) {
if ($user->id == $item->user_id && $item->duration !== null) {
$totalSeconds += CarbonInterval::createFromFormat('H:i:s', $item->duration)->totalSeconds;
}
}
$task->totalDuration = CarbonInterval::seconds($totalSeconds)->cascade()->format('%H:%I:%S');
$active_task = TaskDuration::where('task_id', $task->id)->where('user_id', $user->id)->orderBy('start', 'desc')->first();
$task->active = !empty($active_task) && $active_task->end == null;
}
}
$count = count($tasks);
// dd((new Tasks())->filterByState($tasks)['backlog'][0], 'count ='.$count);
['tasks' => $tasks, 'tasksPayload' => $tasksPayload, 'assigneeFilterOptions' => $assigneeFilterOptions] = $this->loadTasksPageData($user);
$count = $tasks->count();
return view('tasks.tasks', [
'tasks' => (new Tasks())->filterByState($tasks),
'tasksPayload' => $tasksPayload,
'assigneeFilterOptions' => $assigneeFilterOptions,
'count' => $count,
'user_id' => $user->id,
'isAdminLevel1' => $user->is_admin == 1,
'mentionUsers' => User::select('id', 'name', 'user_name')->get(),
'appsList' => App::select('id', 'name_fa')->get(),
'appsList' => App::select('id', 'name_fa', 'type')->get(),
'priorityLabels' => [
'urgent' => 'فوری',
'high' => 'بالا',
@@ -220,6 +416,37 @@ class TaskController extends Controller
}
public function taskDetailsJson($id)
{
if (!isset($_COOKIE['user_identity'])) {
return response()->json(['error' => 'Unauthorized'], 401);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
if (!$user) {
return response()->json(['error' => 'User not found'], 404);
}
$isAdmin = $user->is_admin == 1;
$task = Tasks::query()
->with([
'creator:id,name',
'comments.user:id,name',
'duration' => function ($q) use ($isAdmin, $user) {
if (!$isAdmin) {
$q->where('user_id', $user->id);
}
},
])
->find($id);
if (!$task || !$this->userCanAccessTask($task, $user)) {
return response()->json(['error' => 'Not found'], 404);
}
return response()->json($this->buildTaskDetailPayload($task, $isAdmin, $user));
}
public function editTask(Request $request)
{
// dd($request->all());
@@ -342,6 +569,10 @@ class TaskController extends Controller
public function activeTask(Request $request)
{
// dd($request);
if (!isset($_COOKIE['user_identity'])) {
return response()->json(['action' => 'unauthorized', 'status' => false], 401);
}
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$task = Tasks::find($request->task_id);
if (!$task) {