Adding delay and penalty calculations
This commit is contained in:
@@ -140,6 +140,7 @@ $usersChunked = $users->chunk(3);
|
||||
<p>وظیفه: {{ $u->active_task }}</p>
|
||||
@endif
|
||||
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
|
||||
<p>جریمه تأخیر ماه: {{ number_format($u->monthly_late_fine ?? 0) }} تومان</p>
|
||||
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
|
||||
</a>
|
||||
@if($userType == 'admin')
|
||||
|
||||
@@ -1390,8 +1390,67 @@
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover {
|
||||
background-color: #334155 !important;
|
||||
background-color: #1e293b !important;
|
||||
color: #f1f5f9 !important;
|
||||
border: 1px solid #475569 !important;
|
||||
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.45) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .popover-title {
|
||||
background-color: #0f172a !important;
|
||||
color: #e2e8f0 !important;
|
||||
border-bottom: 1px solid #334155 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .popover-content {
|
||||
background-color: #1e293b !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-plate {
|
||||
background-color: #0f172a !important;
|
||||
border-color: #475569 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-tick {
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-tick.active,
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-tick:hover {
|
||||
background-color: rgba(34, 211, 238, 0.28) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-button,
|
||||
[data-theme="dark"] .clockpicker-popover button.clockpicker-button,
|
||||
[data-theme="dark"] .clockpicker-popover .btn.clockpicker-button,
|
||||
[data-theme="dark"] .clockpicker-popover .btn-default {
|
||||
background-color: #0f172a !important;
|
||||
background-image: none !important;
|
||||
color: #f8fafc !important;
|
||||
border-color: #475569 !important;
|
||||
border-top: 1px solid #475569 !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-button:hover,
|
||||
[data-theme="dark"] .clockpicker-popover button.clockpicker-button:hover,
|
||||
[data-theme="dark"] .clockpicker-popover .btn.clockpicker-button:hover,
|
||||
[data-theme="dark"] .clockpicker-popover .btn-default:hover {
|
||||
background-color: #334155 !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-bearing,
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-fg {
|
||||
fill: #22d3ee !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas-bg {
|
||||
fill: rgba(34, 211, 238, 0.35) !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .clockpicker-popover .clockpicker-canvas line {
|
||||
stroke: #22d3ee !important;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .datepicker-plot-area,
|
||||
|
||||
@@ -180,11 +180,33 @@ if (!empty($user->tasks)) {
|
||||
<th>ورود</th>
|
||||
<th>خروج</th>
|
||||
<th>مدت</th>
|
||||
<th>تأخیر</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
@php
|
||||
$monthLateMinutes = 0;
|
||||
$monthDailyFine = 0;
|
||||
$monthKey = $records->first()?->entered_at
|
||||
? \Carbon\Carbon::parse($records->first()->entered_at)
|
||||
: null;
|
||||
if ($monthKey) {
|
||||
[$mStart, $mEnd] = app(\App\Services\LatePenaltyService::class)->jalaliMonthBounds($monthKey);
|
||||
$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);
|
||||
@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 }} ')">
|
||||
@@ -218,14 +240,35 @@ if (!empty($user->tasks)) {
|
||||
<td style="border-right: 1px solid #ddd; padding: 4px;">
|
||||
{{ $item->entered_at && $item->exited_at ? \Carbon\Carbon::parse($item->duration)->format('H:i') : '-' }}
|
||||
</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(($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="5" class="table-summary">
|
||||
<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>
|
||||
|
||||
|
||||
@@ -36,6 +36,21 @@
|
||||
pattern="^09\d{9}$"
|
||||
class="border border-gray-300 rounded-md text-center" required>
|
||||
|
||||
<label class="pb-3">تاریخ تولد:</label>
|
||||
<div class="flex flex-col gap-1">
|
||||
<input type="text" id="birthday_display"
|
||||
value="{{ $user->birthday ? \Morilog\Jalali\Jalalian::fromCarbon($user->birthday)->format('Y/m/d') : '' }}"
|
||||
placeholder="اختیاری — انتخاب تاریخ"
|
||||
class="border border-gray-300 rounded-md text-center"
|
||||
autocomplete="off" readonly>
|
||||
<input type="hidden" name="birthday" id="birthday"
|
||||
value="{{ $user->birthday ? $user->birthday->format('Y-m-d') : '' }}">
|
||||
<button type="button" id="clear_birthday"
|
||||
class="text-xs text-gray-500 hover:text-red-500 cursor-pointer self-end">
|
||||
پاک کردن
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<label class="pb-3">چت آیدی تلگرام:</label>
|
||||
<input type="text" pattern="\d+" name="chatid" value="{{ $user->chatid ?? '' }}"
|
||||
placeholder="چت آیدی تلگرام"
|
||||
@@ -71,6 +86,27 @@
|
||||
<button type="button" id="registerFingerprint" class="...">ثبت اثر انگشت</button>
|
||||
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('#birthday_display').persianDatepicker({
|
||||
format: 'YYYY/MM/DD',
|
||||
autoClose: true,
|
||||
initialValue: false,
|
||||
toolbox: {
|
||||
calendarSwitch: {enabled: false},
|
||||
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
||||
},
|
||||
onSelect: function (unix) {
|
||||
const gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
||||
$('#birthday').val(gregorian.format('YYYY-MM-DD'));
|
||||
}
|
||||
});
|
||||
|
||||
$('#clear_birthday').on('click', function () {
|
||||
$('#birthday_display').val('');
|
||||
$('#birthday').val('');
|
||||
});
|
||||
});
|
||||
|
||||
function base64urlToBuffer(base64url) {
|
||||
const padding = '='.repeat((4 - base64url.length % 4) % 4);
|
||||
const base64 = (base64url + padding).replace(/-/g, '+').replace(/_/g, '/');
|
||||
|
||||
@@ -201,6 +201,21 @@ if (isset($task)) {
|
||||
updateSelectedCount();
|
||||
|
||||
$(document).ready(function () {
|
||||
function syncGregorianFromDisplay(displaySelector, hiddenSelector) {
|
||||
const displayVal = $(displaySelector).val();
|
||||
if (!displayVal || $(hiddenSelector).val()) return;
|
||||
try {
|
||||
// اگر مقدار نمایشی شمسی YYYY/MM/DD باشد
|
||||
const parts = String(displayVal).replace(/-/g, '/').split('/');
|
||||
if (parts.length === 3) {
|
||||
const pDate = new persianDate([Number(parts[0]), Number(parts[1]), Number(parts[2])]);
|
||||
const gregorian = pDate.toCalendar('gregorian').toLocale('en').format('YYYY-MM-DD');
|
||||
$(hiddenSelector).val(gregorian);
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
$('#start_date').persianDatepicker({
|
||||
format: 'YYYY/MM/DD',
|
||||
autoClose: true,
|
||||
@@ -229,6 +244,26 @@ if (isset($task)) {
|
||||
}
|
||||
});
|
||||
|
||||
// اگر مقدار اولیه از دیتابیس آمده، فیلد مخفی میلادی را هم پر کن
|
||||
@if(!empty($task?->start_date))
|
||||
$('#start_date_gregorian').val(@json(\Carbon\Carbon::parse($task->start_date)->format('Y-m-d')));
|
||||
$('#start_date').val(@json(\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($task->start_date))->format('Y/m/d')));
|
||||
@endif
|
||||
@if(!empty($task?->target_date))
|
||||
$('#target_date_gregorian').val(@json(\Carbon\Carbon::parse($task->target_date)->format('Y-m-d')));
|
||||
$('#target_date').val(@json(\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($task->target_date))->format('Y/m/d')));
|
||||
@endif
|
||||
|
||||
$('form').on('submit', function (e) {
|
||||
syncGregorianFromDisplay('#start_date', '#start_date_gregorian');
|
||||
syncGregorianFromDisplay('#target_date', '#target_date_gregorian');
|
||||
if (!$('#start_date_gregorian').val()) {
|
||||
e.preventDefault();
|
||||
alert('لطفاً تاریخ شروع را از تقویم انتخاب کنید.');
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$('#clear_target_date').on('click', function () {
|
||||
$('#target_date').val('');
|
||||
$('#target_date_gregorian').val('');
|
||||
|
||||
@@ -1077,6 +1077,14 @@ $state = [
|
||||
}
|
||||
}
|
||||
|
||||
function toDatetimeLocalValue(iso) {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
function renderTaskDurations(task) {
|
||||
const list = document.getElementById('modalDurationsList');
|
||||
if (!list) return;
|
||||
@@ -1102,7 +1110,9 @@ $state = [
|
||||
const start = d.start ? new Date(d.start).toLocaleString('fa-IR') : '-';
|
||||
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
|
||||
const dur = d.duration || (d.end ? '-' : '...');
|
||||
return `
|
||||
|
||||
if (!isAdminLevel1) {
|
||||
return `
|
||||
<div class="ui-panel border border-gray-200 rounded-lg p-2">
|
||||
<div class="text-xs text-gray-500 flex justify-between">
|
||||
<span>${escapeHtml(uname)}</span>
|
||||
@@ -1112,9 +1122,92 @@ $state = [
|
||||
شروع: ${escapeHtml(start)}<br/>
|
||||
پایان: ${escapeHtml(end)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="ui-panel border border-gray-200 rounded-lg p-2" data-duration-id="${d.id}">
|
||||
<div class="text-xs text-gray-500 flex justify-between mb-2">
|
||||
<span>${escapeHtml(uname)}</span>
|
||||
<span class="font-mono duration-value">${escapeHtml(dur)}</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-2 text-xs">
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>شروع</span>
|
||||
<input type="datetime-local"
|
||||
class="duration-start ui-input text-sm"
|
||||
value="${toDatetimeLocalValue(d.start)}">
|
||||
</label>
|
||||
<label class="flex flex-col gap-1">
|
||||
<span>پایان (خالی = در حال انجام)</span>
|
||||
<input type="datetime-local"
|
||||
class="duration-end ui-input text-sm"
|
||||
value="${toDatetimeLocalValue(d.end)}">
|
||||
</label>
|
||||
<button type="button"
|
||||
class="save-duration-btn bg-cyan-600 hover:bg-cyan-700 text-white rounded-lg py-1.5 px-3">
|
||||
ذخیره زمان
|
||||
</button>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
|
||||
if (isAdminLevel1) {
|
||||
list.querySelectorAll('.save-duration-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async function () {
|
||||
const card = this.closest('[data-duration-id]');
|
||||
if (!card) return;
|
||||
const durationId = card.getAttribute('data-duration-id');
|
||||
const startVal = card.querySelector('.duration-start')?.value || '';
|
||||
const endVal = card.querySelector('.duration-end')?.value || '';
|
||||
|
||||
if (!startVal) {
|
||||
alert('زمان شروع الزامی است.');
|
||||
return;
|
||||
}
|
||||
|
||||
this.disabled = true;
|
||||
try {
|
||||
const res = await fetch("{{ route('task.duration.edit') }}", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
duration_id: Number(durationId),
|
||||
start: startVal,
|
||||
end: endVal || null,
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.status) {
|
||||
alert(data.message || 'خطا در ذخیره زمان');
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentTaskDetails && Array.isArray(currentTaskDetails.duration)) {
|
||||
const idx = currentTaskDetails.duration.findIndex(x => String(x.id) === String(durationId));
|
||||
if (idx >= 0) {
|
||||
currentTaskDetails.duration[idx] = data.duration;
|
||||
}
|
||||
if (data.finished_at !== undefined) {
|
||||
currentTaskDetails.finished_at = data.finished_at;
|
||||
}
|
||||
renderTaskDurations(currentTaskDetails);
|
||||
updateModalTotalDuration(currentTaskDetails);
|
||||
}
|
||||
alert(data.message || 'ذخیره شد');
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert('خطا در ارتباط با سرور');
|
||||
} finally {
|
||||
this.disabled = false;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function switchDetailsTab(tab) {
|
||||
@@ -1262,6 +1355,7 @@ $state = [
|
||||
? formatDateTime(task.created_at)
|
||||
: '-';
|
||||
document.getElementById('modalTaskAssignees').textContent = formatAssigneeNames(task);
|
||||
updateTaskDateViews(task);
|
||||
}
|
||||
|
||||
// باز کردن مودال جزئیات تسک
|
||||
@@ -1280,8 +1374,7 @@ $state = [
|
||||
// پر کردن فیلدهای مودال
|
||||
document.getElementById('modalTaskTitle').textContent = task.title || 'بدون عنوان';
|
||||
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
|
||||
document.getElementById('modalTaskStartDateView').textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
|
||||
document.getElementById('modalTaskTargetDateView').textContent = task.target_date ? formatJalaliDate(task.target_date) : '-';
|
||||
updateTaskDateViews(task);
|
||||
updateTaskFinishedAtView(task);
|
||||
updateModalInfoFields(task);
|
||||
|
||||
@@ -1497,8 +1590,8 @@ $state = [
|
||||
sdEdit.value = currentTaskDetails.start_date ? formatJalaliDate(currentTaskDetails.start_date) : '';
|
||||
tdEdit.value = currentTaskDetails.target_date ? formatJalaliDate(currentTaskDetails.target_date) : '';
|
||||
|
||||
if (sdGreg) sdGreg.value = currentTaskDetails.start_date || '';
|
||||
if (tdGreg) tdGreg.value = currentTaskDetails.target_date || '';
|
||||
if (sdGreg) sdGreg.value = normalizeDateValue(currentTaskDetails.start_date);
|
||||
if (tdGreg) tdGreg.value = normalizeDateValue(currentTaskDetails.target_date);
|
||||
|
||||
initModalDatepickers();
|
||||
}
|
||||
@@ -1638,15 +1731,52 @@ $state = [
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
// فرمت تاریخ شمسی (ساده)
|
||||
function formatJalaliDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
// فرض میکنیم از کتابخانه Jalalian در blade استفاده میکنید، اما در js نیز میتوان پیادهسازی کرد
|
||||
// برای سادگی از Intl یا یک تبدیل ساده استفاده میکنیم
|
||||
const d = new Date(dateStr);
|
||||
if (isNaN(d)) return dateStr;
|
||||
// این قسمت میتواند با کتابخانه moment-jalaali جایگزین شود
|
||||
return d.toLocaleDateString('fa-IR'); // نیاز به تنظیم locale مرورگر دارد
|
||||
const raw = String(dateStr).trim();
|
||||
if (!raw || raw.startsWith('0000-00-00')) return '-';
|
||||
|
||||
if (typeof persianDate !== 'undefined') {
|
||||
try {
|
||||
const m = raw.match(/^(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return new persianDate(d)
|
||||
.toCalendar('persian')
|
||||
.toLocale('en')
|
||||
.format('YYYY/MM/DD');
|
||||
}
|
||||
const d = new Date(raw);
|
||||
if (!Number.isNaN(d.getTime())) {
|
||||
return new persianDate(d)
|
||||
.toCalendar('persian')
|
||||
.toLocale('en')
|
||||
.format('YYYY/MM/DD');
|
||||
}
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
const d = new Date(raw.includes('T') || raw.includes(' ') ? raw : (raw + 'T00:00:00'));
|
||||
if (Number.isNaN(d.getTime())) return raw;
|
||||
return d.toLocaleDateString('fa-IR');
|
||||
}
|
||||
|
||||
function normalizeDateValue(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const m = String(dateStr).match(/^(\d{4}-\d{2}-\d{2})/);
|
||||
return m ? m[1] : String(dateStr);
|
||||
}
|
||||
|
||||
function updateTaskDateViews(task) {
|
||||
const sdView = document.getElementById('modalTaskStartDateView');
|
||||
const tdView = document.getElementById('modalTaskTargetDateView');
|
||||
if (sdView) {
|
||||
sdView.textContent = task.start_date ? formatJalaliDate(task.start_date) : '-';
|
||||
}
|
||||
if (tdView) {
|
||||
tdView.textContent = task.target_date ? formatJalaliDate(task.target_date) : '-';
|
||||
}
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr) {
|
||||
|
||||
Reference in New Issue
Block a user