Improve tasks UI, dark theme, performance fixes, and admin pages polish

This commit is contained in:
2026-06-02 18:45:01 +03:30
parent 54aa8f40dd
commit 300575930f
18 changed files with 2598 additions and 780 deletions
+37 -111
View File
@@ -1,63 +1,9 @@
<?php
use App\Models\Attendance;
use App\Models\AttendanceBreak;
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
//dd($reqs);
$now = Jalalian::fromCarbon(Carbon::now());
foreach ($users as $user) {
$totaltime = 0;
$tsec = 0;
foreach ($user->attendances as $att) {
$date = $att->entered_at != null ? Jalalian::fromCarbon(Carbon::parse($att->entered_at)) : null;
$att->date = $date != null ? $date->format('Y/m/d') : null;
if ($att->duration != null && $date != null && $now->getMonth() == $date->getMonth()) {
[$h, $m, $s] = explode(':', $att->duration);
$tsec += $h * 3600 + $m * 60 + $s;
}
}
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
$seconds = $tsec % 60;
$totalTime = sprintf('%02d:%02d', $hours, $minutes);
$user->totaltime = $totalTime;
}
//dd($users);
$now = now()->format('Y-m-d');
foreach ($users as $u) {
$us = $u->attendances()
->whereNull('exited_at')
->latest('entered_at')
->first();
// dump($us);
if ($us) {
$us->entered_at = Carbon::parse($us->entered_at)->format('Y-m-d');
if ($us->entered_at == $now)
$us = true;
else
$us = false;
$u->working = $us ? 1 : 0;
} else
$u->working = 0;
foreach ($u->attendances as $u_att) {
$break = AttendanceBreak::wherenull('break_end')->where('attendance_id', $u_att->id)->exists();
if ($break)
$u->working = 2;
}
}
//dd($users);
$users = $users->sortByDesc('working');
//$users = json_decode(json: $users);
// $users = collect($users);
$usersChunked = $users->chunk(3);
//dd($users);
?>
@@ -66,43 +12,35 @@ $usersChunked = $users->chunk(3);
<div class="container w-full flex flex-col items-center">
@if(!collect($reqs)->pluck('attendances')->flatten()->isEmpty())
<div class="mt-10 bg-white rounded-lg shadow-md max-w-[350px] flex flex-col items-center">
<p style="min-font-size: 30; text-align: center; margin-bottom: 10px">درخواست ها</p>
{{-- {{dd($reqs)}}--}}
<div class="ui-requests-card flex flex-col items-center">
<p>درخواست ها</p>
@foreach ($reqs as $u)
@if(!empty($u['attendances']))
<button type="button"
class="w-[150px] p-3 font-bold text-white bg-blue-500 hover:bg-blue-700 rounded transition duration-150 text-center "
onclick="togglerequests('attendance-{{ $loop->index }}')"
style="margin: 10px 0; text-align: center; background-color: #00bcd4">
class="ui-chip-btn"
onclick="togglerequests('attendance-{{ $loop->index }}')">
{{ $u['name'] }}
</button>
{{-- @else--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}}
{{-- @break--}}
@endif
<div id="attendance-{{ $loop->index }}" class="hidden-content pt-4">
<table style="max-width: 350px; margin: auto; text-align: center" class="w-[350px] table-auto">
<div id="attendance-{{ $loop->index }}" class="hidden-content pt-4 w-full">
<div class="ui-table-wrap">
<table class="w-full table-auto">
<thead>
<tr class="text-sm" style="background-color: #00a3ba; color: white; text-align: center;">
<th class="px-2 py-2 border w-1/6">درخواست</th>
<th class="px-2 py-2 border w-1/6">تاریخ</th>
<th class="px-2 py-2 border w-1/6">ورود</th>
<th class="px-2 py-2 border w-1/6">خروج</th>
{{-- @if($userType == 'admin')--}}
<th class="px-2 py-2 border w-1/6">تعیین وضعیت</th>
{{-- @endif--}}
<tr>
<th class="px-2 py-2 w-1/6">درخواست</th>
<th class="px-2 py-2 w-1/6">تاریخ</th>
<th class="px-2 py-2 w-1/6">ورود</th>
<th class="px-2 py-2 w-1/6">خروج</th>
<th class="px-2 py-2 w-1/6">تعیین وضعیت</th>
</tr>
</thead>
<tbody>
{{-- {{dd($u['attendances'])}}--}}
@forelse($u['attendances'] as $rec)
<tr style="background-color: {{$rec->id=='new' ? 'lightblue' : 'white'}}">
<tr class="{{ $rec->id=='new' ? 'is-new' : '' }}">
{{--{{dd($rec)}}--}}
<td class="px-4 py-2 border">{{ match ($rec->function) {
'edit' => "تغییر ساعت",
@@ -138,8 +76,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="approve">
<input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full"
style="background-color: #81ff65; margin-bottom: 5px">
<button type="submit" class="ui-btn-approve">
تایید
</button>
</form>
@@ -148,8 +85,7 @@ $usersChunked = $users->chunk(3);
<input type="hidden" name="update" id="update" value="reject">
<input type="hidden" name="data" id="data"
value="{{json_encode($rec)}}">
<button class="bg-white rounded w-full"
style="background-color: #ff6565">
<button type="submit" class="ui-btn-reject">
رد
</button>
</form>
@@ -163,7 +99,8 @@ $usersChunked = $users->chunk(3);
@endforelse
</tbody>
</table>
</div> {{-- پایان hidden-content --}}
</div>
</div>
@endforeach
{{-- @if(collect($reqs)->pluck('attendances')->flatten()->isEmpty())--}}
{{-- <p class="text-stone-400" style="text-align: center">درخواستی ثبت نشده</p>--}}
@@ -175,38 +112,29 @@ $usersChunked = $users->chunk(3);
<div class="grid grid-cols-2 grid-rows-none ">
@foreach ($usersChunked as $chunk)
@foreach ($chunk as $u)
<div class="form m-3 justify-evenly rounded-lg shadow-md "
style="align-items: center; text-align: center; border: 2px solid
{{ $u->active_task ? '#00ff15' : ($u->working == 1 ? '#16c8ce' : ($u->working == 2 ? 'orange' : 'white' ))}}; padding: 10px; ">
@php
@endphp
<a href="/admin/{{ $u->user_name }}"
class=" rounded-xl"
style="text-decoration: none; color: inherit;">
<p style="font-weight: bold ; color:#333;">
{{ $u->name ?? $u->user_name }}
</p>
@php
$cardClass = 'ui-user-card m-3';
if ($u->active_task) {
$cardClass .= ' is-active';
} elseif ($u->working == 1) {
$cardClass .= ' is-working';
} elseif ($u->working == 2) {
$cardClass .= ' is-break';
}
@endphp
<div class="{{ $cardClass }}">
<a href="/admin/{{ $u->user_name }}">
<p class="ui-user-name">{{ $u->name ?? $u->user_name }}</p>
@if($u->active_task)
<p>وظیفه: {{ $u->active_task }}</p>
@endif
<p>
کارکرد ماه جاری:
{{ $u->totaltime }}
</p>
<p>
تعداد درخواست ها:
{{ $u->req_counts }}
</p>
<p>کارکرد ماه جاری: {{ $u->totaltime }}</p>
<p>تعداد درخواست ها: {{ $u->req_counts }}</p>
</a>
@if($userType == 'admin')
<button type="button"
class="rounded-lg bg-cyan-500 hover:bg-cyan-600"
onclick="window.location.href='{{ url('attendance/profile').'?username='.$u->user_name }}'"
style=" border: none; color: white; padding: 5px 10px; text-align: center; text-decoration: none;
display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; ">
class="ui-chip-btn mt-2 !min-w-0 !w-auto !text-sm"
onclick="window.location.href='{{ url('attendance/profile').'?username='.$u->user_name }}'">
ویرایش
</button>
@endif
@@ -214,10 +142,8 @@ $usersChunked = $users->chunk(3);
@endforeach
@endforeach
<a class="form m-3 justify-evenly rounded-lg shadow-md " href="{{route('admin.new')}}"
style="align-items: center; text-align: center; ">
<i class="fa-regular fa-plus "
style="font-size: 80px; width: 90%; height: 90%;"></i>
<a class="ui-user-card m-3 flex items-center justify-center min-h-[140px]" href="{{route('admin.new')}}">
<i class="fa-regular fa-plus text-5xl text-gray-400"></i>
</a>
</div>
+12 -14
View File
@@ -224,7 +224,7 @@ SVG;
@else
<div class="bg-white flex justify-flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<div class="ui-panel flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<img src="{{$user->picture ?? $gravatarUrl}}" alt="Profile Picture" class="w-16 h-16 rounded-full ">
<label>
@@ -361,7 +361,7 @@ SVG;
<div class="w-full flex justify-center mt-4" style="">
<div class="overflow-hidden border border-gray-300 rounded-[23px] bg-white max-w-[350px]">
<div class="overflow-hidden border border-gray-300 rounded-[23px] ui-panel max-w-[350px]">
<div class=" border-b border-gray-300 text-center w-full">
@@ -369,7 +369,7 @@ SVG;
<div class="w-[350px]">
@if(isset($_COOKIE['user_identity']))
<table class="min-w-full bg-white shadow-md rounded-lg table-auto">
<table class="min-w-full ui-attendance-table table-auto">
<thead class=" text-black sticky top-0 z-10"
style="background: rgba(244,244,244,0.82);border-bottom: 1px solid;border-color: #e1e1e1 ">
<tr>
@@ -456,10 +456,10 @@ SVG;
<!-- ATTENDANCE Modal -->
<div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p>
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">
@@ -559,16 +559,14 @@ SVG;
{{-- START TASK MODAL --}}
<div id="taskModal"
class="hidden fixed inset-0 bg-black/40 items-start justify-center z-50 flex backdrop-blur-sm scroll-auto pt-10">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center">
<button onclick="closeTaskModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
class="ui-modal-backdrop hidden">
<div class="ui-modal-panel">
<button type="button" onclick="closeTaskModal()" class="ui-modal-close" aria-label="بستن">
&times;
</button>
<h2 class="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<h2 class="ui-title" id="modalTitle">انتخاب وظیفه</h2>
<p class="ui-subtitle" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<div class="grid flex-col gap-2 grid-cols-2 pb-3 items-center justify-items-end">
@if(!empty($user->tasks))
+2 -3
View File
@@ -3,10 +3,9 @@
<div class=" flex flex-col items-center justify-center p-4 " dir="rtl"
style="padding-top: 50px">
<div class="bg-white p-6 md:p-8 rounded-2xl shadow-2xl w-[350px] max-w-sm border-t-8 border-[#16c8ce]">
<div class="ui-card ui-card-body w-[350px] max-w-sm" style="border-top: 4px solid var(--primary-color);">
<div class="text-center mb-6">
<h1 class="text-2xl font-extrabold text-gray-800 border-b pb-2">کاربر جدید</h1>
<h1 class="ui-title border-b border-gray-300 pb-2">کاربر جدید</h1>
</div>
<form id="attendanceForm" action="{{route('admin.new')}}" method="post" class="space-y-4">
+273 -86
View File
@@ -3,21 +3,17 @@
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
// پیدا کردن نام ماه قبل به تاریخ شمسی (مثلا اگر الان اسفند است، خروجی "بهمن" خواهد بود)
$previousMonthName = Jalalian::now()->subMonths(1)->format('%B');
// محاسبه مجموع کارکرد برای هر کارمند به تفکیک ماه
$employeeTotals = [];
foreach ($data as $employee) {
// تبدیل آبجکت به آرایه در صورت نیاز برای حلقه
$recordsGrouped = is_array($employee->records) ? $employee->records : (array)$employee->records;
$monthlyTotals = [];
foreach ($recordsGrouped as $month => $attendances) {
$tsec = 0;
foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -26,29 +22,239 @@ foreach ($data as $employee) {
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
// ذخیره زمان برای این ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
}
// ذخیره کل ماه‌ها برای این کارمند
$employeeTotals[$employee->id] = $monthlyTotals;
}
?>
@extends('app')
@section('content')
<div class="container flex flex-col items-center justify-center mx-auto pb-10">
@section('style')
<style>
.admin-show-page,
.ui-page {
width: 100%;
max-width: 420px;
margin: 0 auto;
padding: 0 0.5rem 1.5rem;
}
.admin-show-empty,
.ui-empty {
margin-top: 5rem;
text-align: center;
color: var(--text-muted);
font-size: 1.1rem;
font-weight: 700;
}
.employee-picker {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.45rem;
margin: 0.75rem 0 1rem;
padding: 0.75rem;
background: var(--surface-glass);
border: 1px solid var(--border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
}
.emp-btn {
padding: 0.5rem 0.85rem;
border-radius: 9999px;
font-size: 0.82rem;
font-weight: 700;
border: 1px solid var(--border-input);
background: var(--surface-input);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.emp-btn:hover {
color: var(--text-primary);
background: var(--surface-tab-hover);
border-color: color-mix(in srgb, var(--primary-color) 35%, var(--border-input));
}
.emp-btn-active {
color: #fff;
background: var(--primary-color);
border-color: color-mix(in srgb, var(--primary-color) 80%, white);
box-shadow: 0 6px 18px color-mix(in srgb, var(--primary-color) 35%, transparent);
}
.employee-panel {
display: none;
flex-direction: column;
align-items: center;
width: 100%;
}
.employee-panel.is-visible {
display: flex;
}
.employee-header {
width: 100%;
max-width: 350px;
display: flex;
flex-direction: column;
gap: 0.65rem;
margin-bottom: 0.85rem;
}
.employee-header h2 {
margin: 0;
font-size: 1.15rem;
font-weight: 800;
color: var(--text-primary);
}
.employee-stat {
display: inline-flex;
align-items: center;
flex-wrap: wrap;
gap: 0.35rem;
padding: 0.55rem 0.85rem;
border-radius: var(--radius-lg);
background: color-mix(in srgb, var(--primary-color) 12%, var(--surface-card));
border: 1px solid color-mix(in srgb, var(--primary-color) 28%, var(--border-subtle));
color: var(--text-secondary);
font-size: 0.78rem;
}
.employee-stat strong {
font-size: 0.95rem;
font-weight: 800;
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
font-variant-numeric: tabular-nums;
}
.month-tabs {
width: 100%;
max-width: 350px;
display: flex;
gap: 0.35rem;
overflow-x: auto;
padding: 0.45rem;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
background: var(--surface-tabs);
border: 1px solid var(--border-glass);
border-bottom: none;
scrollbar-width: none;
}
.month-tabs::-webkit-scrollbar {
display: none;
}
.month-tab {
flex-shrink: 0;
padding: 0.5rem 0.85rem;
border: none;
border-radius: 0.75rem;
background: transparent;
color: var(--text-muted);
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
transition: all 0.2s ease;
}
.month-tab:hover {
color: var(--text-primary);
background: var(--surface-tab-hover);
}
.month-tab-active {
color: color-mix(in srgb, var(--primary-color) 78%, var(--text-primary));
background: var(--surface-tab-active);
box-shadow: var(--shadow-soft);
}
.attendance-card {
width: 100%;
max-width: 350px;
background: var(--surface-glass);
border: 1px solid var(--border-glass);
border-radius: 0 0 var(--radius-xl) var(--radius-xl);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.attendance-table {
width: 100%;
border-collapse: collapse;
text-align: center;
}
.attendance-table thead th {
padding: 0.65rem 0.35rem;
font-size: 0.72rem;
font-weight: 700;
color: var(--text-muted);
background: var(--surface-tabs);
border-bottom: 1px solid var(--border-subtle);
}
.attendance-table thead th:not(:last-child) {
border-left: 1px solid var(--border-subtle);
}
.attendance-table tbody td {
padding: 0.5rem 0.25rem;
font-size: 0.82rem;
color: var(--text-primary);
border-bottom: 1px solid var(--border-subtle);
font-variant-numeric: tabular-nums;
}
.attendance-table tbody td:not(:last-child) {
border-left: 1px solid var(--border-subtle);
}
.attendance-table tbody tr:hover td {
background: var(--surface-tab-hover);
}
.attendance-table tfoot td {
padding: 0.75rem 0.5rem;
font-size: 0.85rem;
font-weight: 800;
color: var(--text-primary);
background: color-mix(in srgb, var(--primary-color) 10%, var(--surface-card));
border-top: 2px solid var(--border-subtle);
}
.panel-empty {
margin: 2.5rem 0;
text-align: center;
color: var(--text-muted);
font-weight: 700;
}
</style>
@endsection
@section('content')
<div class="admin-show-page">
@if(empty($data) || count($data) == 0)
<div class="mt-80 text-gray-500 text-xl font-bold italic">
<div class="admin-show-empty">
<h1>اطلاعاتی موجود نمیباشد</h1>
</div>
@else
<div class="flex flex-wrap justify-center gap-2 mt-10 mb-6 max-w-3xl">
<div class="employee-picker">
@foreach($data as $employee)
<button class="emp-btn px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-cyan-500 hover:text-white transition-colors duration-300 font-bold shadow-sm"
<button type="button"
class="emp-btn"
data-emp-id="{{ $employee->id }}"
onclick="openEmployee(event, 'emp_{{ $employee->id }}', '{{ $employee->id }}')">
{{ $employee->name }}
</button>
@@ -56,15 +262,13 @@ foreach ($data as $employee) {
</div>
@foreach($data as $employee)
<div id="emp_{{ $employee->id }}" class="employee-content hidden flex-col items-center justify-center w-full">
<div class="flex justify-between items-center mb-4 w-[350px]">
<h2 class="text-xl font-bold text-cyan-700">{{ $employee->name }}</h2>
<span class="text-xs bg-cyan-50 text-cyan-800 px-3 py-1.5 rounded-full border border-cyan-200 shadow-sm flex items-center gap-1">
<div id="emp_{{ $employee->id }}" class="employee-panel">
<div class="employee-header">
<h2>{{ $employee->name }}</h2>
<div class="employee-stat">
<span>کارکرد ماه قبل ({{ $previousMonthName }}):</span>
<strong class="text-sm font-black">{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
</span>
<strong>{{ $employeeTotals[$employee->id][$previousMonthName] ?? '00:00' }}</strong>
</div>
</div>
@php
@@ -72,13 +276,14 @@ foreach ($data as $employee) {
@endphp
@if(empty($recordsGrouped))
<div class="my-10 text-gray-500 text-lg font-bold italic">
<div class="panel-empty">
رکورد ترددی برای این شخص ثبت نشده است.
</div>
@else
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto whitespace-nowrap hide-scrollbar" style="max-width: 350px">
<div class="month-tabs">
@foreach(array_keys($recordsGrouped) as $month)
<button class="tablinks-{{ $employee->id }} active rounded-t-2xl cursor-pointer px-4 py-2 bg-gray-100 border-b-2 border-transparent hover:bg-gray-200 flex-shrink-0"
<button type="button"
class="month-tab tablinks-{{ $employee->id }}"
onclick="openMonth(event, 'month_{{ $employee->id }}_{{ $month }}', '{{ $employee->id }}')">
{{ $month }}
</button>
@@ -86,43 +291,37 @@ foreach ($data as $employee) {
</div>
@foreach($recordsGrouped as $month => $records)
<div id="month_{{ $employee->id }}_{{ $month }}" class="tabcontent-{{ $employee->id }} hidden w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-lg border border-gray-100">
<table style="width:100%; border-collapse: collapse; text-align: center">
<div id="month_{{ $employee->id }}_{{ $month }}"
class="attendance-card tabcontent-{{ $employee->id }}"
style="display: none;">
<table class="attendance-table">
<thead>
<tr style="border-bottom: 1px solid #ddd; background-color: #f8f9fa;">
<th style="border-right: 1px solid #ddd; padding: 8px 0;">تاریخ</th>
<th style="border-right: 1px solid #ddd;">روز</th>
<th style="border-right: 1px solid #ddd;">ورود</th>
<th style="border-right: 1px solid #ddd;">خروج</th>
<th style="border-right: 1px solid #ddd;">مدت</th>
<tr>
<th>تاریخ</th>
<th>روز</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
<tbody>
@foreach($records as $row)
<tr class="hover:bg-cyan-100 transition-colors" style="border-bottom: 1px solid #ddd;">
<td style="border-right: 1px solid #ddd; padding: 4px 0;">
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ \Carbon\Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ $row->exited_at ? \Carbon\Carbon::parse($row->exited_at)->format('H:i') : '-' }}
</td>
<td style="border-right: 1px solid #ddd;">
{{ $row->duration ? \Carbon\Carbon::parse($row->duration)->format('H:i') : '-' }}
</td>
<tr>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</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->duration ? Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
</tbody>
<tfoot>
<tr>
<td colspan="5" class="bg-cyan-50 font-bold text-gray-700 rounded-b-xl" style="border-top: 2px solid #ddd; text-align: center; padding: 10px 0;">
<td colspan="5">
مجموع کارکرد این ماه: {{ $employeeTotals[$employee->id][$month] ?? '00:00' }}
</td>
</tr>
</tbody>
</tfoot>
</table>
</div>
@endforeach
@@ -132,58 +331,46 @@ foreach ($data as $employee) {
@endif
</div>
<style>
/* مخفی کردن اسکرول‌بار برای تب‌های ماه‌ها اگر تعدادشان زیاد شد */
.hide-scrollbar::-webkit-scrollbar { display: none; }
.hide-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<script>
function openEmployee(evt, empContainerId, empId) {
let employeeContents = document.getElementsByClassName("employee-content");
for (let i = 0; i < employeeContents.length; i++) {
employeeContents[i].style.display = "none";
document.querySelectorAll('.employee-panel').forEach(el => el.classList.remove('is-visible'));
document.querySelectorAll('.emp-btn').forEach(btn => btn.classList.remove('emp-btn-active'));
const panel = document.getElementById(empContainerId);
if (panel) panel.classList.add('is-visible');
if (evt.currentTarget) {
evt.currentTarget.classList.add('emp-btn-active');
}
let empBtns = document.getElementsByClassName("emp-btn");
for (let i = 0; i < empBtns.length; i++) {
empBtns[i].classList.remove("bg-cyan-500", "text-white", "shadow-md");
empBtns[i].classList.add("bg-gray-200", "text-gray-700");
}
document.getElementById(empContainerId).style.display = "flex";
evt.currentTarget.classList.remove("bg-gray-200", "text-gray-700");
evt.currentTarget.classList.add("bg-cyan-500", "text-white", "shadow-md");
let firstMonthTab = document.querySelector("#" + empContainerId + " .tablinks-" + empId);
const firstMonthTab = document.querySelector('#' + empContainerId + ' .tablinks-' + empId);
if (firstMonthTab) {
firstMonthTab.click();
}
}
function openMonth(evt, monthId, empId) {
let tabcontents = document.getElementsByClassName("tabcontent-" + empId);
for (let i = 0; i < tabcontents.length; i++) {
tabcontents[i].style.display = "none";
}
document.querySelectorAll('.tabcontent-' + empId).forEach(el => {
el.style.display = 'none';
});
let tablinks = document.getElementsByClassName("tablinks-" + empId);
for (let i = 0; i < tablinks.length; i++) {
tablinks[i].classList.remove("bg-white", "border-cyan-500");
tablinks[i].classList.add("bg-gray-100", "border-transparent");
}
document.querySelectorAll('.tablinks-' + empId).forEach(btn => {
btn.classList.remove('month-tab-active');
});
document.getElementById(monthId).style.display = "block";
evt.currentTarget.classList.remove("bg-gray-100", "border-transparent");
evt.currentTarget.classList.add("bg-white", "border-cyan-500");
const monthPanel = document.getElementById(monthId);
if (monthPanel) monthPanel.style.display = 'block';
if (evt.currentTarget) {
evt.currentTarget.classList.add('month-tab-active');
}
}
document.addEventListener("DOMContentLoaded", function () {
let firstEmployeeBtn = document.querySelector(".emp-btn");
document.addEventListener('DOMContentLoaded', function () {
const firstEmployeeBtn = document.querySelector('.emp-btn');
if (firstEmployeeBtn) {
firstEmployeeBtn.click();
}
});
</script>
@endsection
@endsection
File diff suppressed because it is too large Load Diff
+50 -78
View File
@@ -37,17 +37,7 @@ if (!empty($user->tasks)) {
return array_search($task->state, $state_order);
});
}
//dd(count($break = $user->attendanceBreaks));
if (count($user->attendanceBreaks) > 0) {
$break = $user->attendanceBreaks[0];
if ($break->break_end == null) {
$now = Carbon::now()->format('Y-m-d H:i:s');
$attendances = new Attendance();
$duration = $attendances->duration($break->break_start, $now);
}
}
//dd($now, $break->break_start, $duration);
// $duration for break timer is passed from AttendanceController
?>
@extends('app')
@@ -123,7 +113,7 @@ if (count($user->attendanceBreaks) > 0) {
@else
<div class="bg-white flex justify-flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<div class="ui-panel flex p-4 rounded-xl max-w-[350px] flex-col items-center">
<label class="text-xl font-bold pb-2">
@@ -169,33 +159,27 @@ if (count($user->attendanceBreaks) > 0) {
</div>
@else
{{-- دکمه‌های تب (نام ماه‌ها) --}}
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex overflow-x-auto"
style="margin-top: 0px; max-width: 350px">
<div class="ui-month-tabs">
@foreach($groupedData->keys() as $month)
<button class="tablinks rounded-t-2xl cursor-pointer px-4 py-2 hover:bg-gray-200 transition flex-shrink-0"
<button type="button" class="tablinks"
onclick="openCity(event, '{{$month}}')">
{{$month}}
</button>
@endforeach
</div>
{{-- محتوای تب‌ها (جدول‌ها) --}}
@foreach($groupedData as $month => $records)
<div id="{{$month}}"
class="tabcontent w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7 shadow-md">
<table class="table-auto"
style="width:100%; border-collapse: collapse; text-align: center">
<div id="{{$month}}" class="tabcontent ui-attendance-card">
<table class="ui-attendance-table">
<thead>
<tr style="border-bottom: 1px solid #ddd; background: rgba(244,244,244,0.82);"
class="border-1 border-slate-300">
<tr>
@if($_COOKIE['user_identity'] == 1370)
<th style="border-right: 1px solid #ddd; padding: 8px;">نام</th>
<th>نام</th>
@endif
<th style="border-right: 1px solid #ddd; padding: 4px;">تاریخ</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">ورود</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">خروج</th>
<th style="border-right: 1px solid #ddd; padding: 4px;">مدت</th>
<th>تاریخ</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
@@ -240,8 +224,7 @@ if (count($user->attendanceBreaks) > 0) {
{{-- ردیف جمع کل (اختیاری - چون متغیر آن موجود نبود کامنت کردم که خطا ندهد) --}}
<tr>
<td colspan="5" class="rounded-b-xl bg-gray-50 font-bold"
style="border-top: 1px solid #ddd; text-align: center; padding: 10px;">
<td colspan="5" class="table-summary">
تعداد رکورد: {{ count($records) }}
</td>
</tr>
@@ -265,10 +248,10 @@ if (count($user->attendanceBreaks) > 0) {
<!-- ATTENDANCE Modal -->
<div id="modal-container"
class="fixed inset-0 z-50 hidden bg-black bg-opacity-60 flex items-center justify-center p-4">
<div id="modal-content" class=" bg-white p-4 sm:p-6 rounded-lg shadow-lg max-w-sm w-full relative">
<h2 id="modal-title" class="text-xl sm:text-2xl font-semibold mb-4 text-center"></h2>
<p id="modal-body" class="text-gray-700 mb-4 text-center"></p>
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">
@@ -376,23 +359,18 @@ if (count($user->attendanceBreaks) > 0) {
{{-- START TASK MODAL --}}
<div id="taskModal"
class="hidden fixed inset-0 bg-black/40 items-start justify-center z-50 flex backdrop-blur-sm scroll-auto pt-10"
class="ui-modal-backdrop hidden"
onclick="closeOnOutsideClick(event)">
<div class="relative bg-white w-[350px] rounded-2xl shadow-lg p-5 text-center">
<button onclick="closeTaskModal()"
class="absolute top-3 right-3 text-gray-400 hover:text-gray-600 transition">
<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="text-lg font-bold mb-3" id="modalTitle">انتخاب وظیفه</h2>
<p class="text-gray-600 mb-4" id="modalDesc">یکی از وظایف زیر را انتخاب کنید:</p>
<input class="h-12 w-full bg-transparent pl-11 pr-4 text-sm text-custom-text-100
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2"
placeholder="جست و جو..." tabindex="1" id="headlessui-combobox-input-:r13m:" role="combobox"
aria-expanded="false" aria-autocomplete="list" data-headlessui-state="" type="text" value=""
aria-controls="headlessui-combobox-options-:r13n:"
aria-activedescendant="headlessui-combobox-option-:r13s:" data-sharkid="__4">
<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)
@@ -400,11 +378,9 @@ if (count($user->attendanceBreaks) > 0) {
<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=" overflow-y-scroll"
style="max-height: calc(100vh - 300px);">
<div class="ui-task-list">
@foreach($user->tasks as $item)
<div class="inline-flex items-center bg-gray-300 hover:bg-cyan-600 rounded-lg m-2 p-2 justify-between"
style="display: flex">
<label class="ui-task-item">
@if($user->active_task)
@if( $user->active_task == $item->title )
<input type="radio" name="task_id" checked="true"
@@ -425,8 +401,7 @@ if (count($user->attendanceBreaks) > 0) {
<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>
</div>
</label>
@endforeach
</div>
@@ -446,21 +421,17 @@ if (count($user->attendanceBreaks) > 0) {
</form>
@else
<div class="inline-flex justify-center items-center"
style="height: calc(100vh - 400px);">
<div class="flex justify-center items-center py-10">
<p class="font-bold text-gray-400">
وظیفه ای وجود ندارد
</p>
</div>
@endif
{{-- افزودن تسک--}}
<form>
<input class="h-12 w-[200px] bg-transparent pl-11 pr-4 text-sm text-custom-text-100
placeholder:text-custom-text-400 focus:ring-0 rounded-3xl border-2 border-gray-300 py-2"
placeholder="عنوان وظیفه" tabindex="1" id="taskTitle" role="combobox"
aria-expanded="false" aria-autocomplete="list" data-headlessui-state="" type="text" value=""
aria-controls="headlessui-combobox-options-:r13n:"
aria-activedescendant="headlessui-combobox-option-:r13s:" data-sharkid="__4">
<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>
@@ -468,10 +439,13 @@ if (count($user->attendanceBreaks) > 0) {
</form>
</div>
</div>
@endsection
</div>
@section('script')
@endsection
@section('script')
<script>
const existed = {{ $existed ? 'true' : 'false' }};
@@ -735,23 +709,21 @@ if (count($user->attendanceBreaks) > 0) {
}
//SEARCH BAR IN TASK MODAL
const searchInput = document.getElementById("headlessui-combobox-input-:r13m:");
const listItems = document.querySelectorAll('#taskModal .inline-flex.items-center');
const searchInput = document.getElementById("taskSearchInput");
const listItems = document.querySelectorAll('#taskModal .ui-task-item');
searchInput.addEventListener("input", function () {
const filter = searchInput.value.toLowerCase();
listItems.forEach(item => {
const nameDiv = item.querySelector('div');
if (nameDiv) {
const name = nameDiv.textContent.toLowerCase();
if (name.includes(filter)) {
item.style.display = 'flex';
} else {
item.style.display = 'none';
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');
+3 -4
View File
@@ -2,7 +2,8 @@
@section('content')
{{-- {{dd($user)}}--}}
<div class="w-full mt-[90px] justify-center container flex">
<div class="bg-white w-[350px] p-6 rounded-xl ">
<div class="ui-card">
<div class="ui-card-body">
<img src="/assets/img/bg/bg-shape.jpg" class="rounded-full w-32 h-32 mx-auto pb-6" alt="">
{{-- <p class=""> تاریخ شروع همکاری:--}}
{{-- {{\Morilog\Jalali\Jalalian::fromCarbon(\Carbon\Carbon::parse($user->created_at))->format('Y/m/d')}}</p>--}}
@@ -144,9 +145,7 @@
}
</script>
</div>
</div>
</div>
+50 -66
View File
@@ -3,14 +3,12 @@
use Morilog\Jalali\Jalalian;
use Carbon\Carbon;
//dd($data);
$monthlyTotals = [];
foreach ($data as $month => $attendances) {
$tsec = 0;
foreach ($attendances as $attendance) {
// اگر duration موجود باشه استفاده می‌کنیم
if (!empty($attendance->duration)) {
[$h, $m, $s] = explode(':', $attendance->duration);
$tsec += (int)$h * 3600 + (int)$m * 60 + (int)$s;
@@ -19,88 +17,74 @@ foreach ($data as $month => $attendances) {
$hours = floor($tsec / 3600);
$minutes = floor(($tsec % 3600) / 60);
$seconds = $tsec % 60;
// ذخیره در آرایه جدید با کل زمان ماه
$monthlyTotals[$month] = sprintf('%02d:%02d', $hours, $minutes);
}
//dd($monthlyTotals);
?>
@extends('app')
@section('content')
<div class="container inline-flex flex-col items-center justify-center ">
@section('content')
<div class="ui-page">
@if(empty($data))
<div class="mt-80 text-gray-500 text-xl font-bold italic">
<div class="ui-empty">
<h1>اطلاعاتی موجود نمیباشد</h1>
</div>
@else
<div class="tab rounded-t-2xl max-w-[350px] mx-auto flex"
style="margin-top: 80px; max-width: 350px">
@foreach(array_keys($data) as $month)
<button class="tablinks active rounded-t-2xl cursor-pointer" onclick="openCity(event, '{{$month}}')">{{$month}}</button>
@endforeach
</div>
@foreach($data as $month => $records)
<div id="{{$month}}" class="tabcontent w-[350px] bg-white rounded-b-xl rounded-tl-2xl cursor-default leading-7" style="">
<table class="" style="width:100%; border-collapse: collapse;text-align: center">
<thead>
<tr class="" style="border-bottom: 1px solid #ddd;">
<th style="border-right: 1px solid #ddd;">تاریخ</th>
<th style="border-right: 1px solid #ddd;">روز</th>
<th style="border-right: 1px solid #ddd;">ورود</th>
<th style="border-right: 1px solid #ddd;">خروج</th>
<th style="border-right: 1px solid #ddd;">مدت</th>
</tr>
</thead>
<tbody >
@foreach($records as $row)
<tr class="hover:bg-cyan-500" style="border-bottom: 1px solid #ddd;">
<td style="border-right: 1px solid #ddd;">{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ Jalalian::fromCarbon(\Carbon\Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ \Carbon\Carbon::parse($row->entered_at)->format('H:i') ?? '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ $row->exited_at ? \Carbon\Carbon::parse($row->exited_at)->format('H:i') : '-' }}</td>
<td style="border-right: 1px solid #ddd;">{{ $row->exited_at ? \Carbon\Carbon::parse($row->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
<tr>
<td colspan="5" class="hover:bg-cyan-500 rounded-b-xl" style="border-top: 1px solid #ddd; text-align: center; ">
مجموع کارکرد این ماه: {{$monthlyTotals[$month]}}
</td>
</tr>
</tbody>
</table>
<div class="ui-month-tabs" style="margin-top: 5rem;">
@foreach(array_keys($data) as $month)
<button type="button" class="tablinks" onclick="openCity(event, '{{ $month }}')">{{ $month }}</button>
@endforeach
</div>
@endforeach
@foreach($data as $month => $records)
<div id="{{ $month }}" class="tabcontent ui-attendance-card">
<table class="ui-attendance-table">
<thead>
<tr>
<th>تاریخ</th>
<th>روز</th>
<th>ورود</th>
<th>خروج</th>
<th>مدت</th>
</tr>
</thead>
<tbody>
@foreach($records as $row)
<tr>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('m/d') ?? '-' }}</td>
<td>{{ Jalalian::fromCarbon(Carbon::parse($row->entered_at))->format('%A') ?? '-' }}</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->duration)->format('H:i') : '-' }}</td>
</tr>
@endforeach
<tr>
<td colspan="5" class="table-summary">
مجموع کارکرد این ماه: {{ $monthlyTotals[$month] }}
</td>
</tr>
</tbody>
</table>
</div>
@endforeach
@endif
</div>
<script>
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", "");
}
document.getElementById(Month).style.display = "block";
evt.currentTarget.className += " active";
document.querySelectorAll('.tabcontent').forEach(el => el.style.display = 'none');
document.querySelectorAll('.tablinks').forEach(el => el.classList.remove('active'));
const panel = document.getElementById(Month);
if (panel) panel.style.display = 'block';
if (evt.currentTarget) evt.currentTarget.classList.add('active');
}
// باز کردن اولین تب به صورت پیش‌فرض
document.addEventListener("DOMContentLoaded", function () {
let firstTab = document.querySelector(".tablinks"); // اولین دکمه
if (firstTab) {
firstTab.click(); // همون اول کلیک مجازی بخوره
}
document.addEventListener('DOMContentLoaded', function () {
const firstTab = document.querySelector('.tablinks');
if (firstTab) firstTab.click();
});
</script>
@endsection
+6 -7
View File
@@ -19,11 +19,10 @@ if (isset($task)) {
?>
@extends('app')
@section('content')
<div class="w-full mt-[70px] justify-center container flex ">
<div class="bg-white w-[350px] rounded-xl ">
<h1 class="w-[350px] bg-cyan-500 rounded-t-xl text-center p-1.5 font-bold text-slate-50">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}
</h1>
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class=" p-6 grid-cols-2 gap-4 items-center">
<div class="w-full mt-[70px] justify-center container flex">
<div class="ui-card">
<div class="ui-card-header">{{isset($task) ? "ویرایش وظیفه" : "ایجاد وظیفه جدید"}}</div>
<form action="{{isset($task) ? Route('task.edit') : Route('task.create')}}" method="POST" class="ui-card-body grid grid-cols-2 gap-4 items-center">
@csrf
@if(isset($task))
<input type="hidden" id="id" name="id" value="{{$task->id}}">
@@ -87,7 +86,7 @@ if (isset($task)) {
<div class="relative" id="dropdown-container">
{{-- دکمه --}}
<button type="button" id="dropdown-btn"
class="w-full bg-white border border-gray-300 rounded-lg px-4 py-2 text-right flex justify-between items-center">
class="ui-input text-right flex justify-between items-center">
<span id="selected-count">انتخاب کاربران</span>
<svg class="w-4 h-4 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
@@ -96,7 +95,7 @@ if (isset($task)) {
</button>
<div id="dropdown-list"
class="hidden absolute z-20 w-full bg-white border border-gray-300 rounded-lg shadow-lg">
class="hidden absolute z-20 w-full ui-panel border border-gray-300 rounded-lg shadow-lg">
<div class="p-2 border-b">
<input type="text" id="search-input"
+499 -160
View File
@@ -64,22 +64,341 @@ $state = [
?>
@extends('app')
@section('content')
<div class="container w-full flex justify-center items-center flex-col ">
{{-- {{dd(count($tasks))}}--}}
@section('style')
<style>
.tasks-page {
width: 100%;
padding: 0 0.5rem 0;
}
.tasks-card {
background: var(--surface-glass);
backdrop-filter: blur(16px) saturate(160%);
-webkit-backdrop-filter: blur(16px) saturate(160%);
border: 1px solid var(--border-glass);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-soft);
overflow: hidden;
}
.tasks-filter {
margin-bottom: 0.75rem;
padding: 0.875rem 1rem;
background: var(--surface-glass);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
border: 1px solid var(--border-glass);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-soft);
}
.tasks-filter select {
border-radius: 0.875rem;
border: 1px solid var(--border-input);
background: var(--surface-input);
color: var(--text-primary);
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.tasks-filter select:focus {
outline: none;
border-color: color-mix(in srgb, var(--primary-color) 55%, white);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color) 22%, transparent);
}
.tasks-tabs {
display: flex;
flex-wrap: nowrap;
gap: 0.35rem;
padding: 0.5rem;
background: var(--surface-tabs);
border-bottom: 1px solid var(--border-input);
}
.task-tab {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding: 0.55rem 0.4rem;
border-radius: 0.875rem;
font-size: 0.8rem;
font-weight: 600;
color: var(--text-muted);
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s ease;
text-align: center;
}
.task-tab:hover {
color: var(--text-secondary);
background: var(--surface-tab-hover);
}
.task-tab-active {
color: color-mix(in srgb, var(--primary-color) 82%, var(--text-primary));
background: var(--surface-tab-active);
box-shadow: var(--shadow-soft);
}
.task-tab-label {
width: 100%;
font-size: 0.72rem;
line-height: 1.25;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.task-tab-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 1.35rem;
padding: 0.12rem 0.45rem;
border-radius: 9999px;
font-size: 0.68rem;
font-weight: 700;
line-height: 1;
background: var(--badge-bg);
color: var(--badge-text);
}
.task-tab-active .task-tab-count {
background: color-mix(in srgb, var(--primary-color) 18%, var(--surface-tab-active));
color: color-mix(in srgb, var(--primary-color) 70%, var(--text-primary));
}
.tasks-table-wrap {
padding: 0.5rem 0.5rem 0.75rem;
}
.tasks-cards-list {
display: flex;
flex-direction: column;
gap: 0.55rem;
}
.task-card {
display: block;
width: 100%;
padding: 0.85rem 0.9rem;
background: var(--surface-card);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-lg);
color: var(--text-primary);
cursor: pointer;
text-align: right;
transition: background-color 0.2s ease, box-shadow 0.2s ease, transform 0.15s ease;
}
.task-card:hover {
background: var(--surface-card-hover);
box-shadow: var(--row-shadow);
}
.task-card.is-active {
border-inline-start: 4px solid var(--primary-color);
padding-inline-start: calc(0.9rem - 3px);
}
.task-card-title-row {
display: flex;
align-items: flex-start;
gap: 0.5rem;
margin-bottom: 0.35rem;
}
.task-card-icon {
flex-shrink: 0;
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.25rem;
height: 1.25rem;
margin-top: 0.1rem;
}
.task-card-title {
margin: 0;
font-size: 0.92rem;
font-weight: 800;
line-height: 1.45;
color: var(--text-primary);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-desc {
margin: 0 0 0.65rem;
padding-inline-start: 1.75rem;
font-size: 0.78rem;
line-height: 1.55;
color: var(--text-muted);
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.task-card-fields {
margin: 0;
padding: 0.55rem 0 0;
border-top: 1px solid var(--border-subtle);
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.task-card-field {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
font-size: 0.78rem;
}
.task-card-label {
flex-shrink: 0;
color: var(--text-muted);
font-weight: 600;
}
.task-card-value {
color: var(--text-secondary);
text-align: left;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.task-card-value.font-mono {
font-variant-numeric: tabular-nums;
}
.tasks-empty {
padding: 2rem 1rem;
text-align: center;
color: var(--text-faint);
font-size: 0.9rem;
}
#taskDetailsModal {
backdrop-filter: blur(6px);
-webkit-backdrop-filter: blur(6px);
}
#taskDetailsModal > div {
background: var(--modal-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--modal-border);
box-shadow: var(--modal-shadow);
color: var(--text-primary);
}
#taskDetailsModal .modal-tab-btn-active {
background: color-mix(in srgb, var(--primary-color) 18%, var(--surface-tab-active)) !important;
color: color-mix(in srgb, var(--primary-color) 82%, var(--text-primary)) !important;
}
[data-theme="dark"] #taskDetailsModal {
background-color: rgba(2, 6, 23, 0.72) !important;
}
[data-theme="dark"] .task-card svg [stroke="#60646C"] {
stroke: #c5d0e0 !important;
}
</style>
@endsection
@section('content')
<script>
window.tasksById = @json($tasksPayload ?? []);
function switchTaskTab(tabId) {
document.querySelectorAll('.task-table-container').forEach(el => el.classList.add('hidden'));
const panel = document.getElementById('tab-' + tabId);
if (panel) panel.classList.remove('hidden');
document.querySelectorAll('.task-tab').forEach(tab => tab.classList.remove('task-tab-active'));
const activeTab = document.querySelector('.task-tab[data-tab="' + tabId + '"]');
if (activeTab) activeTab.classList.add('task-tab-active');
}
function updateTaskTabCounts() {
document.querySelectorAll('.task-tab').forEach(tab => {
const tabId = tab.dataset.tab;
const panel = document.getElementById('tab-' + tabId);
if (!panel) return;
const visibleCount = panel.querySelectorAll('.task-row:not(.hidden)').length;
const badge = tab.querySelector('.task-tab-count');
if (badge) badge.textContent = visibleCount;
});
}
function filterTasksByAssignee(assigneeId) {
document.querySelectorAll('.task-row').forEach(row => {
if (!assigneeId) {
row.classList.remove('hidden');
return;
}
let ids = [];
try {
ids = JSON.parse(row.dataset.assigneeIds || '[]');
} catch (e) {
ids = [];
}
row.classList.toggle('hidden', !ids.includes(String(assigneeId)));
});
document.querySelectorAll('.task-table-container').forEach(panel => {
const emptyMsg = panel.querySelector('.task-filter-empty');
const visibleRows = panel.querySelectorAll('.task-row:not(.hidden)').length;
const hasRows = panel.querySelectorAll('.task-row').length > 0;
if (emptyMsg) {
emptyMsg.classList.toggle('hidden', !assigneeId || !hasRows || visibleRows > 0);
}
});
updateTaskTabCounts();
}
</script>
<div class="container tasks-page w-full flex justify-center items-center flex-col">
@if($count > 0)
<div class="w-full max-w-6xl mx-auto mt-10 px-2">
<div class="flex flex-nowrap gap-1 border-b border-gray-200">
<div class="w-full max-w-6xl mx-auto mt-4">
@if(!empty($assigneeFilterOptions))
<div class="tasks-filter flex flex-wrap items-center gap-3">
<label for="assigneeFilter" class="text-sm font-medium text-gray-700 whitespace-nowrap">
فیلتر بر اساس مسئول:
</label>
<select id="assigneeFilter"
onchange="filterTasksByAssignee(this.value)"
class="flex-1 min-w-[180px] px-3 py-2 text-sm text-gray-800">
<option value="">همه مسئولین</option>
@foreach($assigneeFilterOptions as $option)
<option value="{{ $option['id'] }}">{{ $option['name'] }}</option>
@endforeach
</select>
</div>
@endif
<div class="tasks-card mt-3">
<div class="tasks-tabs">
@foreach(array_keys($tasks) as $stateKey)
<button onclick="switchTaskTab('{{ $stateKey }}')"
class="task-tab px-4 py-2 rounded-t-lg font-medium text-sm transition
{{ $loop->first ? 'bg-white text-blue-600 border-t border-x border-gray-200' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100' }}"
<button type="button" onclick="switchTaskTab('{{ $stateKey }}')"
class="task-tab {{ $loop->first ? 'task-tab-active' : '' }}"
data-tab="{{ $stateKey }}">
{{ $stateKey }}
<span class="mr-1 text-xs bg-gray-200 text-gray-700 px-2 py-0.5 rounded-full">
{{ count($tasks[$stateKey] ?? []) }}
</span>
<span class="task-tab-label">{{ $stateKey }}</span>
<span class="task-tab-count">{{ count($tasks[$stateKey] ?? []) }}</span>
</button>
@endforeach
</div>
@@ -87,142 +406,90 @@ $state = [
{{-- جداول هر وضعیت --}}
@foreach($tasks as $stateKey => $stateTasks)
<div id="tab-{{ $stateKey }}"
class="task-table-container {{ $loop->first ? '' : 'hidden' }} overflow-x-auto bg-white rounded-b-lg shadow">
class="task-table-container tasks-table-wrap {{ $loop->first ? '' : 'hidden' }}">
@if(count($stateTasks) > 0)
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">عنوان
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">
اولویت
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">{{ $isAdminLevel1 ? 'نام اپ' : 'تاریخ شروع' }}
</th>
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">مهلت
</th>
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">جزئیات</th>--}}
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">مدت زمان</th>--}}
{{-- <th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">عملیات</th>--}}
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
<div class="tasks-cards-list">
@foreach($stateTasks as $task)
@php
// دریافت نام ایجادکننده (با فرض وجود رابطه 'creator' در مدل Tasks)
$creatorName = 'نامشخص';
if ($task->relationLoaded('creator') && $task->creator) {
$creatorName = $task->creator->name;
} elseif ($task->created_by) {
// اگر رابطه لود نشده، یک بار کاربر را پیدا کن (توصیه می‌شود در کنترلر eager load شود)
$user = \App\Models\User::find($task->created_by);
$creatorName = $user ? $user->name : $task->created_by;
}
$task->creator_name = $creatorName;
// آیکون وضعیت
$stateIcon = '';
switch($task->state){
case 1: $stateIcon = $backlog; break;
case 2: $stateIcon = $todo; break;
case 3: $stateIcon = $in_progress; break;
case 4: $stateIcon = $done; break;
default: $stateIcon = '';
}
// تعیین وضعیت شروع/پایان برای دکمه
$started_at = $lastDuration?->start ?? null;
$ended_at = $lastDuration?->end ?? null;
$actionType = $ended_at ? 'start' : ($started_at ? 'end' : 'start');
$p_id = $lastDuration?->id ?? '';
$stateIcon = match((int)$task->state) {
1 => $backlog,
2 => $todo,
3 => $in_progress,
4 => $done,
default => '',
};
@endphp
{{--@if($_COOKIE['user_identity'] == 'naser_me') {{dump($task)}} @endif--}}
<tr class="hover:bg-gray-50">
<td class="px-2 py-2 {{ $task->active ? 'border-s-4 border-cyan-400 ' : '' }}">
<div class="flex items-center gap-2">
<span class="inline-flex items-center justify-center w-5 h-5">{!! $stateIcon !!}</span>
<div>
<div class="font-small text-gray-900 text-sm font-bold">{{ $task->title }}</div>
@if($task->description)
<div class="text-xs text-gray-500 mt-1 line-clamp-2">{{ \Illuminate\Support\Str::limit(strip_tags($task->description), 50) }}</div>
@endif
</div>
</div>
</td>
<td class="px-2 py-2">
<span class="px-2 py-1 text-xs rounded-full {{ $priorityColors[$task->priority] ?? 'bg-gray-100 text-gray-800' }}">
{{ $priorityLabels[$task->priority] ?? $task->priority }}
</span>
</td>
<td class="px-2 py-2 text-sm text-gray-600">
{{ $isAdminLevel1 ? $task->appName->name_fa ?? null : Jalalian::fromCarbon(Carbon::parse($task->start_date))->format('Y/m/d') }}
</td>
<td class="px-2 py-2 text-sm text-gray-600">
{{ $task->target_date ? Jalalian::fromCarbon(Carbon::parse($task->target_date))->format('Y/m/d') : '-' }}
</td>
{{-- <td class="px-2 py-2 text-sm text-gray-600 font-mono">--}}
{{-- {{ $totalDuration }}--}}
{{-- </td>--}}
{{-- <td class="px-2 py-2">--}}
{{-- <button onclick="openTaskModal(--}}
{{-- '{{ $task->id }}',--}}
{{-- '{{ $p_id }}',--}}
{{-- '{{ $started_at }}',--}}
{{-- '{{ $ended_at }}',--}}
{{-- '{{ $actionType }}'--}}
{{-- )" class="text-sm bg-blue-50 text-blue-700 px-3 py-1.5 rounded-lg hover:bg-blue-100 transition flex items-center gap-1">--}}
{{-- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>--}}
{{-- </svg>--}}
{{-- {{ $actionType == 'start' ? 'شروع' : 'پایان' }}--}}
{{-- </button>--}}
{{-- </td>--}}
{{-- ستون جزئیات --}}
<td class="px-2 py-2">
<button onclick='openTaskDetailsModal(@json($task))'
class="text-sm bg-gray-50 text-gray-700 px-3 py-1.5 rounded-lg hover:bg-gray-100 transition flex items-center gap-1">
<svg class="w-4 h-4" fill="none" stroke="currentColor"
viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
<path stroke-linecap="round" stroke-linejoin="round"
stroke-width="2"
d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
</svg>
</button>
</td>
<article class="task-card task-row {{ $task->active ? 'is-active' : '' }}"
data-task-id="{{ $task->id }}"
data-assignee-ids='@json($task->assignee_ids ?? [])'
onclick="openTaskDetailsModal({{ $task->id }})"
role="button"
tabindex="0"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();openTaskDetailsModal({{ $task->id }})}">
<div class="task-card-title-row">
<span class="task-card-icon">{!! $stateIcon !!}</span>
<h3 class="task-card-title">{{ $task->title }}</h3>
</div>
{{-- ستون زمان (همان دکمه شروع/پایان قبلی) --}}
{{-- <td class="px-2 py-2">--}}
{{-- <button onclick="openTaskTimeModal(--}}
{{-- '{{ $task->id }}',--}}
{{-- '{{ $p_id }}',--}}
{{-- '{{ $started_at }}',--}}
{{-- '{{ $ended_at }}',--}}
{{-- '{{ $actionType }}'--}}
{{-- )" class="text-sm bg-blue-50 text-blue-700 px-3 py-1.5 rounded-lg hover:bg-blue-100 transition flex items-center gap-1">--}}
{{-- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">--}}
{{-- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>--}}
{{-- </svg>--}}
{{-- {{ $actionType == 'start' ? 'شروع' : 'پایان' }}--}}
{{-- </button>--}}
{{-- </td>--}}
</tr>
@if($task->description)
<p class="task-card-desc">{{ strip_tags($task->description) }}</p>
@endif
<dl class="task-card-fields">
<div class="task-card-field">
<dt class="task-card-label">اولویت</dt>
<dd class="task-card-value">
<span class="px-2 py-0.5 text-xs rounded-full {{ $priorityColors[$task->priority] ?? 'bg-gray-100 text-gray-800' }}">
{{ $priorityLabels[$task->priority] ?? $task->priority }}
</span>
</dd>
</div>
@if($isAdminLevel1)
<div class="task-card-field">
<dt class="task-card-label">نام اپ</dt>
<dd class="task-card-value">{{ $task->appName->name_fa ?? '-' }}</dd>
</div>
@endif
@if($stateKey === 'done')
<div class="task-card-field">
<dt class="task-card-label">زمان اجرا</dt>
<dd class="task-card-value font-mono">{{ $task->total_duration ?? '00:00:00' }}</dd>
</div>
@else
<div class="task-card-field">
<dt class="task-card-label">مهلت</dt>
<dd class="task-card-value">{{ $task->target_date ? Jalalian::fromCarbon(Carbon::parse($task->target_date))->format('Y/m/d') : '-' }}</dd>
</div>
@endif
@if(!empty($task->assignee_names))
<div class="task-card-field">
<dt class="task-card-label">مسئولین</dt>
<dd class="task-card-value">{{ implode('، ', $task->assignee_names) }}</dd>
</div>
@endif
</dl>
</article>
@endforeach
</tbody>
</table>
</div>
<div class="task-filter-empty hidden tasks-empty">
تسکی برای این مسئول در این وضعیت وجود ندارد.
</div>
@else
<div class="p-8 text-center text-gray-400 bg-white rounded-b-lg">
<div class="tasks-empty">
هیچ وظیفه‌ای در این وضعیت وجود ندارد.
</div>
@endif
</div>
@endforeach
</div>
</div>
@else
<div class=" mt-[200px]">
<h1 class=" text-xl font-bold" style="color: #959595">وظیفه ای ندارید</h1>
<h1 class="text-xl font-bold text-gray-500">وظیفه ای ندارید</h1>
</div>
@endif
</div>
@@ -230,12 +497,13 @@ $state = [
{{-- START TASK DETAILS MODAL --}}
<div id="taskDetailsModal" class="fixed inset-0 bg-black/40 hidden items-center justify-center z-50 p-4">
<div class="bg-white w-full max-w-md rounded-2xl shadow-xl max-h-[90vh] overflow-y-auto">
<div id="taskDetailsModal" class="fixed inset-0 bg-slate-900/35 hidden items-center justify-center z-50 p-4">
<div class="w-full max-w-md rounded-2xl max-h-[90vh] overflow-y-auto">
{{-- هدر مودال --}}
<div class="flex justify-between items-center p-4 border-b border-gray-200">
<div class="flex justify-between items-center p-4 border-b border-slate-200/70">
<h2 class="text-lg font-bold text-gray-800" id="modalTaskTitle">عنوان تسک</h2>
<button onclick="closeDetailsModal()" class="text-gray-400 hover:text-gray-600">
<button onclick="closeDetailsModal()"
class="w-9 h-9 inline-flex items-center justify-center rounded-full text-gray-400 hover:text-gray-600 hover:bg-slate-100 transition">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M6 18L18 6M6 6l12 12"></path>
@@ -245,7 +513,7 @@ $state = [
<div class="p-4 space-y-4">
{{-- Tabs --}}
<div class="flex gap-2 bg-white sticky top-0 z-10">
<div class="flex gap-2 sticky top-0 z-10" style="background: var(--modal-bg);">
<button id="modalTabBtn-info" type="button"
onclick="switchDetailsTab('info')"
class="flex-1 px-3 py-2 text-sm rounded-lg bg-gray-100 text-gray-800">
@@ -324,6 +592,14 @@ $state = [
<h3 class="text-sm font-medium text-gray-500 mb-1">ایجادکننده</h3>
<p class="text-gray-800 text-sm" id="modalTaskCreator">-</p>
</div>
<div>
<h3 class="text-sm font-medium text-gray-500 mb-1">تاریخ ایجاد</h3>
<p class="text-gray-800 text-sm" id="modalTaskCreatedAt">-</p>
</div>
<div class="col-span-2">
<h3 class="text-sm font-medium text-gray-500 mb-1">مسئولین</h3>
<p class="text-gray-800 text-sm" id="modalTaskAssignees">-</p>
</div>
</div>
<div class="flex gap-2 pt-2">
@@ -389,7 +665,7 @@ $state = [
placeholder="مثلاً: @ali لطفاً این بخش رو بررسی کن" required></textarea>
<div id="mentionDropdown"
class="hidden absolute z-50 w-full mt-1 bg-white border border-gray-200 rounded-lg shadow-lg max-h-48 overflow-y-auto">
class="hidden absolute z-50 w-full mt-1 ui-panel border border-gray-200 rounded-lg shadow-lg max-h-48 overflow-y-auto">
</div>
</div>
@@ -409,6 +685,7 @@ $state = [
<script>
const mentionUsers = @json($mentionUsers);
const appsList = @json($appsList);
const tasksById = window.tasksById || {};
const isAdminLevel1 = @json($isAdminLevel1);
const currentUserId = @json($user_id ?? null);
let currentTask = {};
@@ -493,21 +770,61 @@ $state = [
document.getElementById(elementId).classList.toggle('hidden');
}
// تابع سوییچ تب‌های جدول
function switchTaskTab(tabId) {
// مخفی کردن همه جداول
document.querySelectorAll('.task-table-container').forEach(el => el.classList.add('hidden'));
// نمایش جدول انتخاب شده
document.getElementById('tab-' + tabId).classList.remove('hidden');
function setTaskDetailsLoading() {
const commentsList = document.getElementById('modalCommentsList');
const durationsList = document.getElementById('modalDurationsList');
const loadingHtml = '<div class="text-sm text-gray-400 text-center">در حال بارگذاری...</div>';
if (commentsList) commentsList.innerHTML = loadingHtml;
if (durationsList) durationsList.innerHTML = loadingHtml;
}
// تغییر استایل تب‌های فعال
document.querySelectorAll('.task-tab').forEach(tab => {
tab.classList.remove('bg-white', 'text-blue-600', 'border-t', 'border-x', 'border-gray-200');
tab.classList.add('text-gray-500', 'hover:text-gray-700', 'hover:bg-gray-100');
});
const activeTab = document.querySelector(`.task-tab[data-tab="${tabId}"]`);
activeTab.classList.add('bg-white', 'text-blue-600', 'border-t', 'border-x', 'border-gray-200');
activeTab.classList.remove('text-gray-500', 'hover:text-gray-700', 'hover:bg-gray-100');
function fetchTaskDetails(taskId) {
setTaskDetailsLoading();
return fetch('/task/' + taskId + '/details', {
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
},
})
.then(res => {
if (!res.ok) {
throw new Error('HTTP ' + res.status);
}
return res.json();
})
.then(data => {
if (data.error) {
throw new Error(data.error);
}
Object.assign(currentTaskDetails, data);
tasksById[taskId] = currentTaskDetails;
tasksById[String(taskId)] = currentTaskDetails;
window.tasksById = tasksById;
updateModalInfoFields(currentTaskDetails);
renderTaskComments(currentTaskDetails);
renderTaskDurations(currentTaskDetails);
updateModalTotalDuration(currentTaskDetails);
const durationBtn = document.getElementById('modalDurationActionBtn');
if (durationBtn) {
const actionType = currentTaskDetails.active ? 'end' : 'start';
durationBtn.textContent = actionType === 'start' ? 'شروع' : 'پایان';
durationBtn.className = 'flex-1 px-4 py-2 rounded-lg font-medium text-white '
+ (actionType === 'start' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-green-600 hover:bg-green-700')
+ ' transition';
}
})
.catch(err => {
console.error(err);
const errorHtml = '<div class="text-sm text-red-500 text-center">خطا در بارگذاری جزئیات</div>';
const commentsList = document.getElementById('modalCommentsList');
const durationsList = document.getElementById('modalDurationsList');
if (commentsList) commentsList.innerHTML = errorHtml;
if (durationsList) durationsList.innerHTML = errorHtml;
});
}
// مقداردهی اولیه (در صورت نیاز)
@@ -541,7 +858,7 @@ $state = [
const created = c.created_at ? new Date(c.created_at).toLocaleString('fa-IR') : '';
const body = escapeHtml(c.body).replace(/\n/g, '<br/>');
return `
<div class="bg-white border border-gray-200 rounded-lg p-2">
<div class="ui-panel border border-gray-200 rounded-lg p-2">
<div class="text-xs text-gray-500 flex justify-between">
<span>${escapeHtml(author)}</span>
<span>${escapeHtml(created)}</span>
@@ -615,7 +932,7 @@ $state = [
const end = d.end ? new Date(d.end).toLocaleString('fa-IR') : 'در حال انجام';
const dur = d.duration || (d.end ? '-' : '...');
return `
<div class="bg-white border border-gray-200 rounded-lg p-2">
<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>
<span class="font-mono">${escapeHtml(dur)}</span>
@@ -636,6 +953,7 @@ $state = [
const btn = document.getElementById(`modalTabBtn-${t}`);
if (el) el.classList.toggle('hidden', t !== tab);
if (btn) {
btn.classList.toggle('modal-tab-btn-active', t === tab);
btn.classList.toggle('bg-gray-100', t === tab);
btn.classList.toggle('text-gray-800', t === tab);
btn.classList.toggle('text-gray-500', t !== tab);
@@ -738,8 +1056,30 @@ $state = [
});
}
function formatAssigneeNames(task) {
const names = Array.isArray(task?.assignee_names) ? task.assignee_names.filter(Boolean) : [];
return names.length ? names.join('، ') : '-';
}
function updateModalInfoFields(task) {
document.getElementById('modalTaskCreator').textContent = task.creator_name || '-';
document.getElementById('modalTaskCreatedAt').textContent = task.created_at
? formatDateTime(task.created_at)
: '-';
document.getElementById('modalTaskAssignees').textContent = formatAssigneeNames(task);
}
// باز کردن مودال جزئیات تسک
function openTaskDetailsModal(task) {
function openTaskDetailsModal(taskOrId) {
const task = typeof taskOrId === 'object'
? taskOrId
: tasksById[taskOrId] ?? tasksById[String(taskOrId)];
if (!task) {
alert('اطلاعات این وظیفه در دسترس نیست.');
return;
}
currentTaskDetails = task;
// پر کردن فیلدهای مودال
@@ -747,7 +1087,7 @@ $state = [
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) : '-';
document.getElementById('modalTaskCreator').textContent = task.creator_name || '-';
updateModalInfoFields(task);
// اپ
const appNameEl = document.getElementById('modalTaskAppName');
@@ -902,11 +1242,10 @@ $state = [
switchDetailsTab('info');
setQuickEditMode(false);
renderTaskComments(task);
renderTaskDurations(task);
updateModalTotalDuration(task);
setTaskDetailsLoading();
setupCommentForm(task);
setupMentionAutocomplete();
fetchTaskDetails(task.id);
}
function setQuickEditMode(on) {
@@ -1089,9 +1428,9 @@ $state = [
function closeDetailsModal() {
const modal = document.getElementById('taskDetailsModal');
if (!modal) return;
modal.classList.add('hidden');
modal.style.display = 'none';
window.location.reload();
}
// فرمت تاریخ شمسی (ساده)