diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 1132f5d9..f9276a17 100755 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -22,24 +22,154 @@ use App\Models\Pivot_Tasks; class AdminController extends Controller { - private function loadPendingRequests($users): array + private function redisIsAvailable(): bool { - $records = []; - try { Redis::connection()->ping(); + + return true; } catch (\Throwable) { - foreach ($users as $user) { - $records[] = [ - 'user_name' => $user->user_name, - 'name' => $user->name, - 'attendances' => [], - ]; + return false; + } + } + + private function appendDecodedRequest(array &$target, ?string $raw): void + { + if (!$raw) { + return; + } + + $request = json_decode($raw); + if ($request && ($request->function ?? null) !== 'rejected') { + $target[] = $request; + } + } + + private function appendDecodedRequestWithAttendance(array &$target, ?string $raw, ?Attendance $attendance = null): void + { + if (!$raw) { + return; + } + + $request = json_decode($raw); + if ($request && ($request->function ?? null) !== 'rejected') { + $target[] = $this->enrichRequestForView($request, $attendance); + } + } + + private function buildFallbackRequestFromAttendance(Attendance $attendance): object + { + $request = (object) [ + 'function' => 'edit', + 'userid' => $attendance->user_identity, + 'id' => $attendance->id, + 'oldenter' => $attendance->entered_at ? Carbon::parse($attendance->entered_at)->format('H:i') : '', + 'newenter' => '', + 'oldexit' => $attendance->exited_at ? Carbon::parse($attendance->exited_at)->format('H:i') : '', + 'newexit' => '', + 'date' => $attendance->entered_at + ? Jalalian::fromCarbon(Carbon::parse($attendance->entered_at))->format('Y/m/d') + : null, + ]; + + $request->display_date = $this->formatRequestDisplayDate($request, $attendance); + + return $request; + } + + private function formatRequestDisplayDate(object $request, ?Attendance $attendance = null): string + { + $candidates = array_filter([ + $attendance?->entered_at, + (!empty($request->oldenter) && str_contains((string) $request->oldenter, ' ')) + ? $request->oldenter + : null, + $request->date ?? null, + ]); + + foreach ($candidates as $value) { + try { + if (is_string($value) && preg_match('/^\d{4}\/\d{2}\/\d{2}$/', $value)) { + $year = (int) substr($value, 0, 4); + + if ($year >= 1700) { + return Jalalian::fromCarbon(Carbon::createFromFormat('Y/m/d', $value))->format('Y/m/d'); + } + + if ($year >= 1300) { + return Jalalian::fromFormat('Y/m/d', $value)->format('Y/m/d'); + } + } + + $carbon = Carbon::parse($value); + if ($carbon->year >= 1000) { + return Jalalian::fromCarbon($carbon)->format('Y/m/d'); + } + } catch (\Throwable) { + continue; + } + } + + return '-'; + } + + private function enrichRequestForView(object $request, ?Attendance $attendance = null): object + { + if (!isset($request->display_date)) { + $request->display_date = $this->formatRequestDisplayDate($request, $attendance); + } + + return $request; + } + + private function resolveRequestTime(?string $preferred, ?string $fallback, $recordDefault = null): ?string + { + $value = ($preferred !== null && $preferred !== '') ? $preferred : $fallback; + if ($value === null || $value === '') { + if ($recordDefault) { + return Carbon::parse($recordDefault)->format('H:i:s'); } - return $records; + return null; } + return Carbon::parse($value)->format('H:i:s'); + } + + private function buildAttendanceTimestamp(string $date, ?string $time): ?string + { + if (!$time) { + return null; + } + + return $date . ' ' . $time; + } + + private function decrementUserRequestCount(?string $userName): void + { + if (!$userName) { + return; + } + + User::where('user_name', $userName) + ->where('req_counts', '>', 0) + ->decrement('req_counts'); + } + + private function loadPendingRequests($users): array + { + $userNames = $users->pluck('user_name')->filter()->values(); + $redisAvailable = $this->redisIsAvailable(); + + $pendingByUser = Attendance::query() + ->whereIn('user_identity', $userNames) + ->whereNotNull('requested') + ->where('requested', '!=', '') + ->get(['id', 'user_identity', 'entered_at', 'exited_at']) + ->groupBy('user_identity'); + + $records = []; + foreach ($users as $user) { $rec = [ 'user_name' => $user->user_name, @@ -47,28 +177,58 @@ class AdminController extends Controller 'attendances' => [], ]; - foreach (Redis::keys("att-tempreq:{$user->user_name}:*") as $key) { - if (str_contains($key, ':creates:')) { - continue; + $pendingForUser = $pendingByUser->get($user->user_name, collect()); + $hasRequestHint = ($user->req_counts ?? 0) > 0 || $pendingForUser->isNotEmpty(); + + if (!$hasRequestHint) { + $records[] = $rec; + continue; + } + + $loadedIds = []; + + if ($redisAvailable) { + foreach (Redis::lrange("att-tempreq:creates:{$user->user_name}", 0, -1) as $reqJson) { + $this->appendDecodedRequestWithAttendance($rec['attendances'], $reqJson); } - $raw = Redis::get($key); - if (!$raw) { - continue; - } + $this->appendDecodedRequestWithAttendance($rec['attendances'], Redis::get("att-tempreq:{$user->user_name}:new")); - $get = json_decode($raw); - if ($get && ($get->function ?? null) !== 'rejected') { - $rec['attendances'][] = $get; + if ($pendingForUser->isNotEmpty()) { + $redisKeys = $pendingForUser + ->map(fn ($attendance) => "att-tempreq:{$user->user_name}:{$attendance->id}") + ->values() + ->all(); + + $rawValues = Redis::mget($redisKeys); + + foreach ($pendingForUser->values() as $index => $attendance) { + $raw = $rawValues[$index] ?? null; + if (!$raw) { + continue; + } + + $beforeCount = count($rec['attendances']); + $this->appendDecodedRequestWithAttendance($rec['attendances'], $raw, $attendance); + if (count($rec['attendances']) > $beforeCount) { + $loadedIds[(string) $attendance->id] = true; + } + } } } - $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; + foreach ($pendingForUser as $attendance) { + if (isset($loadedIds[(string) $attendance->id])) { + continue; } + + $rec['attendances'][] = $this->buildFallbackRequestFromAttendance($attendance); + } + + $pendingById = $pendingForUser->keyBy('id'); + foreach ($rec['attendances'] as $index => $request) { + $attendance = $pendingById->get($request->id ?? null); + $rec['attendances'][$index] = $this->enrichRequestForView($request, $attendance); } $records[] = $rec; @@ -354,6 +514,7 @@ class AdminController extends Controller // Redis::connection()->lrem($create_requests_key, 1, json_encode($data)); Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}"); + $this->decrementUserRequestCount($data->userid); return redirect()->back()->with('message', [ 'color' => '#6aff00ff', 'text' => 'رکورد با موفقیت حذف شد✔️' @@ -371,43 +532,45 @@ class AdminController extends Controller ]); } } elseif ($data->function == 'edit') { -// dd($data); $record = Attendance::find($data->id); + if (!$record) { + return redirect()->back()->with('message', [ + 'color' => '#ff8800ff', + 'text' => 'رکورد پیدا نشد✖️' + ]); + } + $attendance = new Attendance(); $date = Carbon::parse($record->entered_at)->format('Y-m-d'); + $enterTime = $this->resolveRequestTime($data->newenter ?? null, $data->oldenter ?? null, $record->entered_at); + $exitTime = $this->resolveRequestTime($data->newexit ?? null, $data->oldexit ?? null, $record->exited_at); + $record->ip_address = 'admin-edited'; $record->user_identity = $data->userid; - $record->entered_at = $data->newenter ? $date . ' ' . $data->newenter : null; - if ($data->newexit != null) { - //جریمه -// if($data->newexit > '16:00:00'){ -// $data->newexit = date('H:i:s', strtotime($data->newexit . ' -1 hour')); -// } - - $record->exited_at = $date . ' ' . $data->newexit; - $duration = $attendance->duration($data->newenter, $data->newexit); + $record->entered_at = $this->buildAttendanceTimestamp($date, $enterTime) ?? $record->entered_at; + if ($exitTime) { + $record->exited_at = $this->buildAttendanceTimestamp($date, $exitTime); + $duration = $attendance->duration($enterTime, $exitTime); } else { $record->exited_at = null; - $record->duration = null; + $duration = null; } + $break = AttendanceBreak::where('attendance_id', $data->id)->sum('duration'); - if ($break) { + if ($break && $duration) { $breakSec = strtotime("1970-01-01 " . $break) - strtotime("1970-01-01 00:00:00"); $durationSec = strtotime("1970-01-01 " . $duration) - strtotime("1970-01-01 00:00:00"); $durationSec -= $breakSec; $duration = gmdate("H:i:s", $durationSec); - } + $record->duration = $duration ?? $record->duration; -//dd($record->duration, $duration, $break); $record->requested = ''; $record->save(); -// $create_requests_key = "att-tempreq:creates:{$data->userid}"; -// $create_requests_key = "att-tempreq:{$data->userid}:{$data->id}"; -// $t = Redis::connection()->lrem($create_requests_key, -1, json_encode($data)); Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}"); + $this->decrementUserRequestCount($data->userid); tel_notif($record->user_identity, "تایید"); @@ -416,21 +579,33 @@ class AdminController extends Controller "text" => 'رکورد با موفقیت ذخیره شد.✔️' ]); } elseif ($data->function == 'new') { -// dd($data); - $starttime = $data->oldenter ? Carbon::parse($data->oldenter)->format('H:i:s') : null; - $endtime = $data->oldexit ? Carbon::parse($data->oldexit)->format('H:i:s') : null; + $enteredAt = !empty($data->oldenter) ? Carbon::parse($data->oldenter) : null; + $exitedAt = !empty($data->oldexit) ? Carbon::parse($data->oldexit) : null; + + if (!$enteredAt) { + return redirect()->back()->with('message', [ + 'color' => '#ff8800ff', + 'text' => 'زمان ورود در درخواست مشخص نیست✖️' + ]); + } + + $starttime = $enteredAt->format('H:i:s'); + $endtime = $exitedAt?->format('H:i:s'); + if (is_null($endtime) || $endtime > $starttime) { $attendance = new Attendance(); $attendance->ip_address = 'admin-new'; $attendance->user_identity = $data->userid; - $attendance->entered_at = $data->oldenter ?: null; - $attendance->exited_at = $data->oldexit ?: null; - $attendance->duration = $data->oldexit ? $attendance->duration($data->oldenter, $data->oldexit) : null; + $attendance->entered_at = $enteredAt->format('Y-m-d H:i:s'); + $attendance->exited_at = $exitedAt?->format('Y-m-d H:i:s'); + $attendance->duration = $endtime + ? $attendance->duration($starttime, $endtime) + : null; $attendance->save(); $create_requests_key = "att-tempreq:creates:{$data->userid}"; Redis::connection()->lrem($create_requests_key, 1, json_encode($data)); + $this->decrementUserRequestCount($data->userid); -// Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}"); tel_notif($data->userid, "تایید"); return redirect()->back()->with('message', [ 'color' => '#6aff00ff', @@ -456,6 +631,7 @@ class AdminController extends Controller $att->save(); } tel_notif($data->userid, "رد"); + $this->decrementUserRequestCount($data->userid); return redirect()->back()->with('message', [ 'color' => '#6aff00ff', "text" => 'درخواست با موفقیت رد شد.✔️' diff --git a/app/Http/Controllers/TaskController.php b/app/Http/Controllers/TaskController.php index af249f27..f96a5f2f 100755 --- a/app/Http/Controllers/TaskController.php +++ b/app/Http/Controllers/TaskController.php @@ -122,6 +122,28 @@ class TaskController extends Controller return $totalSeconds; } + private function resolveTaskFinishedAt($durations): ?string + { + $latest = collect($durations) + ->pluck('end') + ->filter() + ->map(fn ($value) => Carbon::parse($value)) + ->sortDesc() + ->first(); + + return $latest?->toDateTimeString(); + } + + private function resolveTaskFinishedAtByTaskId(int $taskId): ?string + { + return $this->resolveTaskFinishedAt( + TaskDuration::query() + ->where('task_id', $taskId) + ->whereNotNull('end') + ->get(['end']) + ); + } + private function resolveAssigneeNames(Tasks $task, array $usersById = []): array { $assignees = is_string($task->assignees) ? json_decode($task->assignees, true) : $task->assignees; @@ -155,6 +177,7 @@ class TaskController extends Controller 'description' => $task->description, 'start_date' => $task->start_date, 'target_date' => $task->target_date, + 'finished_at' => $task->finished_at ?? null, 'priority' => $task->priority, 'state' => $task->state, 'app' => $task->app, @@ -200,6 +223,7 @@ class TaskController extends Controller 'description' => $task->description, 'start_date' => $task->start_date, 'target_date' => $task->target_date, + 'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id), 'priority' => $task->priority, 'state' => $task->state, 'app' => $task->app, @@ -295,7 +319,7 @@ class TaskController extends Controller $activeTaskIds = $activeQuery->pluck('task_id')->flip(); $durationQuery = TaskDuration::query() - ->select(['task_id', 'user_id', 'duration']) + ->select(['task_id', 'user_id', 'duration', 'end']) ->whereIn('task_id', $taskIds) ->whereNotNull('duration'); @@ -305,12 +329,20 @@ class TaskController extends Controller $durationsByTaskId = $durationQuery->get()->groupBy('task_id'); + $finishedAtByTaskId = TaskDuration::query() + ->select(['task_id', 'end']) + ->whereIn('task_id', $taskIds) + ->whereNotNull('end') + ->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) : []; + $task->finished_at = $this->resolveTaskFinishedAt($finishedAtByTaskId->get($task->id, collect())); $totalSeconds = $this->sumDurationSeconds( $durationsByTaskId->get($task->id, collect()), $isAdmin ? null : $user->id @@ -475,7 +507,14 @@ class TaskController extends Controller } else { unset($data['assignees']); } - unset($data['_token'], $data['id'], $data['start_date_display'], $data['target_date_display']); + unset( + $data['_token'], + $data['id'], + $data['start_date_display'], + $data['target_date_display'], + $data['completed_at'], + $data['completed_at_display'] + ); $task->update($data); $after = $task->toArray(); @@ -626,6 +665,7 @@ class TaskController extends Controller 'duration' => $duration, 'start' => $taskDU->start, 'end' => $taskDU->end, + 'finished_at' => $this->resolveTaskFinishedAtByTaskId($task->id), ]); } else return response()->json([ diff --git a/resources/views/admin/admin.blade.php b/resources/views/admin/admin.blade.php index 2b95655d..3a649518 100755 --- a/resources/views/admin/admin.blade.php +++ b/resources/views/admin/admin.blade.php @@ -12,7 +12,7 @@ $usersChunked = $users->chunk(3);
درخواست ها
@foreach ($reqs as $u) @@ -21,9 +21,10 @@ $usersChunked = $users->chunk(3); class="ui-chip-btn" onclick="togglerequests('attendance-{{ $loop->index }}')"> {{ $u['name'] }} + ({{ count($u['attendances']) }}) - @endif -| {{ !empty($rec->date) ? Jalalian::fromCarbon(Carbon::parse($rec->date))->format('Y/m/d') - : Jalalian::fromCarbon(Carbon::parse($rec->oldenter))->format('Y/m/d')}} | +{{ $rec->display_date ?? '-' }} | @php - $oldEnter = $rec->oldenter ? Carbon::parse($rec->oldenter)->format('H:i') : null; - $newEnter = $rec->newenter ? Carbon::parse($rec->newenter)->format('H:i') : null; - $oldExit = $rec->oldexit ? Carbon::parse($rec->oldexit)->format('H:i') : null; - $newExit = $rec->newexit ? Carbon::parse($rec->newexit)->format('H:i') : null; + $parseTime = function ($value) { + if (!$value) { + return null; + } + try { + return Carbon::parse($value)->format('H:i'); + } catch (\Throwable) { + return is_string($value) && preg_match('/^\d{1,2}:\d{2}/', $value) + ? substr($value, 0, 5) + : null; + } + }; + $oldEnter = $parseTime($rec->oldenter ?? null); + $newEnter = $parseTime($rec->newenter ?? null); + $oldExit = $parseTime($rec->oldexit ?? null); + $newExit = $parseTime($rec->newexit ?? null); @endphp @if( $oldEnter == $newEnter || $newEnter == null) @@ -74,8 +86,8 @@ $usersChunked = $users->chunk(3);
درخواستی ثبت نشده
--}}