diff --git a/app/Console/Commands/RemindHolidayCalendarUpdate.php b/app/Console/Commands/RemindHolidayCalendarUpdate.php new file mode 100644 index 00000000..791d0f7c --- /dev/null +++ b/app/Console/Commands/RemindHolidayCalendarUpdate.php @@ -0,0 +1,89 @@ +warn('فایل تعطیلات خالی یا نامعتبر است.'); + + return self::FAILURE; + } + + $now = Jalalian::now(); + $currentYear = $now->getYear(); + + // از ۲۰ اسفند سال پوشش‌داده‌شده، یا هر زمان بعد از آن سال + $shouldRemind = $currentYear > $coveredUntil + || ($currentYear === $coveredUntil && $now->getMonth() === 12 && $now->getDay() >= 20); + + if (!$shouldRemind) { + $this->info("هنوز نیازی به یادآوری نیست (پوشش تا {$coveredUntil}، الان {$currentYear})."); + + return self::SUCCESS; + } + + $yearsToRequest = []; + for ($y = $coveredUntil + 1; $y <= max($currentYear + 1, $coveredUntil + 1); $y++) { + $yearsToRequest[] = $y; + } + $yearsToRequest = array_values(array_unique($yearsToRequest)); + $yearsLabel = implode(' و ', $yearsToRequest); + + $cacheKey = 'holidays_remind_' . implode('_', $yearsToRequest); + if (Cache::has($cacheKey)) { + $this->info('یادآوری اخیراً ارسال شده؛ این ماه دوباره ارسال نمی‌شود.'); + + return self::SUCCESS; + } + + $message = "📅 یادآوری تعطیلات رسمی\n\n" + . "پوشش تعطیلات در سیستم تا سال {$coveredUntil} است.\n" + . "لطفاً تعطیلات رسمی سال {$yearsLabel} را طبق تقویم رسمی ایران به فایل\n" + . "database/data/iranian_official_holidays.php\n" + . "اضافه کنید و سپس اجرا کنید:\n" + . "php artisan holidays:seed"; + + $this->notifyAdmins($message); + Cache::put($cacheKey, true, now()->addDays(30)); + + $this->info("یادآوری برای سال(های) {$yearsLabel} به ادمین‌ها ارسال شد."); + + return self::SUCCESS; + } + + private function notifyAdmins(string $message): void + { + $admins = User::query() + ->whereNotNull('is_admin') + ->whereNotNull('bale') + ->select('bale') + ->get(); + + foreach ($admins as $admin) { + try { + Http::withoutVerifying()->post( + 'https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage', + [ + 'chat_id' => $admin->bale, + 'text' => $message, + ] + ); + } catch (\Exception $e) { + } + } + } +} diff --git a/app/Console/Commands/SeedIranianHolidays.php b/app/Console/Commands/SeedIranianHolidays.php new file mode 100644 index 00000000..80d3aa0f --- /dev/null +++ b/app/Console/Commands/SeedIranianHolidays.php @@ -0,0 +1,71 @@ +error('فایل تعطیلات یافت نشد: ' . $path); + + return self::FAILURE; + } + + /** @var array> $years */ + $years = require $path; + $onlyYear = $this->option('year') !== null ? (int) $this->option('year') : null; + + $count = 0; + foreach ($years as $jalaliYear => $days) { + if ($onlyYear !== null && (int) $jalaliYear !== $onlyYear) { + continue; + } + + foreach ($days as $day) { + try { + $gregorian = (new Jalalian((int) $jalaliYear, (int) $day['month'], (int) $day['day'])) + ->toCarbon() + ->format('Y-m-d'); + } catch (\Throwable $e) { + $this->warn("رد شد: {$jalaliYear}/{$day['month']}/{$day['day']} — {$e->getMessage()}"); + continue; + } + + Holiday::updateOrCreate( + ['date' => $gregorian], + ['title' => $day['title']] + ); + $count++; + } + + $this->info("سال {$jalaliYear}: " . count($days) . ' روز'); + } + + $this->info("جمع: {$count} تعطیل رسمی ثبت/به‌روز شد."); + + return self::SUCCESS; + } + + /** آخرین سال شمسی پوشش‌داده‌شده در فایل داده */ + public static function coveredUntilYear(): int + { + $path = database_path('data/iranian_official_holidays.php'); + if (!is_file($path)) { + return 0; + } + + $years = array_keys(require $path); + + return $years ? (int) max($years) : 0; + } +} diff --git a/app/Http/Controllers/AdminController.php b/app/Http/Controllers/AdminController.php index 9032fbb4..15abb3c1 100755 --- a/app/Http/Controllers/AdminController.php +++ b/app/Http/Controllers/AdminController.php @@ -207,6 +207,17 @@ class AdminController extends Controller foreach ($pendingForUser->values() as $index => $attendance) { $raw = $rawValues[$index] ?? null; if (!$raw) { + // Redis موجود است ولی کلید نیست → درخواست قبلاً تایید/رد شده؛ پرچم کهنه را پاک کن + $attendance->requested = null; + $attendance->save(); + continue; + } + + $decoded = json_decode($raw); + if (($decoded->function ?? null) === 'rejected') { + Redis::del("att-tempreq:{$user->user_name}:{$attendance->id}"); + $attendance->requested = null; + $attendance->save(); continue; } @@ -219,12 +230,15 @@ class AdminController extends Controller } } - foreach ($pendingForUser as $attendance) { - if (isset($loadedIds[(string) $attendance->id])) { - continue; - } + // فقط وقتی Redis در دسترس نیست از fallback استفاده کن + if (!$redisAvailable) { + foreach ($pendingForUser as $attendance) { + if (isset($loadedIds[(string) $attendance->id])) { + continue; + } - $rec['attendances'][] = $this->buildFallbackRequestFromAttendance($attendance); + $rec['attendances'][] = $this->buildFallbackRequestFromAttendance($attendance); + } } $pendingById = $pendingForUser->keyBy('id'); @@ -239,6 +253,46 @@ class AdminController extends Controller return $records; } + private function removeCreateRequest(string $userid, object $data): void + { + $key = "att-tempreq:creates:{$userid}"; + try { + $items = Redis::lrange($key, 0, -1); + } catch (\Throwable) { + return; + } + + foreach ($items as $item) { + $decoded = json_decode($item); + if (!$decoded) { + continue; + } + + $sameUser = ($decoded->userid ?? null) === ($data->userid ?? null); + $sameEnter = (string) ($decoded->oldenter ?? '') === (string) ($data->oldenter ?? ''); + $sameExit = (string) ($decoded->oldexit ?? '') === (string) ($data->oldexit ?? ''); + + if ($sameUser && $sameEnter && $sameExit) { + Redis::lrem($key, 1, $item); + } + } + + Redis::del("att-tempreq:{$userid}:new"); + } + + private function clearAttendanceRequestFlag($attendanceId): void + { + if (!$attendanceId || $attendanceId === 'new') { + return; + } + + $att = Attendance::find($attendanceId); + if ($att) { + $att->requested = null; + $att->save(); + } + } + private function enrichUsersForDashboard($users): void { $userNames = $users->pluck('user_name'); @@ -579,7 +633,7 @@ class AdminController extends Controller } $record->duration = $duration ?? $record->duration; - $record->requested = ''; + $record->requested = null; $record->save(); app(LatePenaltyService::class)->syncAttendance($record); @@ -617,8 +671,7 @@ class AdminController extends Controller : null; $attendance->save(); app(LatePenaltyService::class)->syncAttendance($attendance); - $create_requests_key = "att-tempreq:creates:{$data->userid}"; - Redis::connection()->lrem($create_requests_key, 1, json_encode($data)); + $this->removeCreateRequest($data->userid, $data); $this->decrementUserRequestCount($data->userid); tel_notif($data->userid, "تایید"); @@ -633,18 +686,11 @@ class AdminController extends Controller if ($request->update == 'reject') { if ($data->function == 'delete' || $data->function == 'edit') { Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}"); + $this->clearAttendanceRequestFlag($data->id ?? null); } elseif ($data->function == 'new') { - $create_requests_key = "att-tempreq:creates:{$data->userid}"; - Redis::connection()->lrem($create_requests_key, 1, json_encode($data)); + $this->removeCreateRequest($data->userid, $data); } -// dd($data->id); - if ($data->id !== "new") { - $att = Attendance::find($data->id); -// dd($att); - $att->requested = null; - $att->save(); - } tel_notif($data->userid, "رد"); $this->decrementUserRequestCount($data->userid); return redirect()->back()->with('message', [ diff --git a/app/Models/Holiday.php b/app/Models/Holiday.php new file mode 100644 index 00000000..75f10ef8 --- /dev/null +++ b/app/Models/Holiday.php @@ -0,0 +1,20 @@ + 'date', + ]; + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 876399cf..5e5dbcdd 100755 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -111,16 +111,17 @@ class User extends Authenticatable if (!$date) { continue; } - $month = $date->format('%B'); + $key = sprintf('%04d-%02d', $date->getYear(), $date->getMonth()); - if (!isset($result[$month])) { - $result[$month] = []; + if (!isset($result[$key])) { + $result[$key] = []; } - $result[$month][] = $att; + $result[$key][] = $att; } } + krsort($result); return $result; } diff --git a/app/Services/LatePenaltyService.php b/app/Services/LatePenaltyService.php index 03a4cfb2..6eba1ceb 100644 --- a/app/Services/LatePenaltyService.php +++ b/app/Services/LatePenaltyService.php @@ -3,6 +3,7 @@ namespace App\Services; use App\Models\Attendance; +use App\Models\Holiday; use App\Models\LatePeriodFine; use App\Models\LeaveRequest; use App\Models\User; @@ -18,6 +19,22 @@ class LatePenaltyService public const WEEKLY_ALLOWANCE_MINUTES = 15; public const FINE_PER_MINUTE = 2000; + /** جمعه و تعطیلات رسمی روز کاری نیستند؛ ورود در این روزها اضافه‌کاری است. */ + public function isWorkDay(Carbon $date): bool + { + if ($date->isFriday()) { + return false; + } + + try { + return !Holiday::query() + ->whereDate('date', $date->toDateString()) + ->exists(); + } catch (\Throwable $e) { + return true; + } + } + public function calculateFromEnteredAt(?string $enteredAt, ?string $userIdentity = null): array { if (!$enteredAt) { @@ -31,6 +48,17 @@ class LatePenaltyService // فقط تا دقیقه؛ ثانیه نادیده گرفته می‌شود $entered = Carbon::parse($enteredAt)->startOfMinute(); + + // جمعه / تعطیل → بدون تأخیر و جریمه + if (!$this->isWorkDay($entered)) { + return [ + 'late_minutes' => 0, + 'grace_late_minutes' => 0, + 'daily_fine_minutes' => 0, + 'daily_fine_amount' => 0, + ]; + } + $threshold = $entered->copy()->setTimeFromTimeString(self::WORK_START)->startOfMinute(); if ($userIdentity) { diff --git a/database/data/iranian_official_holidays.php b/database/data/iranian_official_holidays.php new file mode 100644 index 00000000..2e28b226 --- /dev/null +++ b/database/data/iranian_official_holidays.php @@ -0,0 +1,89 @@ +> + */ +return [ + 1405 => [ + ['month' => 1, 'day' => 1, 'title' => 'آغاز نوروز / عید سعید فطر'], + ['month' => 1, 'day' => 2, 'title' => 'عید نوروز / تعطیل عید فطر'], + ['month' => 1, 'day' => 3, 'title' => 'عید نوروز'], + ['month' => 1, 'day' => 4, 'title' => 'عید نوروز'], + ['month' => 1, 'day' => 12, 'title' => 'روز جمهوری اسلامی ایران'], + ['month' => 1, 'day' => 13, 'title' => 'روز طبیعت (سیزده‌بدر)'], + ['month' => 1, 'day' => 24, 'title' => 'شهادت امام جعفر صادق (ع)'], + + ['month' => 3, 'day' => 3, 'title' => 'شهادت امام محمد باقر (ع)'], + ['month' => 3, 'day' => 6, 'title' => 'عید سعید قربان'], + ['month' => 3, 'day' => 14, 'title' => 'عید سعید غدیر خم / رحلت امام خمینی (ره)'], + ['month' => 3, 'day' => 15, 'title' => 'قیام ۱۵ خرداد'], + + ['month' => 4, 'day' => 3, 'title' => 'تاسوعای حسینی'], + ['month' => 4, 'day' => 4, 'title' => 'عاشورای حسینی'], + + ['month' => 5, 'day' => 13, 'title' => 'اربعین حسینی'], + ['month' => 5, 'day' => 21, 'title' => 'رحلت رسول اکرم (ص) و شهادت امام حسن مجتبی (ع)'], + ['month' => 5, 'day' => 22, 'title' => 'شهادت امام رضا (ع)'], + ['month' => 5, 'day' => 30, 'title' => 'شهادت امام حسن عسکری (ع)'], + + ['month' => 6, 'day' => 8, 'title' => 'میلاد رسول اکرم (ص) و امام جعفر صادق (ع)'], + + ['month' => 8, 'day' => 22, 'title' => 'شهادت حضرت فاطمه زهرا (س)'], + + ['month' => 10, 'day' => 2, 'title' => 'ولادت امام علی (ع) و روز پدر'], + ['month' => 10, 'day' => 16, 'title' => 'مبعث حضرت رسول اکرم (ص)'], + + ['month' => 11, 'day' => 4, 'title' => 'ولادت حضرت قائم (عج) / نیمه شعبان'], + ['month' => 11, 'day' => 22, 'title' => 'پیروزی انقلاب اسلامی ایران'], + + ['month' => 12, 'day' => 9, 'title' => 'شهادت امام علی (ع)'], + ['month' => 12, 'day' => 19, 'title' => 'عید سعید فطر'], + ['month' => 12, 'day' => 20, 'title' => 'تعطیل به مناسبت عید فطر'], + ['month' => 12, 'day' => 29, 'title' => 'روز ملی شدن صنعت نفت ایران'], + ], + + 1406 => [ + ['month' => 1, 'day' => 1, 'title' => 'آغاز نوروز'], + ['month' => 1, 'day' => 2, 'title' => 'عید نوروز'], + ['month' => 1, 'day' => 3, 'title' => 'عید نوروز'], + ['month' => 1, 'day' => 4, 'title' => 'عید نوروز'], + ['month' => 1, 'day' => 12, 'title' => 'روز جمهوری اسلامی ایران'], + ['month' => 1, 'day' => 13, 'title' => 'روز طبیعت (سیزده‌بدر)'], + ['month' => 1, 'day' => 14, 'title' => 'شهادت امام جعفر صادق (ع)'], + + ['month' => 2, 'day' => 24, 'title' => 'شهادت امام محمد باقر (ع)'], + ['month' => 2, 'day' => 27, 'title' => 'عید سعید قربان'], + + ['month' => 3, 'day' => 4, 'title' => 'عید سعید غدیر خم'], + ['month' => 3, 'day' => 14, 'title' => 'رحلت امام خمینی (ره)'], + ['month' => 3, 'day' => 15, 'title' => 'قیام ۱۵ خرداد'], + ['month' => 3, 'day' => 24, 'title' => 'تاسوعای حسینی'], + ['month' => 3, 'day' => 25, 'title' => 'عاشورای حسینی'], + + ['month' => 5, 'day' => 3, 'title' => 'اربعین حسینی'], + ['month' => 5, 'day' => 11, 'title' => 'رحلت رسول اکرم (ص) و شهادت امام حسن مجتبی (ع)'], + ['month' => 5, 'day' => 12, 'title' => 'شهادت امام رضا (ع)'], + ['month' => 5, 'day' => 20, 'title' => 'شهادت امام حسن عسکری (ع)'], + ['month' => 5, 'day' => 29, 'title' => 'میلاد رسول اکرم (ص) و امام جعفر صادق (ع)'], + + ['month' => 8, 'day' => 12, 'title' => 'شهادت حضرت فاطمه زهرا (س)'], + + ['month' => 9, 'day' => 21, 'title' => 'ولادت امام علی (ع) و روز پدر'], + + ['month' => 10, 'day' => 5, 'title' => 'مبعث حضرت رسول اکرم (ص)'], + ['month' => 10, 'day' => 23, 'title' => 'ولادت حضرت قائم (عج) / نیمه شعبان'], + + ['month' => 11, 'day' => 22, 'title' => 'پیروزی انقلاب اسلامی ایران'], + ['month' => 11, 'day' => 28, 'title' => 'شهادت امام علی (ع)'], + + ['month' => 12, 'day' => 8, 'title' => 'عید سعید فطر'], + ['month' => 12, 'day' => 9, 'title' => 'تعطیل به مناسبت عید فطر'], + ['month' => 12, 'day' => 29, 'title' => 'روز ملی شدن صنعت نفت ایران'], + ], +]; diff --git a/database/migrations/2026_09_23_000001_create_holidays_table.php b/database/migrations/2026_09_23_000001_create_holidays_table.php new file mode 100644 index 00000000..f2cba725 --- /dev/null +++ b/database/migrations/2026_09_23_000001_create_holidays_table.php @@ -0,0 +1,23 @@ +id(); + $table->date('date')->unique(); + $table->string('title')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('holidays'); + } +}; diff --git a/database/migrations/2026_09_23_000002_seed_iranian_official_holidays.php b/database/migrations/2026_09_23_000002_seed_iranian_official_holidays.php new file mode 100644 index 00000000..0205e6c5 --- /dev/null +++ b/database/migrations/2026_09_23_000002_seed_iranian_official_holidays.php @@ -0,0 +1,22 @@ +subMonths(1)->format('%B'); +$previous = Jalalian::now()->subMonths(1); +$previousMonthKey = sprintf('%04d-%02d', $previous->getYear(), $previous->getMonth()); +$previousMonthName = $previous->format('%B'); +$currentJalaliYear = Jalalian::now()->getYear(); $employeeTotals = []; @@ -267,7 +270,7 @@ foreach ($data as $employee) {

{{ $employee->name }}

کارکرد ماه قبل ({{ $previousMonthName }}): - {{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }} + {{ $employeeTotals[$employee->id][$previousMonthKey] ?? '00:00' }}
@@ -282,10 +285,23 @@ foreach ($data as $employee) { @else
@foreach(array_keys($recordsGrouped) as $month) + @php + $monthLabel = $month; + if (preg_match('/^(\d{4})-(\d{2})$/', (string) $month, $mm)) { + try { + $jj = new Jalalian((int) $mm[1], (int) $mm[2], 1); + $monthLabel = ((int) $mm[1] === (int) $currentJalaliYear) + ? $jj->format('%B') + : $jj->format('%B') . ' ' . $mm[1]; + } catch (\Throwable $e) { + $monthLabel = $month; + } + } + @endphp @endforeach
@@ -307,7 +323,7 @@ foreach ($data as $employee) { @foreach($records as $row) - {{ \App\Support\JalaliHelper::format($row->entered_at, 'm/d') }} + {{ \App\Support\JalaliHelper::format($row->entered_at, 'Y/m/d') }} {{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }} {{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }} {{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }} diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php index 4f3c8df1..c0843b14 100755 --- a/resources/views/app.blade.php +++ b/resources/views/app.blade.php @@ -947,6 +947,39 @@ display: none; } + .ui-month-more { + position: relative; + flex-shrink: 0; + } + + .ui-month-more-menu { + position: absolute; + top: calc(100% + 0.35rem); + left: 0; + z-index: 40; + min-width: 9rem; + max-height: 16rem; + overflow-y: auto; + padding: 0.35rem; + border-radius: 0.75rem; + background: var(--surface-panel, #fff); + border: 1px solid var(--border-glass, #e5e7eb); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + gap: 0.15rem; + } + + .ui-month-more-menu.hidden { + display: none; + } + + .ui-month-more-item { + width: 100%; + text-align: right; + justify-content: flex-start; + } + .ui-month-tab, .tablinks { flex-shrink: 0; diff --git a/resources/views/attendance/index.blade.php b/resources/views/attendance/index.blade.php index 52565e53..b3f15d9e 100755 --- a/resources/views/attendance/index.blade.php +++ b/resources/views/attendance/index.blade.php @@ -155,18 +155,59 @@ if (!empty($user->tasks)) {
@if(isset($_COOKIE['user_identity'])) - {{-- قسمت پردازش داده‌ها: گروه‌بندی لیست حضور و غیاب بر اساس نام ماه شمسی --}} + {{-- قسمت پردازش داده‌ها: گروه‌بندی لیست حضور و غیاب بر اساس ماه+سال شمسی --}} @php - // بررسی وجود داده $attendances = $user->attendances ?? collect(); + $currentJalaliYear = \Morilog\Jalali\Jalalian::now()->getYear(); - // گروه‌بندی داده‌ها بر اساس نام ماه شمسی $groupedData = $attendances ->filter(fn ($item) => !empty($item->entered_at)) ->groupBy(function ($item) { - $month = \App\Support\JalaliHelper::format($item->entered_at, '%B', ''); - return $month !== '' ? $month : 'نامعتبر'; - }); + $j = \App\Support\JalaliHelper::fromDateTime($item->entered_at); + if (!$j) { + return 'invalid'; + } + + return sprintf('%04d-%02d', $j->getYear(), $j->getMonth()); + }) + ->reject(fn ($_, $key) => $key === 'invalid') + ->sortKeysDesc(); + + $monthLabels = []; + foreach ($groupedData->keys() as $monthKey) { + [$y, $m] = array_map('intval', explode('-', $monthKey)); + try { + $j = new \Morilog\Jalali\Jalalian($y, $m, 1); + $name = $j->format('%B'); + $monthLabels[$monthKey] = ((int) $y === (int) $currentJalaliYear) + ? $name + : $name . ' ' . $y; + } catch (\Throwable $e) { + $monthLabels[$monthKey] = $monthKey; + } + } + + $recentMonthKeys = $groupedData->keys()->take(3); + $olderMonthKeys = $groupedData->keys()->slice(3); + + $attendanceDates = $attendances + ->map(fn ($a) => $a->entered_at ? \Carbon\Carbon::parse($a->entered_at)->toDateString() : null) + ->filter() + ->unique() + ->values(); + + $holidayDates = collect(); + try { + $holidayDates = $attendanceDates->isEmpty() + ? collect() + : \App\Models\Holiday::query() + ->whereIn('date', $attendanceDates) + ->pluck('date') + ->map(fn ($d) => \Carbon\Carbon::parse($d)->toDateString()) + ->flip(); + } catch (\Throwable $e) { + $holidayDates = collect(); + } @endphp
@@ -175,18 +216,36 @@ if (!empty($user->tasks)) {

اطلاعاتی موجود نمیباشد

@else - {{-- دکمه‌های تب (نام ماه‌ها) --}} + {{-- سه ماه اخیر + منوی بقیه --}}
- @foreach($groupedData->keys() as $month) + @foreach($recentMonthKeys as $monthKey) @endforeach + + @if($olderMonthKeys->isNotEmpty()) +
+ + +
+ @endif
- @foreach($groupedData as $month => $records) -
+ @foreach($groupedData as $monthKey => $records) +
@@ -205,11 +264,11 @@ if (!empty($user->tasks)) { @php $monthLateMinutes = 0; $monthDailyFine = 0; - $monthKey = null; + $monthKeyCarbon = null; $firstEntered = $records->first()?->entered_at; if ($firstEntered && \App\Support\JalaliHelper::fromDateTime($firstEntered)) { - $monthKey = \Carbon\Carbon::parse($firstEntered); - [$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKey); + $monthKeyCarbon = \Carbon\Carbon::parse($firstEntered); + [$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKeyCarbon); $monthWeeklyFine = (int) \App\Models\LatePeriodFine::query() ->where('user_identity', $user->user_name) ->where('period_type', 'weekly') @@ -223,6 +282,11 @@ if (!empty($user->tasks)) { @php $monthLateMinutes += (int) ($item->late_minutes ?? 0); $monthDailyFine += (int) ($item->daily_fine_amount ?? 0); + $enteredCarbon = $item->entered_at ? \Carbon\Carbon::parse($item->entered_at) : null; + $isNonWorkDay = $enteredCarbon && ( + $enteredCarbon->isFriday() + || isset($holidayDates[$enteredCarbon->toDateString()]) + ); @endphp tasks)) { {{-- تاریخ --}} @@ -260,7 +327,9 @@ if (!empty($user->tasks)) { {{-- تأخیر --}}
- {{ \App\Support\JalaliHelper::format($item->entered_at, 'm/d') }} + {{ \App\Support\JalaliHelper::format($item->entered_at, 'Y/m/d') }} {{ \App\Support\JalaliHelper::format($item->entered_at, '%A', '') }} + @if($isNonWorkDay) + · اضافه‌کاری + @endif - @if(($item->late_minutes ?? 0) > 0) + @if($isNonWorkDay) + اضافه‌کاری + @elseif(($item->late_minutes ?? 0) > 0) {{ $item->late_minutes }}د @if(($item->daily_fine_amount ?? 0) > 0) {{ number_format($item->daily_fine_amount) }}ت @@ -707,30 +776,52 @@ if (!empty($user->tasks)) { function openCity(evt, Month) { var i, tabcontent, tablinks; - // مخفی کردن تمام محتواها tabcontent = document.getElementsByClassName("tabcontent"); for (i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = "none"; } - // غیرفعال کردن تمام تب‌ها tablinks = document.getElementsByClassName("tablinks"); for (i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(" active bg-gray-300 font-bold", ""); - // اضافه کردن استایل دیفالت اگر کلاس اکتیو حذف شد if (!tablinks[i].className.includes("active")) { tablinks[i].style.backgroundColor = ""; } } - // نمایش تب انتخاب شده - document.getElementById(Month).style.display = "block"; - evt.currentTarget.className += " active bg-gray-300 font-bold"; + const panel = document.getElementById(Month); + if (panel) { + panel.style.display = "block"; + } + if (evt && evt.currentTarget) { + evt.currentTarget.className += " active bg-gray-300 font-bold"; + } } + function toggleMonthMore(evt) { + if (evt) { + evt.stopPropagation(); + } + const menu = document.getElementById('month-more-menu'); + if (menu) { + menu.classList.toggle('hidden'); + } + } + + function closeMonthMore() { + const menu = document.getElementById('month-more-menu'); + if (menu) { + menu.classList.add('hidden'); + } + } + + document.addEventListener('click', function () { + closeMonthMore(); + }); + // باز کردن اولین تب به صورت خودکار هنگام لود صفحه document.addEventListener("DOMContentLoaded", function () { - let firstTab = document.querySelector(".tablinks"); + let firstTab = document.querySelector(".ui-month-tabs > .tablinks"); if (firstTab) { firstTab.click(); } diff --git a/resources/views/attendance/show.blade.php b/resources/views/attendance/show.blade.php index 776a1c37..70a17673 100755 --- a/resources/views/attendance/show.blade.php +++ b/resources/views/attendance/show.blade.php @@ -4,8 +4,10 @@ use Morilog\Jalali\Jalalian; use Carbon\Carbon; $monthlyTotals = []; +$currentJalaliYear = Jalalian::now()->getYear(); +$monthLabels = []; -foreach ($data as $month => $attendances) { +foreach ($data as $monthKey => $attendances) { $tsec = 0; foreach ($attendances as $attendance) { @@ -18,9 +20,27 @@ foreach ($data as $month => $attendances) { $hours = floor($tsec / 3600); $minutes = floor(($tsec % 3600) / 60); - $monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes); + $monthlyTotals[$monthKey] = sprintf('%02d:%02d', $hours, $minutes); + + if (preg_match('/^(\d{4})-(\d{2})$/', (string) $monthKey, $m)) { + try { + $j = new Jalalian((int) $m[1], (int) $m[2], 1); + $name = $j->format('%B'); + $monthLabels[$monthKey] = ((int) $m[1] === (int) $currentJalaliYear) + ? $name + : $name . ' ' . $m[1]; + } catch (\Throwable $e) { + $monthLabels[$monthKey] = $monthKey; + } + } else { + $monthLabels[$monthKey] = $monthKey; + } } +$monthKeys = array_keys($data); +$recentMonthKeys = array_slice($monthKeys, 0, 3); +$olderMonthKeys = array_slice($monthKeys, 3); + ?> @extends('app') @@ -32,13 +52,27 @@ foreach ($data as $month => $attendances) { @else
- @foreach(array_keys($data) as $month) - + @foreach($recentMonthKeys as $monthKey) + @endforeach + @if(!empty($olderMonthKeys)) +
+ + +
+ @endif
- @foreach($data as $month => $records) -
+ @foreach($data as $monthKey => $records) +
@@ -52,7 +86,7 @@ foreach ($data as $month => $attendances) { @foreach($records as $row) - + @@ -61,7 +95,7 @@ foreach ($data as $month => $attendances) { @endforeach @@ -78,13 +112,24 @@ foreach ($data as $month => $attendances) { const panel = document.getElementById(Month); if (panel) panel.style.display = 'block'; - - if (evt.currentTarget) evt.currentTarget.classList.add('active'); + if (evt && evt.currentTarget) evt.currentTarget.classList.add('active'); } - document.addEventListener('DOMContentLoaded', function () { - const firstTab = document.querySelector('.tablinks'); - if (firstTab) firstTab.click(); + function toggleMonthMore(evt) { + if (evt) evt.stopPropagation(); + const menu = document.getElementById('month-more-menu'); + if (menu) menu.classList.toggle('hidden'); + } + + function closeMonthMore() { + const menu = document.getElementById('month-more-menu'); + if (menu) menu.classList.add('hidden'); + } + + document.addEventListener('click', closeMonthMore); + document.addEventListener('DOMContentLoaded', () => { + const first = document.querySelector('.ui-month-tabs > .tablinks'); + if (first) first.click(); }); @endsection diff --git a/routes/console.php b/routes/console.php index c554377d..f444c314 100755 --- a/routes/console.php +++ b/routes/console.php @@ -10,6 +10,9 @@ Artisan::command('inspire', function () { Schedule::command('birthdays:notify')->dailyAt('09:00'); +// یادآوری ثبت تعطیلات رسمی سال‌های بعد (از پایان پوشش فعلی) +Schedule::command('holidays:remind')->dailyAt('09:30'); + // گزارش تأخیر روز قبل — هر روز ساعت ۲۲:۳۰ Schedule::command('late:daily-report')->dailyAt('22:30');
{{ \App\Support\JalaliHelper::format($row->entered_at, 'm/d') }}{{ \App\Support\JalaliHelper::format($row->entered_at, 'Y/m/d') }} {{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }} {{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }} {{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}
- مجموع کارکرد این ماه: {{ $monthlyTotals[$month] }} + مجموع کارکرد این ماه: {{ $monthlyTotals[$monthKey] }}