From ce2ce49ffb438a6cf4a714594643ad785731281e Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 7 Oct 2026 17:51:03 +0330 Subject: [PATCH] Editing Employee Attendance and Check-ins --- app/Http/Controllers/AdminController.php | 2 +- app/Http/Controllers/AttendanceController.php | 65 ++++++++++++++++++- app/Models/User.php | 1 + app/Services/LatePenaltyService.php | 32 ++++++++- ...001_add_work_start_time_to_users_table.php | 22 +++++++ resources/views/admin/admin.blade.php | 3 + resources/views/attendance/profile.blade.php | 15 +++++ 7 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_10_07_000001_add_work_start_time_to_users_table.php diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 15abb3c1..64bca80e 100755 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -401,7 +401,7 @@ class AdminController extends Controller if ($u->is_admin == '1' || $u->is_admin == '2') { setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/"); $users = User::whereNotNull('user_name') - ->select(['id', 'name', 'user_name', 'req_counts', 'active_task']) + ->select(['id', 'name', 'user_name', 'req_counts', 'active_task', 'work_start_time']) ->get(); $this->enrichUsersForDashboard($users); diff --git a/app/Http/Controllers/AttendanceController.php b/app/Http/Controllers/AttendanceController.php index 0cd0ce58..65608431 100755 --- a/app/Http/Controllers/AttendanceController.php +++ b/app/Http/Controllers/AttendanceController.php @@ -609,18 +609,38 @@ class AttendanceController extends Controller } + private function viewerIsAdmin(): bool + { + $viewer = User::where('user_name', $_COOKIE['user_identity'] ?? '')->first(); + + return $viewer && in_array((string) $viewer->is_admin, ['1', '2'], true); + } + public function profile(Request $request) { $username = $request->query('username') ?: $_COOKIE['user_identity']; $user = User::where('user_name', $username)->first(); - return view('attendance.profile', ['user' => $user]); + return view('attendance.profile', [ + 'user' => $user, + 'viewerIsAdmin' => $this->viewerIsAdmin(), + ]); } public function updateProfile(Request $request) { -// dd($request->all()); - $user = User::where('user_name', $_COOKIE['user_identity'])->first(); + $viewerIsAdmin = $this->viewerIsAdmin(); + $targetUsername = ($viewerIsAdmin && $request->filled('target_username')) + ? $request->input('target_username') + : $_COOKIE['user_identity']; + $user = User::where('user_name', $targetUsername)->first(); + + if (!$user) { + return redirect()->back()->with('message', [ + 'color' => '#ff8800ff', + 'text' => 'کاربر یافت نشد!' + ]); + } if ($user->password != $request->password) { $request->validate([ @@ -645,14 +665,53 @@ class AttendanceController extends Controller $user->is_admin = $request->role == 1 ? 1 : null; } + $workStartChanged = false; + if ($viewerIsAdmin && $request->has('work_start_time')) { + $raw = trim((string) $this->fa2en_numbers($request->input('work_start_time'))); + if ($raw === '') { + $newStart = null; + } elseif (preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', $raw, $m)) { + $newStart = sprintf('%02d:%02d:00', (int) $m[1], (int) $m[2]); + } else { + return redirect()->back()->with('message', [ + 'color' => '#ff4400ff', + 'text' => 'ساعت شروع کار نامعتبر است (مثال: 15:00).' + ]); + } + + $oldStart = $user->work_start_time ? substr((string) $user->work_start_time, 0, 8) : null; + if ($oldStart !== $newStart) { + $user->work_start_time = $newStart; + $workStartChanged = true; + } + } + $user->save(); + if ($workStartChanged) { + $this->resyncCurrentMonthLate($user->user_name); + } + return redirect()->back()->with('message', [ 'color' => '#00c300ff', 'text' => 'اطلاعات با موفقیت ویرایش شد.' ]); } + private function resyncCurrentMonthLate(string $userIdentity): void + { + $service = app(LatePenaltyService::class); + $service->forgetWorkStart($userIdentity); + [$monthStart, $monthEnd] = $service->jalaliMonthBounds(Carbon::now()); + + Attendance::query() + ->where('user_identity', $userIdentity) + ->whereBetween('entered_at', [$monthStart, $monthEnd]) + ->orderBy('entered_at') + ->get() + ->each(fn (Attendance $attendance) => $service->syncAttendance($attendance)); + } + private function dailyReport($userId, $userName) { $tasks = Tasks::with(['duration']) diff --git a/app/Models/User.php b/app/Models/User.php index 5e5dbcdd..f6acd562 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -30,6 +30,7 @@ class User extends Authenticatable 'name', 'number', 'birthday', + 'work_start_time', 'password', 'user_name', 'is_admin', diff --git a/app/Services/LatePenaltyService.php b/app/Services/LatePenaltyService.php index 6eba1ceb..b6626932 100644 --- a/app/Services/LatePenaltyService.php +++ b/app/Services/LatePenaltyService.php @@ -19,6 +19,34 @@ class LatePenaltyService public const WEEKLY_ALLOWANCE_MINUTES = 15; public const FINE_PER_MINUTE = 2000; + /** @var array */ + private array $workStartCache = []; + + /** ساعت شروع کار کاربر (H:i)؛ اگر ادمین تعیین نکرده باشد ۰۸:۳۰ */ + public function workStartFor(?string $userIdentity): string + { + if (!$userIdentity) { + return self::WORK_START; + } + + if (!array_key_exists($userIdentity, $this->workStartCache)) { + $value = null; + try { + $value = User::where('user_name', $userIdentity)->value('work_start_time'); + } catch (\Throwable $e) { + } + + $this->workStartCache[$userIdentity] = $value ? substr((string) $value, 0, 5) : self::WORK_START; + } + + return $this->workStartCache[$userIdentity]; + } + + public function forgetWorkStart(string $userIdentity): void + { + unset($this->workStartCache[$userIdentity]); + } + /** جمعه و تعطیلات رسمی روز کاری نیستند؛ ورود در این روزها اضافه‌کاری است. */ public function isWorkDay(Carbon $date): bool { @@ -59,7 +87,7 @@ class LatePenaltyService ]; } - $threshold = $entered->copy()->setTimeFromTimeString(self::WORK_START)->startOfMinute(); + $threshold = $entered->copy()->setTimeFromTimeString($this->workStartFor($userIdentity))->startOfMinute(); if ($userIdentity) { $leaveThreshold = $this->approvedLeaveWorkThreshold($userIdentity, $entered); @@ -104,7 +132,7 @@ class LatePenaltyService public function approvedLeaveWorkThreshold(string $userIdentity, Carbon $entered): Carbon|false|null { $day = $entered->toDateString(); - $workStart = $entered->copy()->setTimeFromTimeString(self::WORK_START)->startOfMinute(); + $workStart = $entered->copy()->setTimeFromTimeString($this->workStartFor($userIdentity))->startOfMinute(); $leaves = LeaveRequest::query() ->where('user_identity', $userIdentity) diff --git a/database/migrations/2026_10_07_000001_add_work_start_time_to_users_table.php b/database/migrations/2026_10_07_000001_add_work_start_time_to_users_table.php new file mode 100644 index 00000000..0b53f6a1 --- /dev/null +++ b/database/migrations/2026_10_07_000001_add_work_start_time_to_users_table.php @@ -0,0 +1,22 @@ +time('work_start_time')->nullable(); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('work_start_time'); + }); + } +}; diff --git a/resources/views/admin/admin.blade.php b/resources/views/admin/admin.blade.php index 55fc80a7..c9641062 100755 --- a/resources/views/admin/admin.blade.php +++ b/resources/views/admin/admin.blade.php @@ -198,6 +198,9 @@ $usersChunked = $users->chunk(3);

کارکرد ماه جاری: {{ $u->totaltime }}

جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان

تعداد درخواست ها: {{ $u->req_counts }}

+ @if($u->work_start_time) +

شروع کار: {{ substr((string) $u->work_start_time, 0, 5) }}

+ @endif @if($userType == 'admin')