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
+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();
}
// فرمت تاریخ شمسی (ساده)