Editing Employee Attendance and Check-ins

This commit is contained in:
2026-10-07 17:51:03 +03:30
parent 694f41968a
commit ce2ce49ffb
7 changed files with 134 additions and 6 deletions
+1 -1
View File
@@ -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);
+62 -3
View File
@@ -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'])
+1
View File
@@ -30,6 +30,7 @@ class User extends Authenticatable
'name',
'number',
'birthday',
'work_start_time',
'password',
'user_name',
'is_admin',
+30 -2
View File
@@ -19,6 +19,34 @@ class LatePenaltyService
public const WEEKLY_ALLOWANCE_MINUTES = 15;
public const FINE_PER_MINUTE = 2000;
/** @var array<string, string> */
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)
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->time('work_start_time')->nullable();
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('work_start_time');
});
}
};
+3
View File
@@ -198,6 +198,9 @@ $usersChunked = $users->chunk(3);
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
<p>جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان</p>
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
@if($u->work_start_time)
<p>شروع کار: {{ substr((string) $u->work_start_time, 0, 5) }}</p>
@endif
</a>
@if($userType == 'admin')
<button type="button"
@@ -19,6 +19,9 @@
{{-- align-center text-sm text-start">--}}
{{-- <div class="grid grid-cols-2 h-64 gap-2 justify-items-stretch content-between text-sm indent-2.5 align-middle divide">--}}
@csrf
@if(!empty($viewerIsAdmin))
<input type="hidden" name="target_username" value="{{ $user->user_name }}">
@endif
<label class="pb-3">نام و نام خانوادگی:</label>
<input type="text" name="name" value="{{ $user->name ?? '' }}"
@@ -71,6 +74,18 @@
<option value="1" {{ $user->is_admin == 1 ? 'selected' : '' }}>ادمین</option>
</select>
@endif
@if(!empty($viewerIsAdmin))
<label class="pb-3">ساعت شروع کار:</label>
<div class="flex flex-col gap-1">
<input type="time" name="work_start_time"
value="{{ $user->work_start_time ? substr((string) $user->work_start_time, 0, 5) : '' }}"
class="border border-gray-300 rounded-md text-center">
<span class="text-[11px] text-gray-500 indent-0">
خالی = پیش‌فرض {{ \App\Services\LatePenaltyService::WORK_START }}
</span>
</div>
@endif
<button type="submit"
class="col-span-2 bg-cyan-500 hover:bg-cyan-700 cursor-pointer
rounded-xl border border-400 justify-self-center