Compare commits
11
Commits
6caee30d9b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce2ce49ffb | ||
|
|
694f41968a | ||
|
|
9e6407d648 | ||
|
|
0fd5867701 | ||
|
|
5e306564b2 | ||
|
|
280d2748f9 | ||
|
|
c54038d986 | ||
|
|
3c0a797485 | ||
|
|
2c811c0d8c | ||
|
|
49c9a45e82 | ||
|
|
54e995dadd |
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class RemindHolidayCalendarUpdate extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'holidays:remind';
|
||||||
|
|
||||||
|
protected $description = 'یادآوری ثبت تعطیلات رسمی سالهای بعد از اتمام پوشش تقویم پروژه';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$coveredUntil = SeedIranianHolidays::coveredUntilYear();
|
||||||
|
if ($coveredUntil < 1300) {
|
||||||
|
$this->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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\Holiday;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class SeedIranianHolidays extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'holidays:seed {--year= : فقط یک سال شمسی مشخص}';
|
||||||
|
|
||||||
|
protected $description = 'ثبت/بهروزرسانی تعطیلات رسمی ایران از فایل تقویم پروژه';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$path = database_path('data/iranian_official_holidays.php');
|
||||||
|
if (!is_file($path)) {
|
||||||
|
$this->error('فایل تعطیلات یافت نشد: ' . $path);
|
||||||
|
|
||||||
|
return self::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<int, list<array{month:int, day:int, title:string}>> $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\LeaveRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -206,6 +207,17 @@ class AdminController extends Controller
|
|||||||
foreach ($pendingForUser->values() as $index => $attendance) {
|
foreach ($pendingForUser->values() as $index => $attendance) {
|
||||||
$raw = $rawValues[$index] ?? null;
|
$raw = $rawValues[$index] ?? null;
|
||||||
if (!$raw) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,12 +230,15 @@ class AdminController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($pendingForUser as $attendance) {
|
// فقط وقتی Redis در دسترس نیست از fallback استفاده کن
|
||||||
if (isset($loadedIds[(string) $attendance->id])) {
|
if (!$redisAvailable) {
|
||||||
continue;
|
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');
|
$pendingById = $pendingForUser->keyBy('id');
|
||||||
@@ -238,6 +253,46 @@ class AdminController extends Controller
|
|||||||
return $records;
|
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
|
private function enrichUsersForDashboard($users): void
|
||||||
{
|
{
|
||||||
$userNames = $users->pluck('user_name');
|
$userNames = $users->pluck('user_name');
|
||||||
@@ -346,12 +401,17 @@ class AdminController extends Controller
|
|||||||
if ($u->is_admin == '1' || $u->is_admin == '2') {
|
if ($u->is_admin == '1' || $u->is_admin == '2') {
|
||||||
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
|
setcookie('ad', '1', time() + (60 * 60 * 24 * 365 * 5), "/");
|
||||||
$users = User::whereNotNull('user_name')
|
$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();
|
->get();
|
||||||
|
|
||||||
$this->enrichUsersForDashboard($users);
|
$this->enrichUsersForDashboard($users);
|
||||||
$users = $users->sortByDesc('working');
|
$users = $users->sortByDesc('working');
|
||||||
$records = $this->loadPendingRequests($users);
|
$records = $this->loadPendingRequests($users);
|
||||||
|
$leaveRequests = LeaveRequest::query()
|
||||||
|
->where('status', 'pending')
|
||||||
|
->with('user:id,name,user_name')
|
||||||
|
->orderByDesc('created_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
return view("admin..admin")->with(['message', [
|
return view("admin..admin")->with(['message', [
|
||||||
'color' => '#ff8800ff',
|
'color' => '#ff8800ff',
|
||||||
@@ -359,6 +419,7 @@ class AdminController extends Controller
|
|||||||
],
|
],
|
||||||
"users" => $users,
|
"users" => $users,
|
||||||
'reqs' => $records,
|
'reqs' => $records,
|
||||||
|
'leaveRequests' => $leaveRequests,
|
||||||
"userType" => $provider
|
"userType" => $provider
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -572,7 +633,7 @@ class AdminController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$record->duration = $duration ?? $record->duration;
|
$record->duration = $duration ?? $record->duration;
|
||||||
$record->requested = '';
|
$record->requested = null;
|
||||||
$record->save();
|
$record->save();
|
||||||
app(LatePenaltyService::class)->syncAttendance($record);
|
app(LatePenaltyService::class)->syncAttendance($record);
|
||||||
|
|
||||||
@@ -610,8 +671,7 @@ class AdminController extends Controller
|
|||||||
: null;
|
: null;
|
||||||
$attendance->save();
|
$attendance->save();
|
||||||
app(LatePenaltyService::class)->syncAttendance($attendance);
|
app(LatePenaltyService::class)->syncAttendance($attendance);
|
||||||
$create_requests_key = "att-tempreq:creates:{$data->userid}";
|
$this->removeCreateRequest($data->userid, $data);
|
||||||
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
|
|
||||||
$this->decrementUserRequestCount($data->userid);
|
$this->decrementUserRequestCount($data->userid);
|
||||||
|
|
||||||
tel_notif($data->userid, "تایید");
|
tel_notif($data->userid, "تایید");
|
||||||
@@ -626,18 +686,11 @@ class AdminController extends Controller
|
|||||||
if ($request->update == 'reject') {
|
if ($request->update == 'reject') {
|
||||||
if ($data->function == 'delete' || $data->function == 'edit') {
|
if ($data->function == 'delete' || $data->function == 'edit') {
|
||||||
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
|
Redis::connection()->del("att-tempreq:{$data->userid}:{$data->id}");
|
||||||
|
$this->clearAttendanceRequestFlag($data->id ?? null);
|
||||||
} elseif ($data->function == 'new') {
|
} elseif ($data->function == 'new') {
|
||||||
$create_requests_key = "att-tempreq:creates:{$data->userid}";
|
$this->removeCreateRequest($data->userid, $data);
|
||||||
Redis::connection()->lrem($create_requests_key, 1, json_encode($data));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// dd($data->id);
|
|
||||||
if ($data->id !== "new") {
|
|
||||||
$att = Attendance::find($data->id);
|
|
||||||
// dd($att);
|
|
||||||
$att->requested = null;
|
|
||||||
$att->save();
|
|
||||||
}
|
|
||||||
tel_notif($data->userid, "رد");
|
tel_notif($data->userid, "رد");
|
||||||
$this->decrementUserRequestCount($data->userid);
|
$this->decrementUserRequestCount($data->userid);
|
||||||
return redirect()->back()->with('message', [
|
return redirect()->back()->with('message', [
|
||||||
|
|||||||
@@ -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)
|
public function profile(Request $request)
|
||||||
{
|
{
|
||||||
$username = $request->query('username') ?: $_COOKIE['user_identity'];
|
$username = $request->query('username') ?: $_COOKIE['user_identity'];
|
||||||
$user = User::where('user_name', $username)->first();
|
$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)
|
public function updateProfile(Request $request)
|
||||||
{
|
{
|
||||||
// dd($request->all());
|
$viewerIsAdmin = $this->viewerIsAdmin();
|
||||||
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
$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) {
|
if ($user->password != $request->password) {
|
||||||
$request->validate([
|
$request->validate([
|
||||||
@@ -645,14 +665,53 @@ class AttendanceController extends Controller
|
|||||||
$user->is_admin = $request->role == 1 ? 1 : null;
|
$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();
|
$user->save();
|
||||||
|
|
||||||
|
if ($workStartChanged) {
|
||||||
|
$this->resyncCurrentMonthLate($user->user_name);
|
||||||
|
}
|
||||||
|
|
||||||
return redirect()->back()->with('message', [
|
return redirect()->back()->with('message', [
|
||||||
'color' => '#00c300ff',
|
'color' => '#00c300ff',
|
||||||
'text' => 'اطلاعات با موفقیت ویرایش شد.'
|
'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)
|
private function dailyReport($userId, $userName)
|
||||||
{
|
{
|
||||||
$tasks = Tasks::with(['duration'])
|
$tasks = Tasks::with(['duration'])
|
||||||
|
|||||||
@@ -0,0 +1,257 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Attendance;
|
||||||
|
use App\Models\LeaveRequest;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\LatePenaltyService;
|
||||||
|
use App\Support\JalaliHelper;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Morilog\Jalali\Jalalian;
|
||||||
|
|
||||||
|
class LeaveController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request)
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['user_identity'])) {
|
||||||
|
return redirect('/')->with('message', [
|
||||||
|
'color' => '#ff8800ff',
|
||||||
|
'text' => 'ابتدا باید وارد شوید!',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
||||||
|
if (!$user) {
|
||||||
|
return redirect('/')->with('message', [
|
||||||
|
'color' => '#ff8800ff',
|
||||||
|
'text' => 'کاربر یافت نشد!',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $request->input('type');
|
||||||
|
if (!in_array($type, ['daily', 'hourly'], true)) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'نوع مرخصی نامعتبر است.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'daily') {
|
||||||
|
$dateFrom = $this->parseJalaliDate($request->input('date_from'));
|
||||||
|
$dateTo = $this->parseJalaliDate($request->input('date_to'));
|
||||||
|
|
||||||
|
if (!$dateFrom || !$dateTo) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'بازه تاریخ مرخصی روزانه الزامی است.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($dateTo < $dateFrom) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'تاریخ پایان نمیتواند قبل از شروع باشد.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$timeFrom = null;
|
||||||
|
$timeTo = null;
|
||||||
|
} else {
|
||||||
|
$dateFrom = $this->parseJalaliDate($request->input('date'));
|
||||||
|
$dateTo = $dateFrom;
|
||||||
|
$timeFrom = $this->normalizeTime($request->input('time_from'));
|
||||||
|
$timeTo = $this->normalizeTime($request->input('time_to'));
|
||||||
|
|
||||||
|
if (!$dateFrom || !$timeFrom || !$timeTo) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'تاریخ و ساعت شروع/پایان مرخصی ساعتی الزامی است.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($timeTo <= $timeFrom) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'ساعت پایان باید بعد از ساعت شروع باشد.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$leave = LeaveRequest::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'user_identity' => $user->user_name,
|
||||||
|
'type' => $type,
|
||||||
|
'date_from' => $dateFrom,
|
||||||
|
'date_to' => $dateTo,
|
||||||
|
'time_from' => $timeFrom,
|
||||||
|
'time_to' => $timeTo,
|
||||||
|
'reason' => $request->input('reason'),
|
||||||
|
'status' => 'pending',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->req_counts = (int) $user->req_counts + 1;
|
||||||
|
$user->save();
|
||||||
|
|
||||||
|
$this->notifyAdminsNewLeave($leave, $user);
|
||||||
|
|
||||||
|
return redirect('/')->with('message', [
|
||||||
|
'color' => '#00c300ff',
|
||||||
|
'text' => 'درخواست مرخصی ثبت شد و برای تأیید ادمین ارسال گردید.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function decide(Request $request)
|
||||||
|
{
|
||||||
|
if (!isset($_COOKIE['user_identity'])) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff8800ff',
|
||||||
|
'text' => 'ابتدا باید وارد شوید!',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$admin = User::where('user_name', $_COOKIE['user_identity'])->first();
|
||||||
|
if (!$admin || !in_array((string) $admin->is_admin, ['1', '2'], true)) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff8800ff',
|
||||||
|
'text' => 'دسترسی ندارید!',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$leave = LeaveRequest::find($request->input('leave_id'));
|
||||||
|
if (!$leave || $leave->status !== 'pending') {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff8800ff',
|
||||||
|
'text' => 'درخواست مرخصی یافت نشد.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $request->input('update');
|
||||||
|
if (!in_array($action, ['approve', 'reject'], true)) {
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#ff4400ff',
|
||||||
|
'text' => 'عملیات نامعتبر است.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$leave->status = $action === 'approve' ? 'approved' : 'rejected';
|
||||||
|
$leave->reviewed_by = $admin->id;
|
||||||
|
$leave->reviewed_at = now();
|
||||||
|
$leave->save();
|
||||||
|
|
||||||
|
$owner = User::where('user_name', $leave->user_identity)->first();
|
||||||
|
if ($owner && (int) $owner->req_counts > 0) {
|
||||||
|
$owner->req_counts -= 1;
|
||||||
|
$owner->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($leave->status === 'approved') {
|
||||||
|
$this->recalculateLateForLeavePeriod($leave);
|
||||||
|
}
|
||||||
|
|
||||||
|
$statusFa = $leave->status === 'approved' ? 'تایید' : 'رد';
|
||||||
|
if ($owner) {
|
||||||
|
$this->notifyUserLeaveDecision($owner, $statusFa);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('message', [
|
||||||
|
'color' => '#6aff00ff',
|
||||||
|
'text' => "درخواست مرخصی با موفقیت {$statusFa} شد.",
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyUserLeaveDecision(User $user, string $statusFa): void
|
||||||
|
{
|
||||||
|
if (!$user->bale) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Http::withoutVerifying()->post(
|
||||||
|
'https://tapi.bale.ai/1482838710:sYbLzby0tuLRzHvP4d3LsbcNwg2sjSDLTAU/sendMessage',
|
||||||
|
[
|
||||||
|
'chat_id' => $user->bale,
|
||||||
|
'text' => "درخواست مرخصی شما {$statusFa} شد.",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function recalculateLateForLeavePeriod(LeaveRequest $leave): void
|
||||||
|
{
|
||||||
|
$service = app(LatePenaltyService::class);
|
||||||
|
$from = Carbon::parse($leave->date_from)->startOfDay();
|
||||||
|
$to = Carbon::parse($leave->date_to)->endOfDay();
|
||||||
|
|
||||||
|
$attendances = Attendance::query()
|
||||||
|
->where('user_identity', $leave->user_identity)
|
||||||
|
->whereBetween('entered_at', [$from, $to])
|
||||||
|
->orderBy('entered_at')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($attendances as $attendance) {
|
||||||
|
$service->syncAttendance($attendance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function notifyAdminsNewLeave(LeaveRequest $leave, User $user): void
|
||||||
|
{
|
||||||
|
$typeFa = $leave->type === 'daily' ? 'روزانه' : 'ساعتی';
|
||||||
|
$from = JalaliHelper::format($leave->date_from, 'Y/m/d');
|
||||||
|
$to = JalaliHelper::format($leave->date_to, 'Y/m/d');
|
||||||
|
|
||||||
|
$message = "درخواست مرخصی {$typeFa}\n";
|
||||||
|
$message .= "کارمند: {$user->name}\n";
|
||||||
|
if ($leave->type === 'daily') {
|
||||||
|
$message .= "از {$from} تا {$to}\n";
|
||||||
|
} else {
|
||||||
|
$timeFrom = substr((string) $leave->time_from, 0, 5);
|
||||||
|
$timeTo = substr((string) $leave->time_to, 0, 5);
|
||||||
|
$message .= "تاریخ: {$from}\nساعت: {$timeFrom} تا {$timeTo}\n";
|
||||||
|
}
|
||||||
|
if ($leave->reason) {
|
||||||
|
$message .= "دلیل: {$leave->reason}";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->send_admins($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function parseJalaliDate(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (!$value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = \Morilog\Jalali\CalendarUtils::convertNumbers(trim($value), true);
|
||||||
|
$value = str_replace('/', '-', $value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
|
||||||
|
// اگر سال شمسی باشد (مثلاً 1404) با Jalalian تبدیل میشود
|
||||||
|
$year = (int) substr($value, 0, 4);
|
||||||
|
if ($year >= 1300 && $year <= 1500) {
|
||||||
|
return Jalalian::fromFormat('Y-m-d', $value)->toCarbon()->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Carbon::parse($value)->format('Y-m-d');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function normalizeTime(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (!$value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$value = trim(\Morilog\Jalali\CalendarUtils::convertNumbers($value, true));
|
||||||
|
if (preg_match('/^(\d{1,2}):(\d{2})/', $value, $m)) {
|
||||||
|
return sprintf('%02d:%02d:00', (int) $m[1], (int) $m[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ use GuzzleHttp\Client;
|
|||||||
use App\Models\Tasks;
|
use App\Models\Tasks;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Morilog\Jalali\Jalalian;
|
|
||||||
use Carbon\CarbonInterval;
|
use Carbon\CarbonInterval;
|
||||||
|
|
||||||
|
|
||||||
@@ -154,17 +153,17 @@ class TaskController extends Controller
|
|||||||
? "وضعیت: " . ($stateFa[$s1] ?? ($before['state'] ?? '-')) . " ← " . ($stateFa[$s2] ?? ($after['state'] ?? '-'))
|
? "وضعیت: " . ($stateFa[$s1] ?? ($before['state'] ?? '-')) . " ← " . ($stateFa[$s2] ?? ($after['state'] ?? '-'))
|
||||||
: "وضعیت: " . ($stateFa[$s2] ?? ($after['state'] ?? '-'));
|
: "وضعیت: " . ($stateFa[$s2] ?? ($after['state'] ?? '-'));
|
||||||
|
|
||||||
$d1 = $before['start_date'] ?? null;
|
$d1 = \App\Support\JalaliHelper::format($before['start_date'] ?? null, 'Y/m/d', '-');
|
||||||
$d2 = $after['start_date'] ?? null;
|
$d2 = \App\Support\JalaliHelper::format($after['start_date'] ?? null, 'Y/m/d', '-');
|
||||||
$lines[] = ($d1 !== $d2)
|
$lines[] = ($d1 !== $d2)
|
||||||
? "شروع: " . ($d1 ?: '-') . " ← " . ($d2 ?: '-')
|
? "شروع: {$d1} ← {$d2}"
|
||||||
: "شروع: " . ($d2 ?: '-');
|
: "شروع: {$d2}";
|
||||||
|
|
||||||
$t1 = $before['target_date'] ?? null;
|
$t1 = \App\Support\JalaliHelper::format($before['target_date'] ?? null, 'Y/m/d', '-');
|
||||||
$t2 = $after['target_date'] ?? null;
|
$t2 = \App\Support\JalaliHelper::format($after['target_date'] ?? null, 'Y/m/d', '-');
|
||||||
$lines[] = ($t1 !== $t2)
|
$lines[] = ($t1 !== $t2)
|
||||||
? "ددلاین: " . ($t1 ?: '-') . " ← " . ($t2 ?: '-')
|
? "ددلاین: {$t1} ← {$t2}"
|
||||||
: "ددلاین: " . ($t2 ?: '-');
|
: "ددلاین: {$t2}";
|
||||||
|
|
||||||
$desc1 = trim((string)($before['description'] ?? ''));
|
$desc1 = trim((string)($before['description'] ?? ''));
|
||||||
$desc2 = trim((string)($after['description'] ?? ''));
|
$desc2 = trim((string)($after['description'] ?? ''));
|
||||||
@@ -265,12 +264,18 @@ class TaskController extends Controller
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $m)) {
|
|
||||||
return $m[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return Carbon::parse($value)->format('Y-m-d');
|
if (preg_match('/^(\d{4}-\d{2}-\d{2})/', $value, $m)) {
|
||||||
|
$carbon = Carbon::createFromFormat('Y-m-d', $m[1])->startOfDay();
|
||||||
|
} else {
|
||||||
|
$carbon = Carbon::parse($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($carbon->year < 1621 || $carbon->year > 2500) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $carbon->format('Y-m-d');
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -688,13 +693,8 @@ class TaskController extends Controller
|
|||||||
}
|
}
|
||||||
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
$user = User::where('user_name', $_COOKIE['user_identity'])->first();
|
||||||
|
|
||||||
$startDate = $this->normalizeTaskDate($request->input('start_date'));
|
$startDate = $this->normalizeTaskDate($request->input('start_date'))
|
||||||
if (!$startDate) {
|
?: now()->format('Y-m-d');
|
||||||
return back()->withInput()->with('message', [
|
|
||||||
'color' => 'red',
|
|
||||||
'text' => 'تاریخ شروع الزامی است. لطفاً از تقویم انتخاب کنید.',
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'title' => $request->input('title'),
|
'title' => $request->input('title'),
|
||||||
@@ -711,12 +711,14 @@ class TaskController extends Controller
|
|||||||
$task = Tasks::create($data);
|
$task = Tasks::create($data);
|
||||||
|
|
||||||
$message = "تسک جدیدی با عنوان {$data['title']} برای شما ایجاد شد.";
|
$message = "تسک جدیدی با عنوان {$data['title']} برای شما ایجاد شد.";
|
||||||
$deadline = !empty($data['target_date']) ? Jalalian::fromCarbon(Carbon::parse($data['target_date']))->format('Y-m-d') : null;
|
$deadline = \App\Support\JalaliHelper::format($data['target_date'] ?? null, 'Y/m/d', '');
|
||||||
|
$deadline = $deadline !== '' ? $deadline : null;
|
||||||
|
$startDateJalali = \App\Support\JalaliHelper::format($data['start_date'] ?? null, 'Y/m/d', '');
|
||||||
$app = App::find($data['app']);
|
$app = App::find($data['app']);
|
||||||
$message_admin = "ایجاد تسک: «{$data['title']}»";
|
$message_admin = "ایجاد تسک: «{$data['title']}»";
|
||||||
$message_admin .= !empty($app) ? "\nاپ {$app->name_fa}" : null;
|
$message_admin .= !empty($app) ? "\nاپ {$app->name_fa}" : null;
|
||||||
$message_admin .= !empty($deadline) ? "\nددلاین {$deadline}" : null;
|
$message_admin .= !empty($deadline) ? "\nددلاین {$deadline}" : null;
|
||||||
$message_admin .= !empty($data['start_date']) ? "\nتاریخ شروع {$data['start_date']}" : null;
|
$message_admin .= $startDateJalali !== '' ? "\nتاریخ شروع {$startDateJalali}" : null;
|
||||||
$message_admin .= !empty($request->input('state_fa')) ? "\nوضعیت: {$request->input('state_fa')}" : null;
|
$message_admin .= !empty($request->input('state_fa')) ? "\nوضعیت: {$request->input('state_fa')}" : null;
|
||||||
$message_admin .= !empty($data['description']) ? "\nبا توضیحات:\n" . $data['description'] : null;
|
$message_admin .= !empty($data['description']) ? "\nبا توضیحات:\n" . $data['description'] : null;
|
||||||
$message_admin .= !empty($data['assignees']) ? "\nتوسط " . $user->name . " ساخته شد و به" : null;
|
$message_admin .= !empty($data['assignees']) ? "\nتوسط " . $user->name . " ساخته شد و به" : null;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Holiday extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'date',
|
||||||
|
'title',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date' => 'date',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class LeaveRequest extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'user_identity',
|
||||||
|
'type',
|
||||||
|
'date_from',
|
||||||
|
'date_to',
|
||||||
|
'time_from',
|
||||||
|
'time_to',
|
||||||
|
'reason',
|
||||||
|
'status',
|
||||||
|
'reviewed_by',
|
||||||
|
'reviewed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'date_from' => 'date',
|
||||||
|
'date_to' => 'date',
|
||||||
|
'reviewed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'user_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviewer()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'reviewed_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDaily(): bool
|
||||||
|
{
|
||||||
|
return $this->type === 'daily';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isHourly(): bool
|
||||||
|
{
|
||||||
|
return $this->type === 'hourly';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isApproved(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'approved';
|
||||||
|
}
|
||||||
|
}
|
||||||
+6
-4
@@ -30,6 +30,7 @@ class User extends Authenticatable
|
|||||||
'name',
|
'name',
|
||||||
'number',
|
'number',
|
||||||
'birthday',
|
'birthday',
|
||||||
|
'work_start_time',
|
||||||
'password',
|
'password',
|
||||||
'user_name',
|
'user_name',
|
||||||
'is_admin',
|
'is_admin',
|
||||||
@@ -111,16 +112,17 @@ class User extends Authenticatable
|
|||||||
if (!$date) {
|
if (!$date) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$month = $date->format('%B');
|
$key = sprintf('%04d-%02d', $date->getYear(), $date->getMonth());
|
||||||
|
|
||||||
if (!isset($result[$month])) {
|
if (!isset($result[$key])) {
|
||||||
$result[$month] = [];
|
$result[$key] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$result[$month][] = $att;
|
$result[$key][] = $att;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
krsort($result);
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Attendance;
|
use App\Models\Attendance;
|
||||||
|
use App\Models\Holiday;
|
||||||
use App\Models\LatePeriodFine;
|
use App\Models\LatePeriodFine;
|
||||||
|
use App\Models\LeaveRequest;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
@@ -17,7 +19,51 @@ class LatePenaltyService
|
|||||||
public const WEEKLY_ALLOWANCE_MINUTES = 15;
|
public const WEEKLY_ALLOWANCE_MINUTES = 15;
|
||||||
public const FINE_PER_MINUTE = 2000;
|
public const FINE_PER_MINUTE = 2000;
|
||||||
|
|
||||||
public function calculateFromEnteredAt(?string $enteredAt): array
|
/** @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
|
||||||
|
{
|
||||||
|
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) {
|
if (!$enteredAt) {
|
||||||
return [
|
return [
|
||||||
@@ -28,8 +74,35 @@ class LatePenaltyService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$entered = Carbon::parse($enteredAt);
|
// فقط تا دقیقه؛ ثانیه نادیده گرفته میشود
|
||||||
$threshold = $entered->copy()->setTimeFromTimeString(self::WORK_START);
|
$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($this->workStartFor($userIdentity))->startOfMinute();
|
||||||
|
|
||||||
|
if ($userIdentity) {
|
||||||
|
$leaveThreshold = $this->approvedLeaveWorkThreshold($userIdentity, $entered);
|
||||||
|
if ($leaveThreshold === false) {
|
||||||
|
return [
|
||||||
|
'late_minutes' => 0,
|
||||||
|
'grace_late_minutes' => 0,
|
||||||
|
'daily_fine_minutes' => 0,
|
||||||
|
'daily_fine_amount' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
if ($leaveThreshold instanceof Carbon) {
|
||||||
|
$threshold = $leaveThreshold->copy()->startOfMinute();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($entered->lte($threshold)) {
|
if ($entered->lte($threshold)) {
|
||||||
return [
|
return [
|
||||||
@@ -40,7 +113,7 @@ class LatePenaltyService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$lateMinutes = (int) ceil($threshold->diffInSeconds($entered) / 60);
|
$lateMinutes = (int) $threshold->diffInMinutes($entered);
|
||||||
// تأخیر داخل بازه ارفاق روزانه (۸:۳۰ تا ۸:۴۵) — مبنای جریمه هفتگی
|
// تأخیر داخل بازه ارفاق روزانه (۸:۳۰ تا ۸:۴۵) — مبنای جریمه هفتگی
|
||||||
$graceLateMinutes = min($lateMinutes, self::DAILY_GRACE_MINUTES);
|
$graceLateMinutes = min($lateMinutes, self::DAILY_GRACE_MINUTES);
|
||||||
$dailyFineMinutes = max(0, $lateMinutes - self::DAILY_GRACE_MINUTES);
|
$dailyFineMinutes = max(0, $lateMinutes - self::DAILY_GRACE_MINUTES);
|
||||||
@@ -53,6 +126,49 @@ class LatePenaltyService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* مرخصی تأییدشده: false = معاف کامل، Carbon = شروع مؤثر کار، null = بدون تغییر.
|
||||||
|
*/
|
||||||
|
public function approvedLeaveWorkThreshold(string $userIdentity, Carbon $entered): Carbon|false|null
|
||||||
|
{
|
||||||
|
$day = $entered->toDateString();
|
||||||
|
$workStart = $entered->copy()->setTimeFromTimeString($this->workStartFor($userIdentity))->startOfMinute();
|
||||||
|
|
||||||
|
$leaves = LeaveRequest::query()
|
||||||
|
->where('user_identity', $userIdentity)
|
||||||
|
->where('status', 'approved')
|
||||||
|
->whereDate('date_from', '<=', $day)
|
||||||
|
->whereDate('date_to', '>=', $day)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$latestMorningLeaveEnd = null;
|
||||||
|
|
||||||
|
foreach ($leaves as $leave) {
|
||||||
|
if ($leave->type === 'daily') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($leave->type !== 'hourly' || !$leave->time_from || !$leave->time_to) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$leaveStart = Carbon::parse($day . ' ' . $leave->time_from)->startOfMinute();
|
||||||
|
$leaveEnd = Carbon::parse($day . ' ' . $leave->time_to)->startOfMinute();
|
||||||
|
|
||||||
|
// مرخصی صبح که شروع کار را پوشش میدهد
|
||||||
|
if ($leaveStart->lte($workStart) && $leaveEnd->gt($workStart)) {
|
||||||
|
if ($entered->lte($leaveEnd)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($latestMorningLeaveEnd === null || $leaveEnd->gt($latestMorningLeaveEnd)) {
|
||||||
|
$latestMorningLeaveEnd = $leaveEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latestMorningLeaveEnd;
|
||||||
|
}
|
||||||
|
|
||||||
/** دقایق تأخیر داخل بازه ۸:۳۰–۸:۴۵ برای یک رکورد */
|
/** دقایق تأخیر داخل بازه ۸:۳۰–۸:۴۵ برای یک رکورد */
|
||||||
public function graceLateMinutes(Attendance $att): int
|
public function graceLateMinutes(Attendance $att): int
|
||||||
{
|
{
|
||||||
@@ -93,7 +209,7 @@ class LatePenaltyService
|
|||||||
return $attendance;
|
return $attendance;
|
||||||
}
|
}
|
||||||
|
|
||||||
$calc = $this->calculateFromEnteredAt($attendance->entered_at);
|
$calc = $this->calculateFromEnteredAt($attendance->entered_at, $attendance->user_identity);
|
||||||
$attendance->late_minutes = $calc['late_minutes'];
|
$attendance->late_minutes = $calc['late_minutes'];
|
||||||
$attendance->daily_fine_minutes = $calc['daily_fine_minutes'];
|
$attendance->daily_fine_minutes = $calc['daily_fine_minutes'];
|
||||||
$attendance->daily_fine_amount = $calc['daily_fine_amount'];
|
$attendance->daily_fine_amount = $calc['daily_fine_amount'];
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تعطیلات رسمی ایران بر اساس تقویم رسمی (مناسبتهای ملی و مذهبی).
|
||||||
|
* جمعهها جداگانه در LatePenaltyService محاسبه میشوند و اینجا نیستند.
|
||||||
|
*
|
||||||
|
* منابع تقریبی: تقویم رسمی / فهرستهای منتشرشده برای ۱۴۰۵ و ۱۴۰۶.
|
||||||
|
* تاریخهای قمری ممکن است با اعلام رؤیت هلال یک روز جابهجا شوند.
|
||||||
|
*
|
||||||
|
* @return array<int, list<array{month:int, day:int, title:string}>>
|
||||||
|
*/
|
||||||
|
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' => 'روز ملی شدن صنعت نفت ایران'],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<?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::create('leave_requests', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('user_id')->nullable();
|
||||||
|
$table->string('user_identity');
|
||||||
|
$table->string('type'); // daily | hourly
|
||||||
|
$table->date('date_from');
|
||||||
|
$table->date('date_to');
|
||||||
|
$table->time('time_from')->nullable();
|
||||||
|
$table->time('time_to')->nullable();
|
||||||
|
$table->text('reason')->nullable();
|
||||||
|
$table->string('status')->default('pending'); // pending | approved | rejected
|
||||||
|
$table->unsignedBigInteger('reviewed_by')->nullable();
|
||||||
|
$table->timestamp('reviewed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index(['user_identity', 'status']);
|
||||||
|
$table->index(['date_from', 'date_to']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('leave_requests');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?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::create('holidays', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->date('date')->unique();
|
||||||
|
$table->string('title')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('holidays');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (!Schema::hasTable('holidays')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Artisan::call('holidays:seed');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// تعطیلات seedشده را عمداً حذف نمیکنیم
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
|
||||||
|
class HolidaySeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
Artisan::call('holidays:seed');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,62 @@ $usersChunked = $users->chunk(3);
|
|||||||
@section('content')
|
@section('content')
|
||||||
<div class="container w-full flex flex-col items-center">
|
<div class="container w-full flex flex-col items-center">
|
||||||
|
|
||||||
|
@if(isset($leaveRequests) && $leaveRequests->isNotEmpty())
|
||||||
|
<div class="ui-requests-card flex flex-col items-center w-full max-w-[350px] mb-4">
|
||||||
|
<p>درخواستهای مرخصی</p>
|
||||||
|
<div class="ui-table-wrap w-full pt-2">
|
||||||
|
<table class="w-full table-auto text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="px-2 py-2">کارمند</th>
|
||||||
|
<th class="px-2 py-2">نوع</th>
|
||||||
|
<th class="px-2 py-2">بازه</th>
|
||||||
|
<th class="px-2 py-2">وضعیت</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach($leaveRequests as $leave)
|
||||||
|
@php
|
||||||
|
$ownerName = $leave->user->name ?? $leave->user_identity;
|
||||||
|
$fromJ = \App\Support\JalaliHelper::format($leave->date_from, 'Y/m/d');
|
||||||
|
$toJ = \App\Support\JalaliHelper::format($leave->date_to, 'Y/m/d');
|
||||||
|
if ($leave->type === 'daily') {
|
||||||
|
$range = $fromJ === $toJ ? $fromJ : "{$fromJ} تا {$toJ}";
|
||||||
|
} else {
|
||||||
|
$tf = substr((string) $leave->time_from, 0, 5);
|
||||||
|
$tt = substr((string) $leave->time_to, 0, 5);
|
||||||
|
$range = "{$fromJ}<br>{$tf}–{$tt}";
|
||||||
|
}
|
||||||
|
@endphp
|
||||||
|
<tr>
|
||||||
|
<td class="px-2 py-2 border">{{ $ownerName }}</td>
|
||||||
|
<td class="px-2 py-2 border">{{ $leave->type === 'daily' ? 'روزانه' : 'ساعتی' }}</td>
|
||||||
|
<td class="px-2 py-2 border">{!! $range !!}</td>
|
||||||
|
<td class="px-2 py-2 border">
|
||||||
|
@if($leave->reason)
|
||||||
|
<p class="text-xs mb-1 opacity-80">{{ $leave->reason }}</p>
|
||||||
|
@endif
|
||||||
|
<form method="post" action="{{ route('admin.leave.decide') }}" class="inline">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="leave_id" value="{{ $leave->id }}">
|
||||||
|
<input type="hidden" name="update" value="approve">
|
||||||
|
<button type="submit" class="ui-btn-approve">تایید</button>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="{{ route('admin.leave.decide') }}" class="inline">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="leave_id" value="{{ $leave->id }}">
|
||||||
|
<input type="hidden" name="update" value="reject">
|
||||||
|
<button type="submit" class="ui-btn-reject">رد</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
@if(!collect($reqs)->pluck('attendances')->flatten()->isEmpty())
|
@if(!collect($reqs)->pluck('attendances')->flatten()->isEmpty())
|
||||||
<div class="ui-requests-card flex flex-col items-center w-full max-w-[350px]">
|
<div class="ui-requests-card flex flex-col items-center w-full max-w-[350px]">
|
||||||
<p>درخواست ها</p>
|
<p>درخواست ها</p>
|
||||||
@@ -142,6 +198,9 @@ $usersChunked = $users->chunk(3);
|
|||||||
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
|
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
|
||||||
<p>جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان</p>
|
<p>جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان</p>
|
||||||
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
|
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
|
||||||
|
@if($u->work_start_time)
|
||||||
|
<p>شروع کار: {{ substr((string) $u->work_start_time, 0, 5) }}</p>
|
||||||
|
@endif
|
||||||
</a>
|
</a>
|
||||||
@if($userType == 'admin')
|
@if($userType == 'admin')
|
||||||
<button type="button"
|
<button type="button"
|
||||||
|
|||||||
@@ -251,6 +251,8 @@ SVG;
|
|||||||
|
|
||||||
const referenceLat = 32.6504751;
|
const referenceLat = 32.6504751;
|
||||||
const referenceLng = 51.6936045;
|
const referenceLng = 51.6936045;
|
||||||
|
const OFFICE_RADIUS_METERS = 100;
|
||||||
|
const MAX_ACCURACY_CREDIT_METERS = 80;
|
||||||
|
|
||||||
const submitBtn = document.getElementById("submitBtn");
|
const submitBtn = document.getElementById("submitBtn");
|
||||||
const latInput = document.getElementById("latitude");
|
const latInput = document.getElementById("latitude");
|
||||||
@@ -278,26 +280,31 @@ SVG;
|
|||||||
return earthRadius * c;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isInsideOffice(distanceMeters, accuracyMeters) {
|
||||||
|
const accuracyCredit = Math.min(Math.max(0, accuracyMeters || 0), MAX_ACCURACY_CREDIT_METERS);
|
||||||
|
return Math.max(0, distanceMeters - accuracyCredit) <= OFFICE_RADIUS_METERS;
|
||||||
|
}
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
navigator.geolocation.getCurrentPosition(
|
||||||
position => {
|
position => {
|
||||||
const lat = position.coords.latitude;
|
const lat = position.coords.latitude;
|
||||||
const lng = position.coords.longitude;
|
const lng = position.coords.longitude;
|
||||||
|
const accuracy = position.coords.accuracy || 0;
|
||||||
|
|
||||||
latInput.value = lat;
|
latInput.value = lat;
|
||||||
lngInput.value = lng;
|
lngInput.value = lng;
|
||||||
|
|
||||||
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
||||||
distanceInput.value = dist;
|
distanceInput.value = dist;
|
||||||
if (dist < 20) {
|
const inside = isInsideOffice(dist, accuracy);
|
||||||
distanceMessage.textContent = `مکان: شرکت`;
|
distanceMessage.textContent = inside
|
||||||
} else {
|
? `مکان: شرکت (~${dist}م)`
|
||||||
distanceMessage.textContent = `مکان: خارج از شرکت`;
|
: `مکان: خارج از شرکت (~${dist}م)`;
|
||||||
}
|
|
||||||
|
|
||||||
distanceMessage.classList.remove("bg-gray-400", "bg-blue-500", "bg-green-500", "bg-orange-500", "cursor-not-allowed");
|
distanceMessage.classList.remove("bg-gray-400", "bg-blue-500", "bg-green-500", "bg-orange-500", "cursor-not-allowed", "text-green-500", "text-blue-500", "text-orange-500", "text-gray-400");
|
||||||
if (dist < 10) {
|
if (inside) {
|
||||||
distanceMessage.classList.add("text-green-500");
|
distanceMessage.classList.add("text-green-500");
|
||||||
} else if (dist < 50) {
|
} else if (dist < OFFICE_RADIUS_METERS * 2) {
|
||||||
distanceMessage.classList.add("text-blue-500");
|
distanceMessage.classList.add("text-blue-500");
|
||||||
} else if (dist > 2000) {
|
} else if (dist > 2000) {
|
||||||
distanceMessage.classList.add("text-orange-500");
|
distanceMessage.classList.add("text-orange-500");
|
||||||
@@ -311,7 +318,7 @@ SVG;
|
|||||||
},
|
},
|
||||||
error => alert("⛔ خطا در دریافت موقعیت مکانی: " + error.message), {
|
error => alert("⛔ خطا در دریافت موقعیت مکانی: " + error.message), {
|
||||||
enableHighAccuracy: true,
|
enableHighAccuracy: true,
|
||||||
timeout: 10000,
|
timeout: 20000,
|
||||||
maximumAge: 0
|
maximumAge: 0
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,10 @@
|
|||||||
use Morilog\Jalali\Jalalian;
|
use Morilog\Jalali\Jalalian;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
$previousMonthName = Jalalian::now()->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 = [];
|
$employeeTotals = [];
|
||||||
|
|
||||||
@@ -142,7 +145,9 @@ foreach ($data as $employee) {
|
|||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 50;
|
||||||
padding: 0.45rem;
|
padding: 0.45rem;
|
||||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||||
background: var(--surface-tabs);
|
background: var(--surface-tabs);
|
||||||
@@ -155,6 +160,40 @@ foreach ($data as $employee) {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-month-more {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.35rem);
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
z-index: 100;
|
||||||
|
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.18);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-menu.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
.month-tab {
|
.month-tab {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
padding: 0.5rem 0.85rem;
|
padding: 0.5rem 0.85rem;
|
||||||
@@ -187,6 +226,8 @@ foreach ($data as $employee) {
|
|||||||
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attendance-table {
|
.attendance-table {
|
||||||
@@ -267,7 +308,7 @@ foreach ($data as $employee) {
|
|||||||
<h2>{{ $employee->name }}</h2>
|
<h2>{{ $employee->name }}</h2>
|
||||||
<div class="employee-stat">
|
<div class="employee-stat">
|
||||||
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
|
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
|
||||||
<strong>{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
|
<strong>{{ $employeeTotals[$employee->id][$previousMonthKey] ?? '00:00' }}</strong>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -280,14 +321,51 @@ foreach ($data as $employee) {
|
|||||||
رکورد ترددی برای این شخص ثبت نشده است.
|
رکورد ترددی برای این شخص ثبت نشده است.
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
|
@php
|
||||||
|
$allMonthKeys = array_keys($recordsGrouped);
|
||||||
|
$recentMonthKeys = array_slice($allMonthKeys, 0, 3);
|
||||||
|
$olderMonthKeys = array_slice($allMonthKeys, 3);
|
||||||
|
$monthLabelOf = function ($month) use ($currentJalaliYear) {
|
||||||
|
if (preg_match('/^(\d{4})-(\d{2})$/', (string) $month, $mm)) {
|
||||||
|
try {
|
||||||
|
$jj = new Jalalian((int) $mm[1], (int) $mm[2], 1);
|
||||||
|
return ((int) $mm[1] === (int) $currentJalaliYear)
|
||||||
|
? $jj->format('%B')
|
||||||
|
: $jj->format('%B') . ' ' . $mm[1];
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
return $month;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $month;
|
||||||
|
};
|
||||||
|
@endphp
|
||||||
<div class="month-tabs">
|
<div class="month-tabs">
|
||||||
@foreach(array_keys($recordsGrouped) as $month)
|
@foreach($recentMonthKeys as $month)
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="month-tab tablinks-{{ $employee->id }}"
|
class="month-tab tablinks-{{ $employee->id }}"
|
||||||
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
|
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
|
||||||
{{ $month }}
|
{{ $monthLabelOf($month) }}
|
||||||
</button>
|
</button>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
|
@if(!empty($olderMonthKeys))
|
||||||
|
<div class="ui-month-more">
|
||||||
|
<button type="button"
|
||||||
|
class="month-tab ui-month-more-btn"
|
||||||
|
onclick="toggleAdminMonthMore(event, 'month-more-{{ $employee->id }}')"
|
||||||
|
title="ماههای دیگر">⋯</button>
|
||||||
|
<div id="month-more-{{ $employee->id }}" class="ui-month-more-menu hidden">
|
||||||
|
@foreach($olderMonthKeys as $month)
|
||||||
|
<button type="button"
|
||||||
|
class="month-tab tablinks-{{ $employee->id }} ui-month-more-item"
|
||||||
|
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}'); closeAdminMonthMore('month-more-{{ $employee->id }}');">
|
||||||
|
{{ $monthLabelOf($month) }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@foreach($recordsGrouped as $month => $records)
|
@foreach($recordsGrouped as $month => $records)
|
||||||
@@ -307,7 +385,7 @@ foreach ($data as $employee) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach($records as $row)
|
@foreach($records as $row)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, 'm/d') }}</td>
|
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, 'Y/m/d') }}</td>
|
||||||
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }}</td>
|
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }}</td>
|
||||||
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
|
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
|
||||||
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
|
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
|
||||||
@@ -366,6 +444,30 @@ foreach ($data as $employee) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleAdminMonthMore(evt, menuId) {
|
||||||
|
if (evt) evt.stopPropagation();
|
||||||
|
document.querySelectorAll('.ui-month-more-menu').forEach(menu => {
|
||||||
|
if (menu.id !== menuId) menu.classList.add('hidden');
|
||||||
|
});
|
||||||
|
const menu = document.getElementById(menuId);
|
||||||
|
if (menu) menu.classList.toggle('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAdminMonthMore(menuId) {
|
||||||
|
const menu = document.getElementById(menuId);
|
||||||
|
if (menu) menu.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('click', function () {
|
||||||
|
document.querySelectorAll('.ui-month-more-menu').forEach(menu => menu.classList.add('hidden'));
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.ui-month-more-menu').forEach(menu => {
|
||||||
|
menu.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
const firstEmployeeBtn = document.querySelector('.emp-btn');
|
const firstEmployeeBtn = document.querySelector('.emp-btn');
|
||||||
if (firstEmployeeBtn) {
|
if (firstEmployeeBtn) {
|
||||||
|
|||||||
@@ -932,7 +932,9 @@
|
|||||||
.ui-month-tabs {
|
.ui-month-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
overflow-x: auto;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
|
z-index: 50;
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.45rem;
|
padding: 0.45rem;
|
||||||
@@ -947,6 +949,41 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-month-more {
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
z-index: 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-menu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.35rem);
|
||||||
|
right: 0;
|
||||||
|
left: auto;
|
||||||
|
z-index: 100;
|
||||||
|
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.18);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-menu.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui-month-more-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.ui-month-tab,
|
.ui-month-tab,
|
||||||
.tablinks {
|
.tablinks {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
@@ -982,6 +1019,8 @@
|
|||||||
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
|
||||||
box-shadow: var(--shadow-soft);
|
box-shadow: var(--shadow-soft);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,19 +82,25 @@ if (!empty($user->tasks)) {
|
|||||||
|
|
||||||
<div id="output" class="text-red-500 text-sm mt-2 text-center"></div>
|
<div id="output" class="text-red-500 text-sm mt-2 text-center"></div>
|
||||||
|
|
||||||
<div class="flex justify-flex max-w-[240px]" style="align-items: center;">
|
<div class="flex justify-flex max-w-[240px] flex-wrap gap-2" style="align-items: center;">
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="w-1/2 p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600 cursor-pointer"
|
class="w-[calc(50%-4px)] p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600 cursor-pointer"
|
||||||
onclick="openModal('ثبت روز کاری جدید', 'new')">
|
onclick="openModal('ثبت روز کاری جدید', 'new')">
|
||||||
ثبت روز کاری
|
ثبت روز کاری
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button type="button" id="breakAttendance"
|
<button type="button" id="breakAttendance"
|
||||||
class="w-1/2 p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600
|
class="w-[calc(50%-4px)] p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600
|
||||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 cursor-pointer mx-auto"
|
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 cursor-pointer"
|
||||||
href="">
|
href="">
|
||||||
استراحت
|
استراحت
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="w-full p-2 text-sm text-white text-center rounded-xl bg-teal-500 hover:bg-teal-600 cursor-pointer"
|
||||||
|
onclick="openLeaveModal()">
|
||||||
|
درخواست مرخصی
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" id="tasksBtn"
|
<button type="button" id="tasksBtn"
|
||||||
class="text-white rounded-xl py-2 px-4 max-w-60 self-center bg-cyan-500 hover:bg-cyan-700 cursor-pointer w-full"
|
class="text-white rounded-xl py-2 px-4 max-w-60 self-center bg-cyan-500 hover:bg-cyan-700 cursor-pointer w-full"
|
||||||
@@ -120,11 +126,19 @@ if (!empty($user->tasks)) {
|
|||||||
{{$user->name}}
|
{{$user->name}}
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<button type="button"
|
<div class="flex w-full max-w-[240px] flex-wrap gap-2 justify-center">
|
||||||
class="w-1/2 p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600 cursor-pointer"
|
<button type="button"
|
||||||
onclick="openModal('ثبت روز کاری جدید', 'new')">
|
class="w-[calc(50%-4px)] p-2 text-sm text-white text-center rounded-xl bg-cyan-500 hover:bg-cyan-600 cursor-pointer"
|
||||||
ثبت روز کاری
|
onclick="openModal('ثبت روز کاری جدید', 'new')">
|
||||||
</button>
|
ثبت روز کاری
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="button"
|
||||||
|
class="w-[calc(50%-4px)] p-2 text-sm text-white text-center rounded-xl bg-teal-500 hover:bg-teal-600 cursor-pointer"
|
||||||
|
onclick="openLeaveModal()">
|
||||||
|
درخواست مرخصی
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -133,26 +147,67 @@ if (!empty($user->tasks)) {
|
|||||||
|
|
||||||
|
|
||||||
<div class="w-full flex justify-center mt-4 pb-20" style="">
|
<div class="w-full flex justify-center mt-4 pb-20" style="">
|
||||||
<div class="overflow-hidden rounded-[23px] max-w-[350px]">
|
<div class="overflow-visible rounded-[23px] max-w-[350px] relative z-20">
|
||||||
<div class=" border-b border-gray-300 text-center w-full">
|
<div class="text-center w-full">
|
||||||
|
|
||||||
|
|
||||||
<!-- Your Table Code Here -->
|
<!-- Your Table Code Here -->
|
||||||
<div class="w-[350px]">
|
<div class="w-[350px]">
|
||||||
|
|
||||||
@if(isset($_COOKIE['user_identity']))
|
@if(isset($_COOKIE['user_identity']))
|
||||||
{{-- قسمت پردازش دادهها: گروهبندی لیست حضور و غیاب بر اساس نام ماه شمسی --}}
|
{{-- قسمت پردازش دادهها: گروهبندی لیست حضور و غیاب بر اساس ماه+سال شمسی --}}
|
||||||
@php
|
@php
|
||||||
// بررسی وجود داده
|
|
||||||
$attendances = $user->attendances ?? collect();
|
$attendances = $user->attendances ?? collect();
|
||||||
|
$currentJalaliYear = \Morilog\Jalali\Jalalian::now()->getYear();
|
||||||
|
|
||||||
// گروهبندی دادهها بر اساس نام ماه شمسی
|
|
||||||
$groupedData = $attendances
|
$groupedData = $attendances
|
||||||
->filter(fn ($item) => !empty($item->entered_at))
|
->filter(fn ($item) => !empty($item->entered_at))
|
||||||
->groupBy(function ($item) {
|
->groupBy(function ($item) {
|
||||||
$month = \App\Support\JalaliHelper::format($item->entered_at, '%B', '');
|
$j = \App\Support\JalaliHelper::fromDateTime($item->entered_at);
|
||||||
return $month !== '' ? $month : 'نامعتبر';
|
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
|
@endphp
|
||||||
|
|
||||||
<div class="container inline-flex flex-col items-center justify-center">
|
<div class="container inline-flex flex-col items-center justify-center">
|
||||||
@@ -161,18 +216,36 @@ if (!empty($user->tasks)) {
|
|||||||
<h1>اطلاعاتی موجود نمیباشد</h1>
|
<h1>اطلاعاتی موجود نمیباشد</h1>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
{{-- دکمههای تب (نام ماهها) --}}
|
{{-- سه ماه اخیر + منوی بقیه --}}
|
||||||
<div class="ui-month-tabs">
|
<div class="ui-month-tabs">
|
||||||
@foreach($groupedData->keys() as $month)
|
@foreach($recentMonthKeys as $monthKey)
|
||||||
<button type="button" class="tablinks"
|
<button type="button" class="tablinks"
|
||||||
onclick="openCity(event, '{{$month}}')">
|
data-month-id="m-{{ $monthKey }}"
|
||||||
{{$month}}
|
onclick="openCity(event, 'm-{{ $monthKey }}')">
|
||||||
|
{{ $monthLabels[$monthKey] ?? $monthKey }}
|
||||||
</button>
|
</button>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
||||||
|
@if($olderMonthKeys->isNotEmpty())
|
||||||
|
<div class="ui-month-more">
|
||||||
|
<button type="button" class="tablinks ui-month-more-btn"
|
||||||
|
onclick="toggleMonthMore(event)"
|
||||||
|
title="ماههای دیگر">⋯</button>
|
||||||
|
<div id="month-more-menu" class="ui-month-more-menu hidden">
|
||||||
|
@foreach($olderMonthKeys as $monthKey)
|
||||||
|
<button type="button" class="tablinks ui-month-more-item"
|
||||||
|
data-month-id="m-{{ $monthKey }}"
|
||||||
|
onclick="openCity(event, 'm-{{ $monthKey }}'); closeMonthMore();">
|
||||||
|
{{ $monthLabels[$monthKey] ?? $monthKey }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@foreach($groupedData as $month => $records)
|
@foreach($groupedData as $monthKey => $records)
|
||||||
<div id="{{$month}}" class="tabcontent ui-attendance-card">
|
<div id="m-{{ $monthKey }}" class="tabcontent ui-attendance-card">
|
||||||
<table class="ui-attendance-table">
|
<table class="ui-attendance-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -191,11 +264,11 @@ if (!empty($user->tasks)) {
|
|||||||
@php
|
@php
|
||||||
$monthLateMinutes = 0;
|
$monthLateMinutes = 0;
|
||||||
$monthDailyFine = 0;
|
$monthDailyFine = 0;
|
||||||
$monthKey = null;
|
$monthKeyCarbon = null;
|
||||||
$firstEntered = $records->first()?->entered_at;
|
$firstEntered = $records->first()?->entered_at;
|
||||||
if ($firstEntered && \App\Support\JalaliHelper::fromDateTime($firstEntered)) {
|
if ($firstEntered && \App\Support\JalaliHelper::fromDateTime($firstEntered)) {
|
||||||
$monthKey = \Carbon\Carbon::parse($firstEntered);
|
$monthKeyCarbon = \Carbon\Carbon::parse($firstEntered);
|
||||||
[$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKey);
|
[$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKeyCarbon);
|
||||||
$monthWeeklyFine = (int) \App\Models\LatePeriodFine::query()
|
$monthWeeklyFine = (int) \App\Models\LatePeriodFine::query()
|
||||||
->where('user_identity', $user->user_name)
|
->where('user_identity', $user->user_name)
|
||||||
->where('period_type', 'weekly')
|
->where('period_type', 'weekly')
|
||||||
@@ -209,6 +282,11 @@ if (!empty($user->tasks)) {
|
|||||||
@php
|
@php
|
||||||
$monthLateMinutes += (int) ($item->late_minutes ?? 0);
|
$monthLateMinutes += (int) ($item->late_minutes ?? 0);
|
||||||
$monthDailyFine += (int) ($item->daily_fine_amount ?? 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
|
@endphp
|
||||||
<tr class="hover:bg-cyan-500 cursor-pointer transition duration-150"
|
<tr class="hover:bg-cyan-500 cursor-pointer transition duration-150"
|
||||||
style="border-bottom: 1px solid #ddd; background-color: {{$item->requested ? '#bfdbfe' : '#ffffff' }}"
|
style="border-bottom: 1px solid #ddd; background-color: {{$item->requested ? '#bfdbfe' : '#ffffff' }}"
|
||||||
@@ -223,19 +301,22 @@ if (!empty($user->tasks)) {
|
|||||||
|
|
||||||
{{-- تاریخ --}}
|
{{-- تاریخ --}}
|
||||||
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{"#e6e6e6" }};">
|
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{"#e6e6e6" }};">
|
||||||
{{ \App\Support\JalaliHelper::format($item->entered_at, 'm/d') }}
|
{{ \App\Support\JalaliHelper::format($item->entered_at, 'Y/m/d') }}
|
||||||
<span class="text-xs text-gray-500 block">
|
<span class="text-xs text-gray-500 block">
|
||||||
{{ \App\Support\JalaliHelper::format($item->entered_at, '%A', '') }}
|
{{ \App\Support\JalaliHelper::format($item->entered_at, '%A', '') }}
|
||||||
|
@if($isNonWorkDay)
|
||||||
|
· اضافهکاری
|
||||||
|
@endif
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{{-- زمان ورود --}}
|
{{-- زمان ورود --}}
|
||||||
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{$item->entered_at && $item->distance ? ($item->requested !== null ? '#fad1d1' :($item->distance < 50 ? '#d1fae5' : '#edae80')) : '' }};">
|
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{$item->entered_at && $item->distance ? ($item->requested !== null ? '#fad1d1' :($item->distance < 100 ? '#d1fae5' : '#edae80')) : '' }};">
|
||||||
{{ $item->entered_at ? \Carbon\Carbon::parse($item->entered_at)->format('H:i') : '-' }}
|
{{ $item->entered_at ? \Carbon\Carbon::parse($item->entered_at)->format('H:i') : '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
{{-- زمان خروج --}}
|
{{-- زمان خروج --}}
|
||||||
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{$item->exited_at && $item->distance_exit ? ($item->requested !== null ? '#fad1d1' :($item->distance_exit < 50 ? '#d1fae5' : '#edae80')) : '' }};">
|
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{$item->exited_at && $item->distance_exit ? ($item->requested !== null ? '#fad1d1' :($item->distance_exit < 100 ? '#d1fae5' : '#edae80')) : '' }};">
|
||||||
{{ $item->exited_at ? \Carbon\Carbon::parse($item->exited_at)->format('H:i') : '-' }}
|
{{ $item->exited_at ? \Carbon\Carbon::parse($item->exited_at)->format('H:i') : '-' }}
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
@@ -246,7 +327,9 @@ if (!empty($user->tasks)) {
|
|||||||
|
|
||||||
{{-- تأخیر --}}
|
{{-- تأخیر --}}
|
||||||
<td style="border-right: 1px solid #ddd; padding: 4px; {{ ($item->daily_fine_minutes ?? 0) > 0 ? 'color:#dc2626;font-weight:600;' : (($item->late_minutes ?? 0) > 0 ? 'color:#d97706;' : '') }}">
|
<td style="border-right: 1px solid #ddd; padding: 4px; {{ ($item->daily_fine_minutes ?? 0) > 0 ? 'color:#dc2626;font-weight:600;' : (($item->late_minutes ?? 0) > 0 ? 'color:#d97706;' : '') }}">
|
||||||
@if(($item->late_minutes ?? 0) > 0)
|
@if($isNonWorkDay)
|
||||||
|
<span class="text-[10px] text-emerald-600">اضافهکاری</span>
|
||||||
|
@elseif(($item->late_minutes ?? 0) > 0)
|
||||||
{{ $item->late_minutes }}د
|
{{ $item->late_minutes }}د
|
||||||
@if(($item->daily_fine_amount ?? 0) > 0)
|
@if(($item->daily_fine_amount ?? 0) > 0)
|
||||||
<span class="text-[10px] block">{{ number_format($item->daily_fine_amount) }}ت</span>
|
<span class="text-[10px] block">{{ number_format($item->daily_fine_amount) }}ت</span>
|
||||||
@@ -403,6 +486,70 @@ if (!empty($user->tasks)) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{{-- Leave Request Modal --}}
|
||||||
|
<div id="leave-modal-container"
|
||||||
|
class="ui-modal-backdrop centered hidden">
|
||||||
|
<div class="ui-modal-panel max-w-sm w-full">
|
||||||
|
<h2 class="ui-title">درخواست مرخصی</h2>
|
||||||
|
<p class="ui-subtitle">نوع و بازه مرخصی را مشخص کنید</p>
|
||||||
|
|
||||||
|
<form method="post" action="{{ route('leave.store') }}" class="flex flex-col gap-3 items-stretch px-2"
|
||||||
|
onsubmit="return confirm('درخواست مرخصی ارسال شود؟')">
|
||||||
|
@csrf
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-center">
|
||||||
|
<label class="flex items-center gap-1 text-sm cursor-pointer">
|
||||||
|
<input type="radio" name="type" value="daily" checked onchange="toggleLeaveType()">
|
||||||
|
روزانه
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-1 text-sm cursor-pointer">
|
||||||
|
<input type="radio" name="type" value="hourly" onchange="toggleLeaveType()">
|
||||||
|
ساعتی
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="leave-daily-fields" class="flex flex-col gap-2">
|
||||||
|
<label class="text-sm">از تاریخ</label>
|
||||||
|
<input type="text" name="date_from" id="leave_date_from"
|
||||||
|
class="leave-datepicker form-control border p-2 rounded" placeholder="تاریخ شروع" readonly>
|
||||||
|
<label class="text-sm">تا تاریخ</label>
|
||||||
|
<input type="text" name="date_to" id="leave_date_to"
|
||||||
|
class="leave-datepicker form-control border p-2 rounded" placeholder="تاریخ پایان" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="leave-hourly-fields" class="flex flex-col gap-2" style="display:none;">
|
||||||
|
<label class="text-sm">تاریخ</label>
|
||||||
|
<input type="text" name="date" id="leave_date"
|
||||||
|
class="leave-datepicker form-control border p-2 rounded" placeholder="تاریخ" readonly>
|
||||||
|
<label class="text-sm">از ساعت</label>
|
||||||
|
<div class="input-group clockpicker border-2 border-gray-300 rounded-lg">
|
||||||
|
<input type="text" name="time_from" id="leave_time_from"
|
||||||
|
class="form-control cursor-default p-2 w-full" placeholder="08:30" readonly>
|
||||||
|
</div>
|
||||||
|
<label class="text-sm">تا ساعت</label>
|
||||||
|
<div class="input-group clockpicker border-2 border-gray-300 rounded-lg">
|
||||||
|
<input type="text" name="time_to" id="leave_time_to"
|
||||||
|
class="form-control cursor-default p-2 w-full" placeholder="12:00" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<label class="text-sm">دلیل (اختیاری)</label>
|
||||||
|
<textarea name="reason" rows="2" class="border p-2 rounded text-sm" placeholder="توضیح کوتاه..."></textarea>
|
||||||
|
|
||||||
|
<div class="flex gap-2 justify-center mt-2">
|
||||||
|
<button type="submit" class="bg-teal-500 hover:bg-teal-600 text-white rounded p-2 w-[70%]">
|
||||||
|
ارسال درخواست
|
||||||
|
</button>
|
||||||
|
<button type="button" class="bg-gray-400 hover:bg-gray-500 text-white rounded p-2 w-[30%]"
|
||||||
|
onclick="closeLeaveModal()">
|
||||||
|
بستن
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{{-- START TASK MODAL --}}
|
{{-- START TASK MODAL --}}
|
||||||
<div id="taskModal"
|
<div id="taskModal"
|
||||||
class="ui-modal-backdrop hidden"
|
class="ui-modal-backdrop hidden"
|
||||||
@@ -499,8 +646,12 @@ if (!empty($user->tasks)) {
|
|||||||
const distanceMessage = document.getElementById("distanceMessage");
|
const distanceMessage = document.getElementById("distanceMessage");
|
||||||
|
|
||||||
|
|
||||||
|
// مختصات دفتر / شرکت
|
||||||
const referenceLat = 32.6504751;
|
const referenceLat = 32.6504751;
|
||||||
const referenceLng = 51.6936045;
|
const referenceLng = 51.6936045;
|
||||||
|
// شعاع مجاز (متر) — GPS داخل ساختمان معمولاً ۲۰–۸۰ متر خطا دارد
|
||||||
|
const OFFICE_RADIUS_METERS = 100;
|
||||||
|
const MAX_ACCURACY_CREDIT_METERS = 80;
|
||||||
|
|
||||||
const submitBtn = document.getElementById("submitBtn");
|
const submitBtn = document.getElementById("submitBtn");
|
||||||
const latInput = document.getElementById("latitude");
|
const latInput = document.getElementById("latitude");
|
||||||
@@ -530,57 +681,136 @@ if (!empty($user->tasks)) {
|
|||||||
return earthRadius * c;
|
return earthRadius * c;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.geolocation.getCurrentPosition(
|
function isInsideOffice(distanceMeters, accuracyMeters) {
|
||||||
position => {
|
const accuracyCredit = Math.min(Math.max(0, accuracyMeters || 0), MAX_ACCURACY_CREDIT_METERS);
|
||||||
const lat = position.coords.latitude;
|
const effectiveDistance = Math.max(0, distanceMeters - accuracyCredit);
|
||||||
const lng = position.coords.longitude;
|
return effectiveDistance <= OFFICE_RADIUS_METERS;
|
||||||
|
}
|
||||||
|
|
||||||
latInput.value = lat;
|
function applyLocationResult(lat, lng, accuracyMeters) {
|
||||||
lngInput.value = lng;
|
latInput.value = lat;
|
||||||
|
lngInput.value = lng;
|
||||||
|
|
||||||
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
||||||
distanceInput.value = dist;
|
distanceInput.value = dist;
|
||||||
if (dist < 20) {
|
const inside = isInsideOffice(dist, accuracyMeters);
|
||||||
distanceMessage.textContent = `مکان: شرکت`;
|
const accuracyText = accuracyMeters
|
||||||
} else {
|
? ` · دقت ±${Math.round(accuracyMeters)}م`
|
||||||
distanceMessage.textContent = `مکان: خارج از شرکت`;
|
: '';
|
||||||
}
|
|
||||||
|
|
||||||
distanceMessage.classList.remove("bg-gray-400", "bg-blue-500", "bg-green-500", "bg-orange-500", "cursor-not-allowed");
|
distanceMessage.textContent = inside
|
||||||
if (dist < 10) {
|
? `مکان: شرکت (~${dist}م)${accuracyText}`
|
||||||
distanceMessage.classList.add("text-green-500");
|
: `مکان: خارج از شرکت (~${dist}م)${accuracyText}`;
|
||||||
} else if (dist < 50) {
|
|
||||||
distanceMessage.classList.add("text-blue-500");
|
|
||||||
} else if (dist > 2000) {
|
|
||||||
distanceMessage.classList.add("text-orange-500");
|
|
||||||
} else {
|
|
||||||
distanceMessage.classList.add("text-gray-400");
|
|
||||||
}
|
|
||||||
submitBtn.disabled = false;
|
|
||||||
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
|
||||||
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
|
||||||
|
|
||||||
},
|
distanceMessage.classList.remove(
|
||||||
error => {
|
"bg-gray-400", "bg-blue-500", "bg-green-500", "bg-orange-500",
|
||||||
const lat = 32.6520532791536; // از مختصات ثابت استفاده کنید
|
"cursor-not-allowed", "text-green-500", "text-blue-500",
|
||||||
const lng = 51.68472614422439; // از مختصات ثابت استفاده کنید
|
"text-orange-500", "text-gray-400"
|
||||||
|
);
|
||||||
|
if (inside) {
|
||||||
|
distanceMessage.classList.add("text-green-500");
|
||||||
|
} else if (dist <= OFFICE_RADIUS_METERS * 2) {
|
||||||
|
distanceMessage.classList.add("text-blue-500");
|
||||||
|
} else if (dist > 2000) {
|
||||||
|
distanceMessage.classList.add("text-orange-500");
|
||||||
|
} else {
|
||||||
|
distanceMessage.classList.add("text-gray-400");
|
||||||
|
}
|
||||||
|
|
||||||
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
submitBtn.disabled = false;
|
||||||
distanceInput.value = dist;
|
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
||||||
distanceMessage.textContent = `مکان: خارج از شرکت`;
|
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
||||||
|
}
|
||||||
|
|
||||||
distanceMessage.classList.remove("bg-gray-400", "bg-blue-500", "bg-green-500", "bg-orange-500", "cursor-not-allowed");
|
function locateUser() {
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
distanceMessage.textContent = 'موقعیتیاب در دسترس نیست';
|
||||||
distanceMessage.classList.add("text-orange-500");
|
distanceMessage.classList.add("text-orange-500");
|
||||||
submitBtn.disabled = false;
|
submitBtn.disabled = false;
|
||||||
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
||||||
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
||||||
|
return;
|
||||||
// alert("⛔ خطا در دریافت موقعیت مکانی: " + error.message), {
|
|
||||||
// enableHighAccuracy: true,
|
|
||||||
// timeout: 10000,
|
|
||||||
// maximumAge: 0
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
distanceMessage.textContent = 'در حال بررسی موقعیت...';
|
||||||
|
distanceMessage.classList.remove("text-green-500", "text-blue-500", "text-orange-500", "text-gray-400");
|
||||||
|
|
||||||
|
const geoOptions = {
|
||||||
|
enableHighAccuracy: true,
|
||||||
|
timeout: 20000,
|
||||||
|
maximumAge: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let bestReading = null;
|
||||||
|
let watchId = null;
|
||||||
|
let finished = false;
|
||||||
|
|
||||||
|
const finishWithBest = () => {
|
||||||
|
if (finished) return;
|
||||||
|
finished = true;
|
||||||
|
if (watchId !== null) {
|
||||||
|
navigator.geolocation.clearWatch(watchId);
|
||||||
|
}
|
||||||
|
if (bestReading) {
|
||||||
|
applyLocationResult(
|
||||||
|
bestReading.lat,
|
||||||
|
bestReading.lng,
|
||||||
|
bestReading.accuracy
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const considerReading = (position) => {
|
||||||
|
const lat = position.coords.latitude;
|
||||||
|
const lng = position.coords.longitude;
|
||||||
|
const accuracy = position.coords.accuracy || 9999;
|
||||||
|
const dist = getDistanceInMeters(referenceLat, referenceLng, lat, lng);
|
||||||
|
const score = dist + Math.min(accuracy, 150);
|
||||||
|
|
||||||
|
if (!bestReading || score < bestReading.score) {
|
||||||
|
bestReading = { lat, lng, accuracy, dist, score };
|
||||||
|
}
|
||||||
|
|
||||||
|
// اگر با دقت خوب داخل محدوده شدیم، زودتر تمام کن
|
||||||
|
if (isInsideOffice(dist, accuracy) && accuracy <= 60) {
|
||||||
|
finishWithBest();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
considerReading(position);
|
||||||
|
// چند ثانیه watch برای خواندن دقیقتر داخل ساختمان
|
||||||
|
watchId = navigator.geolocation.watchPosition(
|
||||||
|
considerReading,
|
||||||
|
() => finishWithBest(),
|
||||||
|
geoOptions
|
||||||
|
);
|
||||||
|
setTimeout(finishWithBest, 5000);
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
// تلاش دوم بدون الزام دقت بالا
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
considerReading(position);
|
||||||
|
finishWithBest();
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
distanceMessage.textContent = 'موقعیت دریافت نشد — میتوانید ورود بزنید';
|
||||||
|
distanceMessage.classList.add("text-orange-500");
|
||||||
|
distanceInput.value = '';
|
||||||
|
submitBtn.disabled = false;
|
||||||
|
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
||||||
|
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
||||||
|
},
|
||||||
|
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 60000 }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
geoOptions
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
locateUser();
|
||||||
const attendanceForm = document.getElementById('attendanceForm');
|
const attendanceForm = document.getElementById('attendanceForm');
|
||||||
|
|
||||||
const output = document.getElementById('output');
|
const output = document.getElementById('output');
|
||||||
@@ -629,30 +859,56 @@ if (!empty($user->tasks)) {
|
|||||||
|
|
||||||
function openCity(evt, Month) {
|
function openCity(evt, Month) {
|
||||||
var i, tabcontent, tablinks;
|
var i, tabcontent, tablinks;
|
||||||
// مخفی کردن تمام محتواها
|
|
||||||
tabcontent = document.getElementsByClassName("tabcontent");
|
tabcontent = document.getElementsByClassName("tabcontent");
|
||||||
for (i = 0; i < tabcontent.length; i++) {
|
for (i = 0; i < tabcontent.length; i++) {
|
||||||
tabcontent[i].style.display = "none";
|
tabcontent[i].style.display = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
// غیرفعال کردن تمام تبها
|
|
||||||
tablinks = document.getElementsByClassName("tablinks");
|
tablinks = document.getElementsByClassName("tablinks");
|
||||||
for (i = 0; i < tablinks.length; i++) {
|
for (i = 0; i < tablinks.length; i++) {
|
||||||
tablinks[i].className = tablinks[i].className.replace(" active bg-gray-300 font-bold", "");
|
tablinks[i].className = tablinks[i].className.replace(" active bg-gray-300 font-bold", "");
|
||||||
// اضافه کردن استایل دیفالت اگر کلاس اکتیو حذف شد
|
|
||||||
if (!tablinks[i].className.includes("active")) {
|
if (!tablinks[i].className.includes("active")) {
|
||||||
tablinks[i].style.backgroundColor = "";
|
tablinks[i].style.backgroundColor = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// نمایش تب انتخاب شده
|
const panel = document.getElementById(Month);
|
||||||
document.getElementById(Month).style.display = "block";
|
if (panel) {
|
||||||
evt.currentTarget.className += " active bg-gray-300 font-bold";
|
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.getElementById('month-more-menu')?.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener('click', function () {
|
||||||
|
closeMonthMore();
|
||||||
|
});
|
||||||
|
|
||||||
// باز کردن اولین تب به صورت خودکار هنگام لود صفحه
|
// باز کردن اولین تب به صورت خودکار هنگام لود صفحه
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
let firstTab = document.querySelector(".tablinks");
|
let firstTab = document.querySelector(".ui-month-tabs > .tablinks");
|
||||||
if (firstTab) {
|
if (firstTab) {
|
||||||
firstTab.click();
|
firstTab.click();
|
||||||
}
|
}
|
||||||
@@ -1065,6 +1321,69 @@ if (!empty($user->tasks)) {
|
|||||||
document.getElementById('modal-container').classList.add('hidden');
|
document.getElementById('modal-container').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openLeaveModal() {
|
||||||
|
document.getElementById('leave-modal-container').classList.remove('hidden');
|
||||||
|
toggleLeaveType();
|
||||||
|
if (!window._leavePickersReady) {
|
||||||
|
$('.leave-datepicker').persianDatepicker({
|
||||||
|
format: "YYYY/MM/DD",
|
||||||
|
calendar: {
|
||||||
|
persian: {
|
||||||
|
locale: 'fa',
|
||||||
|
leapYearMode: "astronomical"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
observer: true,
|
||||||
|
initialValue: false,
|
||||||
|
autoClose: true,
|
||||||
|
toolbox: {
|
||||||
|
calendarSwitch: {enabled: false},
|
||||||
|
todayButton: {enabled: true, text: {fa: "امروز"}}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
$('#leave-modal-container .clockpicker').clockpicker({
|
||||||
|
autoclose: false,
|
||||||
|
placement: 'center',
|
||||||
|
donetext: 'ثبت',
|
||||||
|
vibrate: true,
|
||||||
|
});
|
||||||
|
window._leavePickersReady = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLeaveModal() {
|
||||||
|
document.getElementById('leave-modal-container').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLeaveType() {
|
||||||
|
const type = document.querySelector('input[name="type"]:checked')?.value || 'daily';
|
||||||
|
const daily = document.getElementById('leave-daily-fields');
|
||||||
|
const hourly = document.getElementById('leave-hourly-fields');
|
||||||
|
if (type === 'hourly') {
|
||||||
|
daily.style.display = 'none';
|
||||||
|
hourly.style.display = 'flex';
|
||||||
|
document.getElementById('leave_date_from').required = false;
|
||||||
|
document.getElementById('leave_date_to').required = false;
|
||||||
|
document.getElementById('leave_date').required = true;
|
||||||
|
document.getElementById('leave_time_from').required = true;
|
||||||
|
document.getElementById('leave_time_to').required = true;
|
||||||
|
} else {
|
||||||
|
daily.style.display = 'flex';
|
||||||
|
hourly.style.display = 'none';
|
||||||
|
document.getElementById('leave_date_from').required = true;
|
||||||
|
document.getElementById('leave_date_to').required = true;
|
||||||
|
document.getElementById('leave_date').required = false;
|
||||||
|
document.getElementById('leave_time_from').required = false;
|
||||||
|
document.getElementById('leave_time_to').required = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('leave-modal-container')?.addEventListener('click', (e) => {
|
||||||
|
if (e.target === document.getElementById('leave-modal-container')) {
|
||||||
|
closeLeaveModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('modal-container').addEventListener('click', (e) => {
|
document.getElementById('modal-container').addEventListener('click', (e) => {
|
||||||
if (e.target === document.getElementById('modal-container')) {
|
if (e.target === document.getElementById('modal-container')) {
|
||||||
closeModal();
|
closeModal();
|
||||||
|
|||||||
@@ -19,6 +19,9 @@
|
|||||||
{{-- align-center text-sm text-start">--}}
|
{{-- 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">--}}
|
{{-- <div class="grid grid-cols-2 h-64 gap-2 justify-items-stretch content-between text-sm indent-2.5 align-middle divide">--}}
|
||||||
@csrf
|
@csrf
|
||||||
|
@if(!empty($viewerIsAdmin))
|
||||||
|
<input type="hidden" name="target_username" value="{{ $user->user_name }}">
|
||||||
|
@endif
|
||||||
|
|
||||||
<label class="pb-3">نام و نام خانوادگی:</label>
|
<label class="pb-3">نام و نام خانوادگی:</label>
|
||||||
<input type="text" name="name" value="{{ $user->name ?? '' }}"
|
<input type="text" name="name" value="{{ $user->name ?? '' }}"
|
||||||
@@ -71,6 +74,18 @@
|
|||||||
<option value="1" {{ $user->is_admin == 1 ? 'selected' : '' }}>ادمین</option>
|
<option value="1" {{ $user->is_admin == 1 ? 'selected' : '' }}>ادمین</option>
|
||||||
</select>
|
</select>
|
||||||
@endif
|
@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"
|
<button type="submit"
|
||||||
class="col-span-2 bg-cyan-500 hover:bg-cyan-700 cursor-pointer
|
class="col-span-2 bg-cyan-500 hover:bg-cyan-700 cursor-pointer
|
||||||
rounded-xl border border-400 justify-self-center
|
rounded-xl border border-400 justify-self-center
|
||||||
|
|||||||
@@ -4,8 +4,10 @@ use Morilog\Jalali\Jalalian;
|
|||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
|
||||||
$monthlyTotals = [];
|
$monthlyTotals = [];
|
||||||
|
$currentJalaliYear = Jalalian::now()->getYear();
|
||||||
|
$monthLabels = [];
|
||||||
|
|
||||||
foreach ($data as $month => $attendances) {
|
foreach ($data as $monthKey => $attendances) {
|
||||||
$tsec = 0;
|
$tsec = 0;
|
||||||
|
|
||||||
foreach ($attendances as $attendance) {
|
foreach ($attendances as $attendance) {
|
||||||
@@ -18,9 +20,27 @@ foreach ($data as $month => $attendances) {
|
|||||||
$hours = floor($tsec / 3600);
|
$hours = floor($tsec / 3600);
|
||||||
$minutes = floor(($tsec % 3600) / 60);
|
$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')
|
@extends('app')
|
||||||
|
|
||||||
@@ -32,13 +52,27 @@ foreach ($data as $month => $attendances) {
|
|||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
<div class="ui-month-tabs" style="margin-top: 5rem;">
|
<div class="ui-month-tabs" style="margin-top: 5rem;">
|
||||||
@foreach(array_keys($data) as $month)
|
@foreach($recentMonthKeys as $monthKey)
|
||||||
<button type="button" class="tablinks" onclick="openCity(event, '{{ $month }}')">{{ $month }}</button>
|
<button type="button" class="tablinks"
|
||||||
|
onclick="openCity(event, 'm-{{ $monthKey }}')">{{ $monthLabels[$monthKey] ?? $monthKey }}</button>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
@if(!empty($olderMonthKeys))
|
||||||
|
<div class="ui-month-more">
|
||||||
|
<button type="button" class="tablinks ui-month-more-btn" onclick="toggleMonthMore(event)">⋯</button>
|
||||||
|
<div id="month-more-menu" class="ui-month-more-menu hidden">
|
||||||
|
@foreach($olderMonthKeys as $monthKey)
|
||||||
|
<button type="button" class="tablinks ui-month-more-item"
|
||||||
|
onclick="openCity(event, 'm-{{ $monthKey }}'); closeMonthMore();">
|
||||||
|
{{ $monthLabels[$monthKey] ?? $monthKey }}
|
||||||
|
</button>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@foreach($data as $month => $records)
|
@foreach($data as $monthKey => $records)
|
||||||
<div id="{{ $month }}" class="tabcontent ui-attendance-card">
|
<div id="m-{{ $monthKey }}" class="tabcontent ui-attendance-card">
|
||||||
<table class="ui-attendance-table">
|
<table class="ui-attendance-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -52,7 +86,7 @@ foreach ($data as $month => $attendances) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach($records as $row)
|
@foreach($records as $row)
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, 'm/d') }}</td>
|
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, 'Y/m/d') }}</td>
|
||||||
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }}</td>
|
<td>{{ \App\Support\JalaliHelper::format($row->entered_at, '%A') }}</td>
|
||||||
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
|
<td>{{ Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
|
||||||
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
|
<td>{{ $row->exited_at ? Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
|
||||||
@@ -61,7 +95,7 @@ foreach ($data as $month => $attendances) {
|
|||||||
@endforeach
|
@endforeach
|
||||||
<tr>
|
<tr>
|
||||||
<td colspan="5" class="table-summary">
|
<td colspan="5" class="table-summary">
|
||||||
مجموع کارکرد این ماه: {{ $monthlyTotals[$month] }}
|
مجموع کارکرد این ماه: {{ $monthlyTotals[$monthKey] }}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -78,13 +112,24 @@ foreach ($data as $month => $attendances) {
|
|||||||
|
|
||||||
const panel = document.getElementById(Month);
|
const panel = document.getElementById(Month);
|
||||||
if (panel) panel.style.display = 'block';
|
if (panel) panel.style.display = 'block';
|
||||||
|
if (evt && evt.currentTarget) evt.currentTarget.classList.add('active');
|
||||||
if (evt.currentTarget) evt.currentTarget.classList.add('active');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
function toggleMonthMore(evt) {
|
||||||
const firstTab = document.querySelector('.tablinks');
|
if (evt) evt.stopPropagation();
|
||||||
if (firstTab) firstTab.click();
|
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();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@endsection
|
@endsection
|
||||||
|
|||||||
@@ -52,10 +52,17 @@ if (isset($task)) {
|
|||||||
|
|
||||||
<div class="ui-form-field">
|
<div class="ui-form-field">
|
||||||
<label for="start_date">تاریخ شروع <span class="text-red-500">*</span></label>
|
<label for="start_date">تاریخ شروع <span class="text-red-500">*</span></label>
|
||||||
|
@php
|
||||||
|
$existingStart = isset($task) && !empty($task->start_date) ? $task->start_date : null;
|
||||||
|
$defaultStartGregorian = $existingStart
|
||||||
|
? \App\Support\JalaliHelper::toGregorianDate($existingStart)
|
||||||
|
: now()->format('Y-m-d');
|
||||||
|
@endphp
|
||||||
<input type="text" id="start_date" name="start_date_display" class="ui-input"
|
<input type="text" id="start_date" name="start_date_display" class="ui-input"
|
||||||
value="{{$task->start_date ?? null}}"
|
value=""
|
||||||
placeholder="تاریخ شروع" autocomplete="off" readonly required>
|
placeholder="تاریخ شروع" autocomplete="off" readonly required>
|
||||||
<input type="hidden" name="start_date" id="start_date_gregorian">
|
<input type="hidden" name="start_date" id="start_date_gregorian"
|
||||||
|
value="{{ old('start_date', $defaultStartGregorian) }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ui-form-field">
|
<div class="ui-form-field">
|
||||||
@@ -201,11 +208,18 @@ if (isset($task)) {
|
|||||||
updateSelectedCount();
|
updateSelectedCount();
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
|
function jalaliFromGregorianYmd(ymd) {
|
||||||
|
if (!ymd) return '';
|
||||||
|
const parts = String(ymd).split('-').map(Number);
|
||||||
|
if (parts.length < 3 || parts.some(n => Number.isNaN(n))) return '';
|
||||||
|
const gDate = new Date(parts[0], parts[1] - 1, parts[2]);
|
||||||
|
return new persianDate(gDate).toLocale('en').format('YYYY/MM/DD');
|
||||||
|
}
|
||||||
|
|
||||||
function syncGregorianFromDisplay(displaySelector, hiddenSelector) {
|
function syncGregorianFromDisplay(displaySelector, hiddenSelector) {
|
||||||
const displayVal = $(displaySelector).val();
|
const displayVal = $(displaySelector).val();
|
||||||
if (!displayVal || $(hiddenSelector).val()) return;
|
if (!displayVal || $(hiddenSelector).val()) return;
|
||||||
try {
|
try {
|
||||||
// اگر مقدار نمایشی شمسی YYYY/MM/DD باشد
|
|
||||||
const parts = String(displayVal).replace(/-/g, '/').split('/');
|
const parts = String(displayVal).replace(/-/g, '/').split('/');
|
||||||
if (parts.length === 3) {
|
if (parts.length === 3) {
|
||||||
const pDate = new persianDate([Number(parts[0]), Number(parts[1]), Number(parts[2])]);
|
const pDate = new persianDate([Number(parts[0]), Number(parts[1]), Number(parts[2])]);
|
||||||
@@ -216,51 +230,62 @@ if (isset($task)) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#start_date').persianDatepicker({
|
const startPickerOptions = {
|
||||||
format: 'YYYY/MM/DD',
|
format: 'YYYY/MM/DD',
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
|
initialValue: false,
|
||||||
toolbox: {
|
toolbox: {
|
||||||
calendarSwitch: {enabled: false},
|
calendarSwitch: {enabled: false},
|
||||||
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
||||||
},
|
},
|
||||||
onSelect: function (unix) {
|
onSelect: function (unix) {
|
||||||
let gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
const gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
||||||
let dateStr = gregorian.format('YYYY-MM-DD');
|
$('#start_date_gregorian').val(gregorian.format('YYYY-MM-DD'));
|
||||||
$('#start_date_gregorian').val(dateStr);
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
$('#start_date').persianDatepicker(startPickerOptions);
|
||||||
|
|
||||||
$('#target_date').persianDatepicker({
|
$('#target_date').persianDatepicker({
|
||||||
format: 'YYYY/MM/DD',
|
format: 'YYYY/MM/DD',
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
|
initialValue: false,
|
||||||
toolbox: {
|
toolbox: {
|
||||||
calendarSwitch: {enabled: false},
|
calendarSwitch: {enabled: false},
|
||||||
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
||||||
},
|
},
|
||||||
onSelect: function (unix) {
|
onSelect: function (unix) {
|
||||||
let gregorianDate = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
const gregorianDate = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
||||||
let dateStrTarget = gregorianDate.format('YYYY-MM-DD');
|
$('#target_date_gregorian').val(gregorianDate.format('YYYY-MM-DD'));
|
||||||
$('#target_date_gregorian').val(dateStrTarget);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// اگر مقدار اولیه از دیتابیس آمده، فیلد مخفی میلادی را هم پر کن
|
// نمایش شمسی را از روی تاریخ میلادی بساز (نه برعکس) تا دیتپیکر اشتباه تفسیر نکند
|
||||||
@if(!empty($task?->start_date))
|
@if(!empty($task) && !empty($task->start_date))
|
||||||
$('#start_date_gregorian').val(@json(\App\Support\JalaliHelper::toGregorianDate($task->start_date)));
|
$('#start_date_gregorian').val(@json(\App\Support\JalaliHelper::toGregorianDate($task->start_date)));
|
||||||
$('#start_date').val(@json(\App\Support\JalaliHelper::format($task->start_date, 'Y/m/d', '')));
|
|
||||||
@endif
|
@endif
|
||||||
@if(!empty($task?->target_date))
|
const startGregorian = $('#start_date_gregorian').val() || @json($defaultStartGregorian);
|
||||||
$('#target_date_gregorian').val(@json(\App\Support\JalaliHelper::toGregorianDate($task->target_date)));
|
$('#start_date_gregorian').val(startGregorian);
|
||||||
$('#target_date').val(@json(\App\Support\JalaliHelper::format($task->target_date, 'Y/m/d', '')));
|
$('#start_date').val(jalaliFromGregorianYmd(startGregorian));
|
||||||
|
|
||||||
|
@if(!empty($task) && !empty($task->target_date))
|
||||||
|
const targetGregorian = @json(\App\Support\JalaliHelper::toGregorianDate($task->target_date));
|
||||||
|
$('#target_date_gregorian').val(targetGregorian);
|
||||||
|
$('#target_date').val(jalaliFromGregorianYmd(targetGregorian));
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
$('form').on('submit', function (e) {
|
$('form').on('submit', function () {
|
||||||
syncGregorianFromDisplay('#start_date', '#start_date_gregorian');
|
syncGregorianFromDisplay('#start_date', '#start_date_gregorian');
|
||||||
syncGregorianFromDisplay('#target_date', '#target_date_gregorian');
|
syncGregorianFromDisplay('#target_date', '#target_date_gregorian');
|
||||||
if (!$('#start_date_gregorian').val()) {
|
if (!$('#start_date_gregorian').val()) {
|
||||||
e.preventDefault();
|
const today = new Date();
|
||||||
alert('لطفاً تاریخ شروع را از تقویم انتخاب کنید.');
|
const ymd = [
|
||||||
return false;
|
today.getFullYear(),
|
||||||
|
String(today.getMonth() + 1).padStart(2, '0'),
|
||||||
|
String(today.getDate()).padStart(2, '0')
|
||||||
|
].join('-');
|
||||||
|
$('#start_date_gregorian').val(ymd);
|
||||||
|
$('#start_date').val(jalaliFromGregorianYmd(ymd));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use GuzzleHttp\Client;
|
|||||||
use App\Models\Tasks;
|
use App\Models\Tasks;
|
||||||
use App\Models\Pivot_Tasks;
|
use App\Models\Pivot_Tasks;
|
||||||
use App\Http\Controllers\AttendanceController;
|
use App\Http\Controllers\AttendanceController;
|
||||||
|
use App\Http\Controllers\LeaveController;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -34,6 +35,8 @@ Route::post('/delete', [AdminController::class, 'delete'])->name('admin.delete')
|
|||||||
|
|
||||||
Route::post('/update', [AdminController::class, 'update'])->name('admin.update');
|
Route::post('/update', [AdminController::class, 'update'])->name('admin.update');
|
||||||
|
|
||||||
|
Route::post('/leave/decide', [LeaveController::class, 'decide'])->name('admin.leave.decide');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Route::get('/job', function () {
|
Route::get('/job', function () {
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ Artisan::command('inspire', function () {
|
|||||||
|
|
||||||
Schedule::command('birthdays:notify')->dailyAt('09:00');
|
Schedule::command('birthdays:notify')->dailyAt('09:00');
|
||||||
|
|
||||||
|
// یادآوری ثبت تعطیلات رسمی سالهای بعد (از پایان پوشش فعلی)
|
||||||
|
Schedule::command('holidays:remind')->dailyAt('09:30');
|
||||||
|
|
||||||
// گزارش تأخیر روز قبل — هر روز ساعت ۲۲:۳۰
|
// گزارش تأخیر روز قبل — هر روز ساعت ۲۲:۳۰
|
||||||
Schedule::command('late:daily-report')->dailyAt('22:30');
|
Schedule::command('late:daily-report')->dailyAt('22:30');
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use App\Models\Pivot_Tasks;
|
|||||||
use App\Http\Controllers\AttendanceController;
|
use App\Http\Controllers\AttendanceController;
|
||||||
use App\Http\Controllers\TaskController;
|
use App\Http\Controllers\TaskController;
|
||||||
use App\Http\Controllers\FingerprintController;
|
use App\Http\Controllers\FingerprintController;
|
||||||
|
use App\Http\Controllers\LeaveController;
|
||||||
|
|
||||||
|
|
||||||
Route::prefix('admin')->middleware('admin')->group(function () {
|
Route::prefix('admin')->middleware('admin')->group(function () {
|
||||||
@@ -58,6 +59,7 @@ Route::get('/attendance/profile', [AttendanceController::class, 'profile'])->nam
|
|||||||
|
|
||||||
Route::post('/attendance/profile', [AttendanceController::class, 'updateProfile'])->name('edit.profile');
|
Route::post('/attendance/profile', [AttendanceController::class, 'updateProfile'])->name('edit.profile');
|
||||||
|
|
||||||
|
Route::post('/leave', [LeaveController::class, 'store'])->name('leave.store');
|
||||||
|
|
||||||
//Task routes
|
//Task routes
|
||||||
Route::get('/tasks', [TaskController::class, 'Tasks']);
|
Route::get('/tasks', [TaskController::class, 'Tasks']);
|
||||||
|
|||||||
Reference in New Issue
Block a user