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
+147 -41
View File
@@ -22,6 +22,128 @@ use App\Models\Pivot_Tasks;
class AdminController extends Controller
{
private function loadPendingRequests($users): array
{
$records = [];
try {
Redis::connection()->ping();
} catch (\Throwable) {
foreach ($users as $user) {
$records[] = [
'user_name' => $user->user_name,
'name' => $user->name,
'attendances' => [],
];
}
return $records;
}
foreach ($users as $user) {
$rec = [
'user_name' => $user->user_name,
'name' => $user->name,
'attendances' => [],
];
foreach (Redis::keys("att-tempreq:{$user->user_name}:*") as $key) {
if (str_contains($key, ':creates:')) {
continue;
}
$raw = Redis::get($key);
if (!$raw) {
continue;
}
$get = json_decode($raw);
if ($get && ($get->function ?? null) !== 'rejected') {
$rec['attendances'][] = $get;
}
}
$createRequests = Redis::lrange("att-tempreq:creates:{$user->user_name}", 0, -1);
foreach ($createRequests as $reqJson) {
$get = json_decode($reqJson);
if ($get && ($get->function ?? null) !== 'rejected') {
$rec['attendances'][] = $get;
}
}
$records[] = $rec;
}
return $records;
}
private function enrichUsersForDashboard($users): void
{
$userNames = $users->pluck('user_name');
$now = Carbon::now();
$monthStart = $now->copy()->startOfMonth();
$monthEnd = $now->copy()->endOfMonth();
$today = $now->format('Y-m-d');
$monthAttendances = Attendance::query()
->whereIn('user_identity', $userNames)
->whereBetween('entered_at', [$monthStart, $monthEnd])
->whereNotNull('duration')
->select(['user_identity', 'duration'])
->get()
->groupBy('user_identity');
$openAttendances = Attendance::query()
->whereIn('user_identity', $userNames)
->whereNull('exited_at')
->select(['id', 'user_identity', 'entered_at'])
->get()
->groupBy('user_identity');
$activeBreakAttendanceIds = AttendanceBreak::query()
->whereIn('attendance_id', $openAttendances->flatten()->pluck('id'))
->whereNull('break_end')
->pluck('attendance_id')
->flip();
foreach ($users as $user) {
$totalSeconds = 0;
foreach ($monthAttendances->get($user->user_name, []) as $attendance) {
if (!$attendance->duration) {
continue;
}
$parts = explode(':', $attendance->duration);
if (count($parts) !== 3) {
continue;
}
[$h, $m, $s] = array_map('intval', $parts);
$totalSeconds += ($h * 3600) + ($m * 60) + $s;
}
$hours = intdiv($totalSeconds, 3600);
$minutes = intdiv($totalSeconds % 3600, 60);
$user->totaltime = sprintf('%02d:%02d', $hours, $minutes);
$user->working = 0;
$openForUser = $openAttendances->get($user->user_name, collect());
if ($openForUser->isEmpty()) {
continue;
}
$latestOpen = $openForUser->sortByDesc('entered_at')->first();
if ($latestOpen && Carbon::parse($latestOpen->entered_at)->format('Y-m-d') === $today) {
$user->working = 1;
}
foreach ($openForUser as $openAttendance) {
if ($activeBreakAttendanceIds->has($openAttendance->id)) {
$user->working = 2;
break;
}
}
}
}
public function dashboard()
{
@@ -29,60 +151,44 @@ class AdminController extends Controller
$uname = $_COOKIE['token-0'];
setcookie("user_identity", $_COOKIE['token-0'], time() + (60 * 60 * 24 * 365), '/');
Cookie::queue(Cookie::forget('token-0'));
} else
$uname = $_COOKIE['user_identity'];
} else {
$uname = $_COOKIE['user_identity'] ?? null;
}
if (!$uname) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'ابتدا باید وارد شوید!'
]);
}
$u = User::select('name', 'is_admin')
->where('user_name', $uname)
->first();
if (!$u) {
return redirect('/')->with('message', [
'color' => '#ff8800ff',
'text' => 'کاربر یافت نشد!'
]);
}
if ($u->is_admin == 1) {
$provider = 'admin';
} elseif ($u->is_admin == 2) {
$provider = 'supervisor';
}
if ($u->is_admin == '1' || $u->is_admin == '2') {
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
$users = User::wherenotnull('user_name')
->with('attendances')
$users = User::whereNotNull('user_name')
->select(['id', 'name', 'user_name', 'req_counts', 'active_task'])
->get();
$records = [];
foreach ($users as $user) {
$rec = [
"user_name" => $user->user_name,
"name" => $user->name,
"attendances" => []
];
foreach ($user->attendances as $attendance) {
$key = "att-tempreq:{$user->user_name}:{$attendance->id}";
$get = json_decode(Redis::get("{$key}"));
$date = $attendance->entered_at ? Carbon::parse($attendance->entered_at)->format('Y-m-d') : null;
if ($get)
if ($get->function != 'rejected')
$rec['attendances'][] = $get;
}
// $key = "att-tempreq:{$user->user_name}:new";
$create_requests_key = "att-tempreq:creates:{$user->user_name}";
$create_requests = Redis::connection()->lrange($create_requests_key, 0, -1); // خواندن تمام آیتم های لیست
$this->enrichUsersForDashboard($users);
$users = $users->sortByDesc('working');
$records = $this->loadPendingRequests($users);
foreach ($create_requests as $req_json) {
$get = json_decode($req_json);
if ($get && $get->function != 'rejected') {
$rec['attendances'][] = $get;
}
}
// $get = json_decode(Redis::get("{$key}"));
// if ($get) {
//// dd($get);
//
// if ($get->function != 'rejected')
//
// $rec['attendances'][] = $get;
// }
$records[] = $rec;
}
// dd($users);
// dd(json_decode($json,true));
return view("admin..admin")->with(['message', [
'color' => '#ff8800ff',
"text" => 'شما به پنل ادمین وارد شدید✔️'
+42 -30
View File
@@ -26,14 +26,14 @@ class AttendanceController extends Controller
public function index()
{
if (isset($_COOKIE['user_identity'])) {
$username = $_COOKIE['user_identity'];
$attendanceSince = Carbon::now()->subMonths(12)->startOfMonth()->format('Y-m-d 00:00:00');
$Attendance = isset($_COOKIE['user_identity'])
? ($_COOKIE['user_identity'] == 1370
? Attendance::all()
: Attendance::where('user_identity', $_COOKIE['user_identity'])->get())
: collect();
$Attendance = $username == 1370
? Attendance::where('entered_at', '>=', $attendanceSince)->get()
: Attendance::where('user_identity', $username)
->where('entered_at', '>=', $attendanceSince)
->get();
$jalalian_object = Jalalian::now();
@@ -48,23 +48,27 @@ class AttendanceController extends Controller
->orderByDesc('id')
->first()
?? ai::where('key', 'angizeh')->orderByDesc('id')->first();
$ids = array_merge($ids, [$s->id]);
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400);
if ($s) {
$ids = array_merge($ids, [$s->id]);
Cookie::queue('seen_angizeh_ids', json_encode($ids), 14400);
}
//total time
$now = Jalalian::fromCarbon(Carbon::now());
$tsec = 0;
//dd(->user_profile);
if (isset($_COOKIE['token01'])) {
$a = json_decode($_COOKIE['token01']);
$user = User::where('user_name', $a->user_profile)->with('attendances')->first();
$user = User::where('user_name', $a->user_profile)
->with(['attendances' => fn($q) => $q->where('entered_at', '>=', $attendanceSince)])
->first();
} else {
$user = isset($_COOKIE['user_identity']) ? User::where('user_name', $_COOKIE['user_identity'])->with('attendances')->first() : null;
$user = User::where('user_name', $username)
->with(['attendances' => fn($q) => $q->where('entered_at', '>=', $attendanceSince)])
->first();
}
//dd($user);
if (!empty($user->attendances)) {
if ($user && !empty($user->attendances)) {
foreach ($user->attendances as $att) {
$date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($att->entered_at)) : null;
$att->date = $date != null ? $date->format('Y/m/d') : null;
@@ -83,33 +87,41 @@ class AttendanceController extends Controller
$formaction = (isset($_COOKIE['ad']) && $_COOKIE['ad'] == 1)
? 'admin/'
: 'attendance/';
$now = Carbon::now()->format('Y-m-d 00:00:00');
$existed = $Attendance->whereNull('exited_at')->where('entered_at', '>=', now()->format('Y-m-d 00:00:00'))->where('entered_at', '<=', now()->format('Y-m-d 23:59:59'))->first();
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
$until_now = null;
if ($user->break == 1) {
$att = Attendance::whereNull('exited_at')->latest('entered_at')->first();
$break = AttendanceBreak::where('attendance_id', $att->id)
->whereNull('break_end')
->latest('break_start')
$duration = null;
if ($user && $user->break == 1) {
$att = Attendance::where('user_identity', $user->user_name)
->whereNull('exited_at')
->latest('entered_at')
->first();
if ($break)
$break = $att
? AttendanceBreak::where('attendance_id', $att->id)
->whereNull('break_end')
->latest('break_start')
->first()
: null;
if ($break) {
$until_now = (int)Carbon::parse($break->break_start)->diffInSeconds(now(), false);
// $until_now = Carbon::now()->diffInSeconds(Carbon::parse($break->break_start));
$duration = $att->duration($break->break_start, now()->format('Y-m-d H:i:s'));
}
}
$user->tasks = Tasks::where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id)
->orderBy('target_date', 'desc')
->get();
//dd($user->tasks);
// dd($until_now);
if ($user) {
$user->tasks = Tasks::where('created_by', $user->id)
->orWhereJsonContains('assignees', (string)$user->id)
->orderBy('target_date', 'desc')
->limit(50)
->get();
}
return view('attendance.index', compact('user'))->with([
's' => $s,
'existed' => $existed,
'formaction' => $formaction,
'breakDu' => $until_now ?? null,
'duration' => $duration,
]);
} else {
foreach (request()->cookies as $name => $value) {
+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) {