1569 lines
82 KiB
PHP
Executable File
1569 lines
82 KiB
PHP
Executable File
<?php
|
|
|
|
use App\Models\Attendance;
|
|
use App\Models\User;
|
|
use App\Models\ai;
|
|
use Morilog\Jalali\Jalalian;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Support\Collection;
|
|
|
|
|
|
if (!empty($user->tasks)) {
|
|
$in_progress = '#fca000';
|
|
$todo = '#7d7d7d';
|
|
$error = '#fc0000';
|
|
$backlog = '#bababa';
|
|
$done = '#40de02';
|
|
$cancelled = 'black';
|
|
|
|
$state = [
|
|
"c73c2044-ea14-42cb-b08d-cecf3d1fdfb5" => "$in_progress",// in progress
|
|
"a9ce312f-cfca-491e-8e3d-1975aa6ebc10" => "$todo",//to do
|
|
"dea1fb57-a281-4fe7-8f3e-ee5567637e35" => "$error",// error
|
|
"770fbe42-99f6-41db-9b9e-773d9cced1a5" => "$backlog",//backlog
|
|
"b0e464e1-34eb-41ea-b2f7-3ef7483bc9f0" => "$done",//done
|
|
"d61d14a3-72f7-4f44-894b-efe8b2a75be1" => "$cancelled",//canceled
|
|
];
|
|
$state_order = [
|
|
"c73c2044-ea14-42cb-b08d-cecf3d1fdfb5",
|
|
"a9ce312f-cfca-491e-8e3d-1975aa6ebc10",
|
|
"dea1fb57-a281-4fe7-8f3e-ee5567637e35",
|
|
"770fbe42-99f6-41db-9b9e-773d9cced1a5",
|
|
"b0e464e1-34eb-41ea-b2f7-3ef7483bc9f0",
|
|
"d61d14a3-72f7-4f44-894b-efe8b2a75be1",
|
|
];
|
|
|
|
$user->tasks = $user->tasks->sortBy(function ($task) use ($state_order) {
|
|
return array_search($task->state, $state_order);
|
|
});
|
|
}
|
|
// $duration for break timer is passed from AttendanceController
|
|
|
|
?>
|
|
@extends('app')
|
|
@section('content')
|
|
|
|
<div class="container flex-col"
|
|
style="letter-spacing: 0;display: flex;justify-content: center;align-items: center; margin : 90px 0 10px 0">
|
|
|
|
|
|
{{-- <button type="button"--}}
|
|
{{-- class="fixed max-w-[350px] bottom-24 right-9 z-50--}}
|
|
{{-- w-16 h-16 flex items-center justify-center--}}
|
|
{{-- text-white rounded-full--}}
|
|
{{-- bg-blue-500 hover:bg-blue-600--}}
|
|
{{-- shadow-lg cursor-pointer transition-all duration-300"--}}
|
|
{{-- onclick="openModal('ثبت روز کاری جدید', 'new')">--}}
|
|
{{-- ثبت روز جدید--}}
|
|
{{-- </button>--}}
|
|
@if (!isset($_COOKIE['token-0']))
|
|
|
|
<form id="attendanceForm" class="form items-center" action="{{ url('attendance/enter') }}" method="post"
|
|
style="text-align: left">
|
|
@csrf
|
|
<p class="title text-center font-bold text-lg mb-2">سامانه گذارش کار</p>
|
|
<p class="message text-right text-sm text-gray-700">{{ $s->response }}</p>
|
|
|
|
@php
|
|
$user_identity = $_COOKIE['user_identity'] ?? '';
|
|
@endphp
|
|
|
|
<input type="hidden" name="latitude" id="latitude">
|
|
<input type="hidden" name="longitude" id="longitude">
|
|
<input type="hidden" name="distance" id="distance">
|
|
|
|
<p id="distanceMessage" class="text-center mb-2 font-semibold text-gray-600">در حال بررسی موقعیت...</p>
|
|
|
|
<button id="submitBtn" disabled type="button"
|
|
class="bg-gray-400 text-white rounded cursor-not-allowed rounded-xl py-2 px-4 max-w-60 self-center"
|
|
style="width: 100%">
|
|
ورود/خروج
|
|
</button>
|
|
|
|
<div id="output" class="text-red-500 text-sm mt-2 text-center"></div>
|
|
|
|
<div class="flex justify-flex max-w-[240px] flex-wrap gap-2" style="align-items: center;">
|
|
<button type="button"
|
|
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 type="button" id="breakAttendance"
|
|
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"
|
|
href="">
|
|
استراحت
|
|
</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>
|
|
<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"
|
|
onclick="openTaskModal()">
|
|
وظایف
|
|
</button>
|
|
{{--<p>{{Carbon::now()}}</p>--}}
|
|
</form>
|
|
{{-- @if(session('open_task_modal'))--}}
|
|
{{-- <script>--}}
|
|
{{-- window.onload = () => {--}}
|
|
{{-- openTaskModal();--}}
|
|
{{-- };--}}
|
|
{{-- </script>--}}
|
|
{{-- @endif--}}
|
|
|
|
@else
|
|
|
|
<div class="ui-panel flex p-4 rounded-xl max-w-[350px] flex-col items-center">
|
|
|
|
|
|
<label class="text-xl font-bold pb-2">
|
|
{{$user->name}}
|
|
</label>
|
|
|
|
<div class="flex w-full max-w-[240px] flex-wrap gap-2 justify-center">
|
|
<button type="button"
|
|
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 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>
|
|
@endif
|
|
|
|
|
|
|
|
<div class="w-full flex justify-center mt-4 pb-20" style="">
|
|
<div class="overflow-visible rounded-[23px] max-w-[350px] relative z-20">
|
|
<div class="text-center w-full">
|
|
|
|
|
|
<!-- Your Table Code Here -->
|
|
<div class="w-[350px]">
|
|
|
|
@if(isset($_COOKIE['user_identity']))
|
|
{{-- قسمت پردازش دادهها: گروهبندی لیست حضور و غیاب بر اساس ماه+سال شمسی --}}
|
|
@php
|
|
$attendances = $user->attendances ?? collect();
|
|
$currentJalaliYear = \Morilog\Jalali\Jalalian::now()->getYear();
|
|
|
|
$groupedData = $attendances
|
|
->filter(fn ($item) => !empty($item->entered_at))
|
|
->groupBy(function ($item) {
|
|
$j = \App\Support\JalaliHelper::fromDateTime($item->entered_at);
|
|
if (!$j) {
|
|
return 'invalid';
|
|
}
|
|
|
|
return sprintf('%04d-%02d', $j->getYear(), $j->getMonth());
|
|
})
|
|
->reject(fn ($_, $key) => $key === 'invalid')
|
|
->sortKeysDesc();
|
|
|
|
$monthLabels = [];
|
|
foreach ($groupedData->keys() as $monthKey) {
|
|
[$y, $m] = array_map('intval', explode('-', $monthKey));
|
|
try {
|
|
$j = new \Morilog\Jalali\Jalalian($y, $m, 1);
|
|
$name = $j->format('%B');
|
|
$monthLabels[$monthKey] = ((int) $y === (int) $currentJalaliYear)
|
|
? $name
|
|
: $name . ' ' . $y;
|
|
} catch (\Throwable $e) {
|
|
$monthLabels[$monthKey] = $monthKey;
|
|
}
|
|
}
|
|
|
|
$recentMonthKeys = $groupedData->keys()->take(3);
|
|
$olderMonthKeys = $groupedData->keys()->slice(3);
|
|
|
|
$attendanceDates = $attendances
|
|
->map(fn ($a) => $a->entered_at ? \Carbon\Carbon::parse($a->entered_at)->toDateString() : null)
|
|
->filter()
|
|
->unique()
|
|
->values();
|
|
|
|
$holidayDates = collect();
|
|
try {
|
|
$holidayDates = $attendanceDates->isEmpty()
|
|
? collect()
|
|
: \App\Models\Holiday::query()
|
|
->whereIn('date', $attendanceDates)
|
|
->pluck('date')
|
|
->map(fn ($d) => \Carbon\Carbon::parse($d)->toDateString())
|
|
->flip();
|
|
} catch (\Throwable $e) {
|
|
$holidayDates = collect();
|
|
}
|
|
@endphp
|
|
|
|
<div class="container inline-flex flex-col items-center justify-center">
|
|
@if($groupedData->isEmpty())
|
|
<div class="mt-80 text-gray-500 text-xl font-bold italic">
|
|
<h1>اطلاعاتی موجود نمیباشد</h1>
|
|
</div>
|
|
@else
|
|
{{-- سه ماه اخیر + منوی بقیه --}}
|
|
<div class="ui-month-tabs">
|
|
@foreach($recentMonthKeys as $monthKey)
|
|
<button type="button" class="tablinks"
|
|
data-month-id="m-{{ $monthKey }}"
|
|
onclick="openCity(event, 'm-{{ $monthKey }}')">
|
|
{{ $monthLabels[$monthKey] ?? $monthKey }}
|
|
</button>
|
|
@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>
|
|
|
|
@foreach($groupedData as $monthKey => $records)
|
|
<div id="m-{{ $monthKey }}" class="tabcontent ui-attendance-card">
|
|
<table class="ui-attendance-table">
|
|
<thead>
|
|
<tr>
|
|
@if($_COOKIE['user_identity'] == 1370)
|
|
<th>نام</th>
|
|
@endif
|
|
<th>تاریخ</th>
|
|
<th>ورود</th>
|
|
<th>خروج</th>
|
|
<th>مدت</th>
|
|
<th>تأخیر</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
@php
|
|
$monthLateMinutes = 0;
|
|
$monthDailyFine = 0;
|
|
$monthKeyCarbon = null;
|
|
$firstEntered = $records->first()?->entered_at;
|
|
if ($firstEntered && \App\Support\JalaliHelper::fromDateTime($firstEntered)) {
|
|
$monthKeyCarbon = \Carbon\Carbon::parse($firstEntered);
|
|
[$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKeyCarbon);
|
|
$monthWeeklyFine = (int) \App\Models\LatePeriodFine::query()
|
|
->where('user_identity', $user->user_name)
|
|
->where('period_type', 'weekly')
|
|
->whereBetween('period_start', [$mStart->toDateString(), $mEnd->toDateString()])
|
|
->sum('fine_amount');
|
|
} else {
|
|
$monthWeeklyFine = 0;
|
|
}
|
|
@endphp
|
|
@foreach($records as $item)
|
|
@php
|
|
$monthLateMinutes += (int) ($item->late_minutes ?? 0);
|
|
$monthDailyFine += (int) ($item->daily_fine_amount ?? 0);
|
|
$enteredCarbon = $item->entered_at ? \Carbon\Carbon::parse($item->entered_at) : null;
|
|
$isNonWorkDay = $enteredCarbon && (
|
|
$enteredCarbon->isFriday()
|
|
|| isset($holidayDates[$enteredCarbon->toDateString()])
|
|
);
|
|
@endphp
|
|
<tr class="hover:bg-cyan-500 cursor-pointer transition duration-150"
|
|
style="border-bottom: 1px solid #ddd; background-color: {{$item->requested ? '#bfdbfe' : '#ffffff' }}"
|
|
onclick="openModal('{{ $item->id }}', '{{ $item->id }}' , '{{ $item->entered_at }}', '{{ $item->exited_at }} ')">
|
|
|
|
{{-- شرط نمایش نام --}}
|
|
@if($_COOKIE['user_identity'] == 1370)
|
|
<td style="border-right: 1px solid #ddd; padding: 8px;">
|
|
{{$item->user_identity}}
|
|
</td>
|
|
@endif
|
|
|
|
{{-- تاریخ --}}
|
|
<td style="border-right: 1px solid #ddd; padding: 4px; background-color: {{"#e6e6e6" }};">
|
|
{{ \App\Support\JalaliHelper::format($item->entered_at, 'Y/m/d') }}
|
|
<span class="text-xs text-gray-500 block">
|
|
{{ \App\Support\JalaliHelper::format($item->entered_at, '%A', '') }}
|
|
@if($isNonWorkDay)
|
|
· اضافهکاری
|
|
@endif
|
|
</span>
|
|
</td>
|
|
|
|
{{-- زمان ورود --}}
|
|
<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') : '-' }}
|
|
</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 < 100 ? '#d1fae5' : '#edae80')) : '' }};">
|
|
{{ $item->exited_at ? \Carbon\Carbon::parse($item->exited_at)->format('H:i') : '-' }}
|
|
</td>
|
|
|
|
{{-- مدت زمان --}}
|
|
<td style="border-right: 1px solid #ddd; padding: 4px;">
|
|
{{ $item->entered_at && $item->exited_at && $item->duration ? \Illuminate\Support\Str::of($item->duration)->beforeLast(':')->toString() : '-' }}
|
|
</td>
|
|
|
|
{{-- تأخیر --}}
|
|
<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($isNonWorkDay)
|
|
<span class="text-[10px] text-emerald-600">اضافهکاری</span>
|
|
@elseif(($item->late_minutes ?? 0) > 0)
|
|
{{ $item->late_minutes }}د
|
|
@if(($item->daily_fine_amount ?? 0) > 0)
|
|
<span class="text-[10px] block">{{ number_format($item->daily_fine_amount) }}ت</span>
|
|
@endif
|
|
@else
|
|
-
|
|
@endif
|
|
</td>
|
|
</tr>
|
|
@endforeach
|
|
|
|
@php
|
|
$monthTotalFine = $monthDailyFine + $monthWeeklyFine;
|
|
@endphp
|
|
<tr>
|
|
<td colspan="6" class="table-summary text-right leading-6">
|
|
تعداد رکورد: {{ count($records) }}
|
|
<br>
|
|
تأخیر ماه: {{ $monthLateMinutes }} دقیقه
|
|
<br>
|
|
جریمه روزانه: {{ number_format($monthDailyFine) }} تومان
|
|
<br>
|
|
جریمه هفتگی: {{ number_format($monthWeeklyFine) }} تومان
|
|
<br>
|
|
<strong>جمع جریمه ماه: {{ number_format($monthTotalFine) }} تومان</strong>
|
|
</td>
|
|
</tr>
|
|
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
@endforeach
|
|
@endif
|
|
</div>
|
|
|
|
|
|
@endif
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- ATTENDANCE Modal -->
|
|
<div id="modal-container"
|
|
class="ui-modal-backdrop centered hidden">
|
|
<div id="modal-content" class="ui-modal-panel max-w-sm w-full">
|
|
<h2 id="modal-title" class="ui-title"></h2>
|
|
<p id="modal-body" class="ui-subtitle"></p>
|
|
<div class="flex flex-col gap-4" style="align-items: center">
|
|
|
|
<div class="flex justify-center gap-2">
|
|
<div id="panel-access-link" href="#" class="" title="Panel Access"></div>
|
|
</div>
|
|
|
|
<form id="edit-form" method="post" action="{{$formaction.'edit'}}"
|
|
class="inline-block flex-col gap-2 flex items-center">
|
|
@csrf
|
|
|
|
<p id="new-date-label" style="display: none; text-align: right; ">تاریخ:</p>
|
|
<div id="new-date-group" class="input-group" style="margin: 20px; display: none;">
|
|
<input type="text" name="display_date" id="display_date"
|
|
class=" example1 form-control border p-2 rounded date-picker-input" value=""
|
|
placeholder="تاریخ"
|
|
readonly
|
|
>
|
|
|
|
</div>
|
|
|
|
<p style=" ">ساعت شروع:</p>
|
|
<div class="input-group clockpicker border-2 border-gray-300 rounded-lg"
|
|
style="margin: 20px;">
|
|
<span class="input-group-addon bg-gray-200 align-middle text-stone-600 rounded-r-md p-2 pt-2.5">
|
|
<i class="fa-solid fa-clock"></i>
|
|
</span>
|
|
<input type="text" name="starttime" id="starttime"
|
|
class="form-control cursor-default p-2 w-36 "
|
|
placeholder="09:00"
|
|
value=""
|
|
readonly
|
|
>
|
|
<button type="button" onclick="clearTime('starttime')"
|
|
class="p-2 bg-gray-200 hover:bg-gray-300 text-red-600 rounded-l-md w-9
|
|
title=" خالی کردن ساعت
|
|
">
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<p>ساعت پایان:</p>
|
|
<div class="input-group clockpicker border-2 border-gray-300 rounded-lg"
|
|
style="margin: 20px;">
|
|
<span class="input-group-addon bg-gray-200 align-middle text-stone-600 rounded-r-md p-2 pt-2.5">
|
|
<i class="fa-solid fa-clock"></i>
|
|
</span>
|
|
<input type="text" name="endtime" id="endtime"
|
|
class="form-control cursor-default p-2 w-36"
|
|
placeholder="17:30"
|
|
value=""
|
|
readonly
|
|
>
|
|
|
|
|
|
<button type="button" onclick="clearTime('endtime')"
|
|
class="p-2 bg-gray-200 hover:bg-gray-300 text-red-600 rounded-l-md w-9
|
|
title=" خالی کردن ساعت
|
|
">
|
|
×
|
|
</button>
|
|
</div>
|
|
|
|
<input type="hidden" name="uname" id="uname" value="{{$user->user_name ?? null}}">
|
|
<input type="hidden" name="date" id="date" value="">
|
|
<input type="hidden" name="id" id="edit_id" value="">
|
|
|
|
<button type="submit" id="submit-edit-button" style="display: none;"
|
|
onclick="return confirmSubmit()">
|
|
ثبت تغییرات
|
|
</button>
|
|
</form>
|
|
|
|
<form id="delete-form" action="{{$formaction.'delete'}}" method="post" style="display: none;">
|
|
@csrf
|
|
<input type="hidden" name="id" id="delete_id" value="">
|
|
<button type="submit" id="submit-delete-button" onclick="return confirmSubmit()">
|
|
حذف
|
|
</button>
|
|
</form>
|
|
|
|
|
|
<div class="flex justify-center gap-2 mt-2 px-4">
|
|
|
|
<button type="button" class="bg-blue-500 text-white rounded p-2 **w-[70%]**"
|
|
onclick="document.getElementById('edit-form').submit(); return confirmSubmit()">
|
|
@if(isset($_COOKIE['ad']) && $_COOKIE['ad'] == 1)
|
|
ثبت تغییرات
|
|
@else
|
|
ارسال برای تایید
|
|
@endif
|
|
</button>
|
|
|
|
<button type="button" class="bg-red-500 text-white rounded p-2 **w-[30%]**"
|
|
onclick="document.getElementById('delete-form').submit(); return confirmSubmit()">
|
|
<i class="fas fa-trash text-white text-lg"></i>
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</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 --}}
|
|
<div id="taskModal"
|
|
class="ui-modal-backdrop hidden"
|
|
onclick="closeOnOutsideClick(event)">
|
|
<div class="ui-modal-panel">
|
|
<button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
|
|
<i class="fa-solid fa-xmark"></i>
|
|
</button>
|
|
|
|
<h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
|
|
<p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
|
|
<input class="ui-input mb-3"
|
|
placeholder="جست و جو..." type="text" value=""
|
|
id="taskSearchInput">
|
|
|
|
{{-- <div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">--}}
|
|
@if(isset($user->tasks) && count($user->tasks) >= 1)
|
|
|
|
<form action="{{url("active_task")}}" method="POST" class=" ">@csrf
|
|
{{-- <input type="hidden" name="pid" value="{{ $user->tasks->pivot->id }}" class="">--}}
|
|
<input type="hidden" name="uid" value="{{ $user->id }}" class="">
|
|
<div class="ui-task-list">
|
|
@foreach($user->tasks as $item)
|
|
<label class="ui-task-item">
|
|
@if($user->active_task)
|
|
@if( $user->active_task == $item->title )
|
|
<input type="radio" name="task_id" checked="true"
|
|
value="{{ $item->id }}"
|
|
class="">
|
|
|
|
@else
|
|
<input type="radio" name="task_id" value="{{ $item->id }}" class=""
|
|
disabled>
|
|
|
|
@endif
|
|
@else
|
|
<input type="radio" name="task_id" value="{{ $item->id }}" class="">
|
|
|
|
{{-- <input type="hidden" name="task_name" value="{{$item->name}}">--}}
|
|
@endif
|
|
|
|
<div class="px-2.5" style="direction: rtl">{{ $item->title }}</div>
|
|
<span class="block h-1.5 w-1.5 flex-shrink-0 rounded-full" style="background-color:
|
|
{!! array_key_exists($item->state,$state) ? $state[$item->state] : "" !!};"></span>
|
|
</label>
|
|
|
|
@endforeach
|
|
</div>
|
|
@if( $user->active_task )
|
|
{{-- <div class="absolute bottom-2 left-0 w-full flex justify-center">--}}
|
|
<button class="submit mt-2">
|
|
پایان
|
|
</button>
|
|
{{-- </div>--}}
|
|
@else
|
|
{{-- <div class="absolute bottom-2 left-0 w-full flex justify-center">--}}
|
|
<button class="submit mt-2">
|
|
شروع
|
|
</button>
|
|
{{-- </div>--}}
|
|
@endif
|
|
|
|
</form>
|
|
@else
|
|
<div class="flex justify-center items-center py-10">
|
|
<p class="font-bold text-gray-400">
|
|
وظیفه ای وجود ندارد
|
|
</p>
|
|
</div>
|
|
@endif
|
|
{{-- افزودن تسک--}}
|
|
<form class="mt-3 flex items-center justify-center gap-2 flex-wrap">
|
|
<input class="ui-input !w-[200px]"
|
|
placeholder="عنوان وظیفه" type="text" value=""
|
|
id="taskTitle">
|
|
<button id="createTask"
|
|
class="mt-4 inline-block bg-green-500 hover:bg-green-700 text-white font-bold py-2 px-2.5 fa-notdog fa-solid fa-plus rounded-full">
|
|
</button>
|
|
|
|
</form>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
@endsection
|
|
|
|
@section('script')
|
|
<script>
|
|
|
|
const existed = {{ $existed ? 'true' : 'false' }};
|
|
{{--const today = {{$today}};--}}
|
|
const distanceMessage = document.getElementById("distanceMessage");
|
|
|
|
|
|
// مختصات دفتر / شرکت
|
|
const referenceLat = 32.6504751;
|
|
const referenceLng = 51.6936045;
|
|
// شعاع مجاز (متر) — GPS داخل ساختمان معمولاً ۲۰–۸۰ متر خطا دارد
|
|
const OFFICE_RADIUS_METERS = 100;
|
|
const MAX_ACCURACY_CREDIT_METERS = 80;
|
|
|
|
const submitBtn = document.getElementById("submitBtn");
|
|
const latInput = document.getElementById("latitude");
|
|
const lngInput = document.getElementById("longitude");
|
|
const distanceInput = document.getElementById("distance");
|
|
const form = document.getElementById("attendanceForm");
|
|
|
|
if (submitBtn && latInput && lngInput && distanceInput && distanceMessage && form) {
|
|
// naser
|
|
if (existed) {
|
|
submitBtn.textContent = "خروج";
|
|
} else {
|
|
submitBtn.textContent = "ورود";
|
|
}
|
|
|
|
|
|
function getDistanceInMeters(lat1, lon1, lat2, lon2) {
|
|
const earthRadius = 6371000; // meters
|
|
const toRad = deg => deg * Math.PI / 180;
|
|
const dLat = toRad(lat2 - lat1);
|
|
const dLon = toRad(lon2 - lon1);
|
|
lat1 = toRad(lat1);
|
|
lat2 = toRad(lat2);
|
|
const a = Math.sin(dLat / 2) ** 2 +
|
|
Math.cos(lat1) * Math.cos(lat2) * Math.sin(dLon / 2) ** 2;
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
return earthRadius * c;
|
|
}
|
|
|
|
function isInsideOffice(distanceMeters, accuracyMeters) {
|
|
const accuracyCredit = Math.min(Math.max(0, accuracyMeters || 0), MAX_ACCURACY_CREDIT_METERS);
|
|
const effectiveDistance = Math.max(0, distanceMeters - accuracyCredit);
|
|
return effectiveDistance <= OFFICE_RADIUS_METERS;
|
|
}
|
|
|
|
function applyLocationResult(lat, lng, accuracyMeters) {
|
|
latInput.value = lat;
|
|
lngInput.value = lng;
|
|
|
|
const dist = Math.round(getDistanceInMeters(referenceLat, referenceLng, lat, lng));
|
|
distanceInput.value = dist;
|
|
const inside = isInsideOffice(dist, accuracyMeters);
|
|
const accuracyText = accuracyMeters
|
|
? ` · دقت ±${Math.round(accuracyMeters)}م`
|
|
: '';
|
|
|
|
distanceMessage.textContent = inside
|
|
? `مکان: شرکت (~${dist}م)${accuracyText}`
|
|
: `مکان: خارج از شرکت (~${dist}م)${accuracyText}`;
|
|
|
|
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 (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");
|
|
}
|
|
|
|
submitBtn.disabled = false;
|
|
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
|
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
|
}
|
|
|
|
function locateUser() {
|
|
if (!navigator.geolocation) {
|
|
distanceMessage.textContent = 'موقعیتیاب در دسترس نیست';
|
|
distanceMessage.classList.add("text-orange-500");
|
|
submitBtn.disabled = false;
|
|
submitBtn.classList.remove("bg-gray-400", "cursor-not-allowed");
|
|
submitBtn.classList.add("bg-cyan-500", "hover:bg-cyan-700", "cursor-pointer");
|
|
return;
|
|
}
|
|
|
|
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 output = document.getElementById('output');
|
|
const challenge = new Uint8Array(32);
|
|
window.crypto.getRandomValues(challenge);
|
|
const createOptions = {
|
|
challenge,
|
|
rp: {
|
|
name: "مثال سایت شما"
|
|
},
|
|
user: {
|
|
id: Uint8Array.from('user-id', c => c.charCodeAt(0)),
|
|
name: "user@example.com",
|
|
displayName: "نام کاربر"
|
|
},
|
|
pubKeyCredParams: [{
|
|
type: 'public-key',
|
|
alg: -7
|
|
}],
|
|
authenticatorSelection: {
|
|
authenticatorAttachment: "platform",
|
|
userVerification: "required"
|
|
},
|
|
timeout: 60000,
|
|
attestation: "direct"
|
|
};
|
|
submitBtn.addEventListener('click', async () => {
|
|
// ابتدا اعتبارسنجی اثر انگشت توسط WebAuthn
|
|
if (!window.PublicKeyCredential) {
|
|
alert('مرورگر شما از احراز هویت بیومتریک پشتیبانی نمیکند.');
|
|
return;
|
|
}
|
|
try {
|
|
const credential = await navigator.credentials.create({
|
|
publicKey: createOptions
|
|
});
|
|
attendanceForm.submit();
|
|
} catch (error) {
|
|
if (output) {
|
|
output.textContent = error.name === 'NotAllowedError' ? 'احراز هویت لغو شد یا دسترسی مجاز نبود.' : 'عملیات ناموفق بود. لطفاً دوباره امتحان کنید.';
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
function openCity(evt, Month) {
|
|
var i, tabcontent, tablinks;
|
|
tabcontent = document.getElementsByClassName("tabcontent");
|
|
for (i = 0; i < tabcontent.length; i++) {
|
|
tabcontent[i].style.display = "none";
|
|
}
|
|
|
|
tablinks = document.getElementsByClassName("tablinks");
|
|
for (i = 0; i < tablinks.length; i++) {
|
|
tablinks[i].className = tablinks[i].className.replace(" active bg-gray-300 font-bold", "");
|
|
if (!tablinks[i].className.includes("active")) {
|
|
tablinks[i].style.backgroundColor = "";
|
|
}
|
|
}
|
|
|
|
const panel = document.getElementById(Month);
|
|
if (panel) {
|
|
panel.style.display = "block";
|
|
}
|
|
if (evt && evt.currentTarget) {
|
|
evt.currentTarget.className += " active bg-gray-300 font-bold";
|
|
}
|
|
}
|
|
|
|
function toggleMonthMore(evt) {
|
|
if (evt) {
|
|
evt.stopPropagation();
|
|
}
|
|
const menu = document.getElementById('month-more-menu');
|
|
if (menu) {
|
|
menu.classList.toggle('hidden');
|
|
}
|
|
}
|
|
|
|
function closeMonthMore() {
|
|
const menu = document.getElementById('month-more-menu');
|
|
if (menu) {
|
|
menu.classList.add('hidden');
|
|
}
|
|
}
|
|
|
|
document.getElementById('month-more-menu')?.addEventListener('click', function (e) {
|
|
e.stopPropagation();
|
|
});
|
|
|
|
document.addEventListener('click', function () {
|
|
closeMonthMore();
|
|
});
|
|
|
|
// باز کردن اولین تب به صورت خودکار هنگام لود صفحه
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
let firstTab = document.querySelector(".ui-month-tabs > .tablinks");
|
|
if (firstTab) {
|
|
firstTab.click();
|
|
}
|
|
});
|
|
|
|
{{-- TASKS MODAL--}}
|
|
function openTaskModal(task) {
|
|
const modal = document.getElementById('taskModal');
|
|
if (!modal) {
|
|
return;
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
|
|
if (task) {
|
|
// آپدیت متن دکمه بر اساس وضعیت تسک
|
|
updateStatusButtonText(task.started_at, task.ended_at);
|
|
|
|
// تنظیم عملکرد دکمه بر اساس تسک فعلی
|
|
const statusBtn = document.getElementById('statusBtn');
|
|
if (statusBtn) {
|
|
statusBtn.onclick = () => handleTaskAction(
|
|
task.id,
|
|
task.p_id,
|
|
task.started_at,
|
|
task.ended_at
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
// -------------------------------
|
|
// بستن مودال
|
|
// -------------------------------
|
|
function closeTaskModal() {
|
|
const modal = document.getElementById('taskModal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
function closeOnOutsideClick(event) {
|
|
// وقتی کاربر بیرون از محتوای مودال کلیک کنه، ببندش
|
|
if (event.target.id === 'taskModal') {
|
|
closeTaskModal();
|
|
}
|
|
}
|
|
|
|
// -------------------------------
|
|
// آپدیت متن دکمه شروع/پایان
|
|
// -------------------------------
|
|
function updateStatusButtonText(started_at, ended_at) {
|
|
const statusBtn = document.getElementById('statusBtn');
|
|
if (!statusBtn) {
|
|
return;
|
|
}
|
|
|
|
let text = "شروع";
|
|
|
|
if (started_at && !ended_at) {
|
|
text = "پایان";
|
|
}
|
|
|
|
statusBtn.textContent = text;
|
|
}
|
|
|
|
// -------------------------------
|
|
// هندل عملیات شروع / پایان تسک
|
|
// -------------------------------
|
|
function handleTaskAction(taskId, p_id, started_at, ended_at) {
|
|
// ... (منطق تعیین actionType و زمانها) ...
|
|
|
|
// ارسال داده به سرور
|
|
fetch('{{ url("/task-manager") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
task_id: taskId,
|
|
p_id: p_id,
|
|
started_at: started_at,
|
|
ended_at: ended_at,
|
|
action_type: actionType
|
|
})
|
|
})
|
|
.then(res => {
|
|
// ابتدا بررسی وضعیت HTTP
|
|
if (!res.ok) {
|
|
// اگر کد وضعیت 4xx یا 5xx باشد
|
|
throw new Error(`HTTP error! status: ${res.status}`);
|
|
}
|
|
// در صورت موفقیت (200)، تلاش برای خواندن JSON
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
// 💡 اصلاح: بررسی دقیق فیلد success
|
|
if (data && data.success === true) {
|
|
console.log('✅ Task updated successfully:', data);
|
|
closeTaskModal();
|
|
// 💡 رفع مشکل عدم بهروزرسانی وضعیت دکمه در بار بعدی:
|
|
// برای سادگی، صفحه را رفرش کنید تا دادههای جدید واکشی شوند.
|
|
window.location.reload();
|
|
} else {
|
|
// سرور با کد 200 جواب داده اما success: false بوده یا ساختار پاسخ مشکل دارد.
|
|
console.error('❌ Operation failed based on server data:', data);
|
|
alert('❌ خطا در انجام عملیات! پاسخ سرور را در کنسول بررسی کنید.');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
// شامل خطای ارتباط، خطای HTTP و خطای JSON نامعتبر
|
|
console.error('⚠️ Server or JSON parsing error:', err);
|
|
alert(`⚠️ خطا در ارتباط با سرور یا پردازش داده!`);
|
|
});
|
|
}
|
|
|
|
//SEARCH BAR IN TASK MODAL
|
|
const searchInput = document.getElementById("taskSearchInput");
|
|
const listItems = document.querySelectorAll('#taskModal .ui-task-item');
|
|
|
|
if (searchInput && listItems.length) {
|
|
searchInput.addEventListener("input", function () {
|
|
const filter = searchInput.value.toLowerCase();
|
|
listItems.forEach(item => {
|
|
const nameDiv = item.querySelector('div');
|
|
if (nameDiv) {
|
|
const name = nameDiv.textContent.toLowerCase();
|
|
item.style.display = name.includes(filter) ? 'flex' : 'none';
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
const taskTitle = document.getElementById('taskTitle');
|
|
|
|
function createTask(taskTitle) {
|
|
// const createTask = document.getElementById('createTask');
|
|
|
|
|
|
if (createTask) {
|
|
createTask.addEventListener('click', () => {
|
|
createTask.disabled = true;
|
|
|
|
fetch("{{ url('task/create') }}", {
|
|
method: "POST",
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
},
|
|
body: JSON.stringify({taskTitle})
|
|
})
|
|
.then(res => {
|
|
if (res.ok) {
|
|
window.location.reload();
|
|
} else {
|
|
createTask.textContent = 'خطا';
|
|
createTask.disabled = false;
|
|
console.error('Server error on create task action:', res.statusText);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
createTask.textContent = 'خطای شبکه';
|
|
createTask.disabled = false;
|
|
console.error('Network error:', err);
|
|
});
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{{--BREAK--}}
|
|
|
|
const breakBtn = document.getElementById('breakAttendance');
|
|
let breakTimer = null;
|
|
let currentSeconds = 0;
|
|
|
|
// server-provided initial duration (string or null)
|
|
const serverDuration = {!! json_encode($duration ?? null) !!};
|
|
|
|
function parseDurationString(str) {
|
|
if (!str) return 0;
|
|
const parts = String(str).split(':').map(s => parseInt(s, 10) || 0);
|
|
if (parts.length === 3) {
|
|
return parts[0] * 3600 + parts[1] * 60 + parts[2];
|
|
} else if (parts.length === 2) {
|
|
// treat as MM:SS
|
|
return parts[0] * 60 + parts[1];
|
|
} else if (parts.length === 1) {
|
|
return parts[0];
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function formatDuration(seconds) {
|
|
const h = Math.floor(seconds / 3600);
|
|
const m = Math.floor((seconds % 3600) / 60);
|
|
const s = seconds % 60;
|
|
const mm = String(m).padStart(2, '0');
|
|
const ss = String(s).padStart(2, '0');
|
|
if (h > 0) {
|
|
return `${h}:${mm}:${ss}`;
|
|
}
|
|
return `${mm}:${ss}`;
|
|
}
|
|
|
|
function startBreakCounter(initialSeconds) {
|
|
if (!breakBtn) {
|
|
return;
|
|
}
|
|
|
|
// ensure no duplicate timer
|
|
if (breakTimer) clearInterval(breakTimer);
|
|
currentSeconds = initialSeconds || 0;
|
|
// update immediately
|
|
breakBtn.textContent = formatDuration(currentSeconds);
|
|
breakBtn.classList.add('bg-red-500');
|
|
breakBtn.classList.remove('bg-gray-500');
|
|
breakTimer = setInterval(() => {
|
|
currentSeconds++;
|
|
breakBtn.textContent = formatDuration(currentSeconds);
|
|
}, 1000);
|
|
}
|
|
|
|
function stopBreakCounter() {
|
|
if (breakTimer) {
|
|
clearInterval(breakTimer);
|
|
breakTimer = null;
|
|
}
|
|
currentSeconds = 0;
|
|
if (breakBtn) {
|
|
breakBtn.textContent = 'استراحت';
|
|
breakBtn.classList.remove('bg-red-500');
|
|
breakBtn.classList.add('bg-gray-500');
|
|
}
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
if (!breakBtn) return;
|
|
|
|
if (serverDuration !== null && serverDuration !== '') {
|
|
const secs = parseDurationString(serverDuration);
|
|
startBreakCounter(secs);
|
|
console.log(`استراحت فعال است. زمان شروع از سرور: ${serverDuration} (${secs} ثانیه).`);
|
|
} else {
|
|
stopBreakCounter();
|
|
}
|
|
});
|
|
|
|
if (breakBtn) {
|
|
breakBtn.addEventListener('click', () => {
|
|
// breakBtn.textContent = '...در حال ارسال';
|
|
// breakBtn.disabled = true;
|
|
|
|
fetch("{{ url('attendance/break') }}", {
|
|
method: "POST",
|
|
headers: {
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
|
'Accept': 'application/json'
|
|
}
|
|
})
|
|
.then(res => {
|
|
if (res.status == 'error') {
|
|
console.log(res);
|
|
window.location.reload();
|
|
} else {
|
|
console.log(res);
|
|
breakBtn.textContent = 'خطا';
|
|
breakBtn.disabled = false;
|
|
console.error('Server error on break action:', res.statusText);
|
|
window.location.reload();
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.log(err);
|
|
window.location.reload();
|
|
|
|
// breakBtn.textContent = 'خطای شبکه';
|
|
// breakBtn.disabled = false;
|
|
// console.error('Network error:', err);
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
{{--let breakStarted = false;--}}
|
|
{{--let breakStartTime = null;--}}
|
|
{{--let breakTimer = null;--}}
|
|
|
|
{{--const breakBtn = document.getElementById('breakAttendance');--}}
|
|
|
|
{{--function formatLocalDatetime(date) {--}}
|
|
{{-- const year = date.getFullYear();--}}
|
|
{{-- const month = String(date.getMonth() + 1).padStart(2, '0');--}}
|
|
{{-- const day = String(date.getDate()).padStart(2, '0');--}}
|
|
{{-- const hours = String(date.getHours()).padStart(2, '0');--}}
|
|
{{-- const minutes = String(date.getMinutes()).padStart(2, '0');--}}
|
|
{{-- const seconds = String(date.getSeconds()).padStart(2, '0');--}}
|
|
|
|
{{-- return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;--}}
|
|
{{--}--}}
|
|
|
|
{{--function formatDuration(totalSeconds) {--}}
|
|
{{-- const hours = Math.floor(totalSeconds / 3600);--}}
|
|
{{-- const minutes = Math.floor((totalSeconds % 3600) / 60);--}}
|
|
|
|
{{-- const h = String(hours);--}}
|
|
{{-- const i = String(minutes).padStart(2, '0');--}}
|
|
|
|
{{-- return `${h}:${i}`;--}}
|
|
{{--}--}}
|
|
|
|
|
|
{{--breakBtn.addEventListener('click', () => {--}}
|
|
{{-- if (!breakStarted) {--}}
|
|
{{-- // شروع استراحت 🚀--}}
|
|
{{-- breakStarted = true;--}}
|
|
{{-- breakStartTime = new Date();--}}
|
|
|
|
{{-- // تغییر ظاهر دکمه و شروع تایمر (همچنان M:S نمایش داده می شود)--}}
|
|
{{-- breakBtn.classList.remove('bg-gray-500', 'hover:bg-gray-600');--}}
|
|
{{-- breakBtn.classList.add('bg-red-500', 'hover:bg-red-600');--}}
|
|
{{-- breakBtn.textContent = '00:00';--}}
|
|
|
|
{{-- let seconds = 0;--}}
|
|
{{-- breakTimer = setInterval(() => {--}}
|
|
{{-- seconds++;--}}
|
|
{{-- // اینجا همچنان به صورت دقیقه:ثانیه نمایش داده می شود--}}
|
|
{{-- const m = String(Math.floor(seconds / 60)).padStart(2, '0');--}}
|
|
{{-- const s = String(seconds % 60).padStart(2, '0');--}}
|
|
{{-- breakBtn.textContent = ` ${m}:${s}`;--}}
|
|
{{-- }, 1000);--}}
|
|
{{-- //send start time--}}
|
|
{{-- let formData = new FormData();--}}
|
|
{{-- formData.append('start_time', formatLocalDatetime(breakStartTime));--}}
|
|
|
|
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
|
|
|
|
{{-- fetch("{{ url('attendance/break') }}", {--}}
|
|
{{-- method: "POST",--}}
|
|
{{-- body: formData--}}
|
|
{{-- })--}}
|
|
{{-- .then(res => res.json())--}}
|
|
{{-- .then(data => console.log('✅ Break Started Logged:', data))--}}
|
|
{{-- .catch(err => console.error('💥 Error starting break:', err));--}}
|
|
|
|
{{-- } else {--}}
|
|
{{-- // پایان استراحت 🛑--}}
|
|
{{-- breakStarted = false;--}}
|
|
{{-- const breakEndTime = new Date();--}}
|
|
{{-- clearInterval(breakTimer);--}}
|
|
|
|
{{-- // 1. محاسبه مدت زمان استراحت (بر حسب ثانیه)--}}
|
|
{{-- const durationSec = Math.floor((breakEndTime - breakStartTime) / 1000);--}}
|
|
|
|
{{-- // 2. تبدیل زمان های شروع و پایان به فرمت رشته ای--}}
|
|
{{-- const breakStartTime_str = formatLocalDatetime(breakStartTime);--}}
|
|
{{-- const breakEndTime_str = formatLocalDatetime(breakEndTime);--}}
|
|
|
|
{{-- // 3. فرمت کردن مدت زمان به H:i (فقط برای نمایش در کنسول/هشدار)--}}
|
|
{{-- const duration_formatted_h_i = formatDuration(durationSec);--}}
|
|
|
|
{{-- // 4. نمایش دکمه به حالت اولیه--}}
|
|
{{-- breakBtn.classList.remove('bg-red-500', 'hover:bg-red-600');--}}
|
|
{{-- // breakBtn.classList.add('bg-gray-500', 'hover:bg-gray-600');--}}
|
|
{{-- breakBtn.textContent = 'استراحت';--}}
|
|
|
|
{{-- // 5. ارسال دادهها به سرور--}}
|
|
{{-- let formData = new FormData();--}}
|
|
{{-- formData.append('start_time', breakStartTime_str);--}}
|
|
{{-- formData.append('end_time', breakEndTime_str);--}}
|
|
{{-- formData.append('duration_seconds', durationSec);--}}
|
|
{{-- formData.append('_token', '{{ csrf_token() }}');--}}
|
|
|
|
{{-- fetch("{{ url('attendance/break') }}", {--}}
|
|
{{-- method: "POST",--}}
|
|
{{-- body: formData--}}
|
|
{{-- })--}}
|
|
{{-- .then(res => res.json())--}}
|
|
{{-- .then(data => console.log('✅ Break saved:', data))--}}
|
|
{{-- .catch(err => console.error('💥 Error:', err));--}}
|
|
{{-- }--}}
|
|
{{--});--}}
|
|
|
|
|
|
|
|
|
|
|
|
{{-- ATTENDANCE MODAL--}}
|
|
function clearTime(fieldId) {
|
|
document.getElementById(fieldId).value = '';
|
|
}
|
|
|
|
function closeModal() {
|
|
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) => {
|
|
if (e.target === document.getElementById('modal-container')) {
|
|
closeModal();
|
|
}
|
|
});
|
|
|
|
function confirmSubmit() {
|
|
return confirm("آیا تغییرات اعمال شوند؟");
|
|
}
|
|
|
|
function openModal(title, id, enteredAt, exitedAt) {
|
|
const isNewEntry = id === 'new';
|
|
const modalTitleElement = document.getElementById('modal-title');
|
|
|
|
const hiddenIdInput = document.getElementById('edit_id');
|
|
// const hiddenIdInput = document.getElementById('delete_id');
|
|
|
|
const deleteIdInput = document.getElementById('delete_id');
|
|
const submitButton = document.querySelector('#modal-content button[type="submit"]');
|
|
const hiddenDateInput = document.querySelector('input[name="date"]');
|
|
|
|
// مقداردهی به فرم حذف
|
|
if (deleteIdInput) {
|
|
deleteIdInput.value = id;
|
|
}
|
|
|
|
const newDateLabel = document.getElementById('new-date-label');
|
|
const newDateGroup = document.getElementById('new-date-group');
|
|
const displayDateInput = document.getElementById('display_date');
|
|
|
|
const panelLink = document.getElementById('panel-access-link');
|
|
panelLink.href = `/go_down/${id}`;
|
|
|
|
document.getElementById('modal-container').classList.remove('hidden');
|
|
|
|
const startTimeInput = document.querySelector('input[name="starttime"]');
|
|
const endTimeInput = document.querySelector('input[name="endtime"]');
|
|
|
|
// تنظیم ساعتها
|
|
if (enteredAt && !isNaN(new Date(enteredAt).getTime())) {
|
|
startTimeInput.value = new Date(enteredAt).toLocaleTimeString('en-GB', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
} else {
|
|
startTimeInput.value = '09:00';
|
|
}
|
|
|
|
if (exitedAt && !isNaN(new Date(exitedAt).getTime())) {
|
|
endTimeInput.value = new Date(exitedAt).toLocaleTimeString('en-GB', {
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
});
|
|
}
|
|
// else {
|
|
// endTimeInput.value = '17:30';
|
|
// }
|
|
|
|
|
|
if (isNewEntry) {
|
|
// حالت ثبت جدید
|
|
modalTitleElement.innerText = 'ثبت روز کاری جدید';
|
|
newDateLabel.style.display = 'block';
|
|
newDateGroup.style.display = 'block';
|
|
|
|
displayDateInput.value = '';
|
|
hiddenDateInput.value = '';
|
|
|
|
// ✅ اصلاح: مقداردهی 'new' به فیلد ID
|
|
if (hiddenIdInput) hiddenIdInput.value = 'new';
|
|
|
|
submitButton.onclick = function () {
|
|
if (hiddenDateInput.value === '') {
|
|
alert("لطفاً یک تاریخ را از تقویم انتخاب کنید.");
|
|
return false;
|
|
}
|
|
return confirmSubmit();
|
|
};
|
|
|
|
} else {
|
|
// حالت ویرایش
|
|
modalTitleElement.innerText = 'تغییر ساعت کاری';
|
|
newDateLabel.style.display = 'none';
|
|
newDateGroup.style.display = 'none';
|
|
|
|
// ✅ اصلاح: استفاده از متغیر 'id' که از جدول فرستاده شده است
|
|
if (hiddenIdInput) hiddenIdInput.value = id;
|
|
if (hiddenDateInput) hiddenDateInput.value = enteredAt;
|
|
|
|
submitButton.onclick = function () {
|
|
return confirmSubmit();
|
|
};
|
|
}
|
|
|
|
//date and clock picker
|
|
$('.clockpicker').clockpicker({
|
|
autoclose: false,
|
|
placement: 'center',
|
|
donetext: 'ثبت',
|
|
vibrate: true,
|
|
});
|
|
|
|
// --- فعالسازی تقویم ---
|
|
$(".example1").persianDatepicker({
|
|
format: "YYYY/MM/DD",
|
|
altField: '#date',
|
|
altFormat: 'YYYY-MM-DD',
|
|
calendar: {
|
|
persian: {
|
|
locale: 'fa',
|
|
leapYearMode: "astronomical"
|
|
}
|
|
},
|
|
observer: true,
|
|
initialValue: false,
|
|
initialValueType: 'gregorian',
|
|
autoClose: true,
|
|
toolbox: {
|
|
calendarSwitch: {enabled: false},
|
|
todayButton: {enabled: true, text: {fa: "امروز"}}
|
|
},
|
|
});
|
|
|
|
}
|
|
|
|
|
|
$(document).ready(function () {
|
|
|
|
// 1. فعالسازی Clockpicker
|
|
$('.clockpicker').clockpicker({
|
|
autoclose: false,
|
|
placement: 'center',
|
|
donetext: 'ثبت',
|
|
vibrate: true,
|
|
// donetext: true,
|
|
});
|
|
|
|
|
|
$(".example1").persianDatepicker({
|
|
format: "YYYY/MM/DD",
|
|
altField: '#date',
|
|
altFormat: 'YYYY-MM-DD',
|
|
calendar: {
|
|
persian: {
|
|
locale: 'fa',
|
|
leapYearMode: "astronomical"
|
|
}
|
|
},
|
|
observer: true,
|
|
initialValue: false,
|
|
initialValueType: 'gregorian',
|
|
autoClose: true,
|
|
toolbox: {
|
|
calendarSwitch: {enabled: false},
|
|
todayButton: {enabled: true, text: {fa: "امروز"}}
|
|
},
|
|
onSelect: function (unix) {
|
|
// تبدیل تاریخ انتخابی به میلادی درست با timezone ایران
|
|
const d = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
|
console.log("Gregorian (Fixed):", d.format("YYYY-MM-DD"));
|
|
}
|
|
});
|
|
|
|
// 2. فعالسازی Persian Datepicker
|
|
// $('.example1').pDatepicker({
|
|
// format: 'YYYY/MM/DD',
|
|
// // placement: 'center',
|
|
// altField: '#date', // این فیلد مخفی، تاریخ میلادی تبدیل شده را دریافت میکند
|
|
// altFormat: 'YYYY-MM-DD', // میلادی ذخیره بشه
|
|
// // changeMonth: true,
|
|
// // changeYear: true,
|
|
// // showOtherMonths: true,
|
|
// // selectOtherMonths: true,
|
|
// autoClose: true,
|
|
//
|
|
// });
|
|
|
|
|
|
});
|
|
|
|
</script>
|
|
|
|
@endsection
|