1826 lines
88 KiB
PHP
Executable File
1826 lines
88 KiB
PHP
Executable File
<?php
|
|
|
|
use Morilog\Jalali\Jalalian;
|
|
use Carbon\Carbon;
|
|
use App\Models\User;
|
|
use App\Models\App;
|
|
|
|
|
|
$backlog = <<<SVG
|
|
<svg height="12px" width="12px" viewBox="0 0 16 16" class="flex-shrink-0 flex-shrink-0 size-4" xmlns="http://www.w3.org/2000/svg"><g>
|
|
<g transform="translate(8 8) rotate(-90)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(-66)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(-42)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(-18)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(6)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(30)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(54)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(78)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(102)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(126)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(150)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(174)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(198)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(222)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g><g transform="translate(8 8) rotate(246)"><line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#60646C" stroke-width="1.21" stroke-linecap="round">
|
|
</line></g></g>
|
|
</svg>
|
|
SVG;
|
|
$todo = <<<SVG
|
|
<svg width="12px" height="12px" viewBox="0 0 16 16" class="flex-shrink-0 flex-shrink-0 size-4"><g></g>
|
|
<circle cx="8" cy="8" r="6" fill="none" stroke="#60646C" stroke-width="1.5" stroke-dasharray="37.69911184307752" stroke-dashoffset="0" stroke-linecap="round" transform="rotate(-90 8 8)">
|
|
</circle></svg>
|
|
SVG;
|
|
$error = <<<SVG
|
|
<svg width="12px" height="12px" viewBox="0 0 16 16" class="flex-shrink-0 flex-shrink-0 size-4"><g><g transform="translate(8 8) rotate(102)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(126)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(150)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(174)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(198)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(222)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g><g transform="translate(8 8) rotate(246)">
|
|
<line x1="5.75" y1="0" x2="6.5" y2="0" stroke="#eb144c" stroke-width="1.21" stroke-linecap="round"></line></g></g><circle cx="6" cy="6" r="3" stroke-width="1.5" stroke-linecap="round" fill="none" transform="rotate(-90 8 6)" stroke-dasharray="37.69911184307752" stroke-dashoffset="0" stroke="#eb144c">
|
|
</circle><circle cx="8" cy="8" r="6" fill="none" stroke="#eb144c" stroke-width="1.5" stroke-dasharray="37.69911184307752" stroke-dashoffset="18.84955592153876" stroke-linecap="round" transform="rotate(-90 8 8)"></circle></svg>
|
|
SVG;
|
|
$in_progress = <<<SVG
|
|
<svg width="12px" height="12px" viewBox="0 0 16 16" class="flex-shrink-0 flex-shrink-0 size-4"><g></g><circle cx="6" cy="6" r="3" stroke-width="1.5" stroke-linecap="round" fill="none" transform="rotate(-90 8 6)" stroke-dasharray="37.69911184307752" stroke-dashoffset="0" stroke="#F59E0B"></circle><circle cx="8" cy="8" r="6" fill="none" stroke="#F59E0B" stroke-width="1.5" stroke-dasharray="37.69911184307752" stroke-dashoffset="0" stroke-linecap="round" transform="rotate(-90 8 8)"></circle></svg>
|
|
SVG;
|
|
$done = <<<SVG
|
|
<svg height="12px" width="12px" class="flex-shrink-0 flex-shrink-0 size-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" percentage="1"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM11.3587 6.18828C11.6007 5.85214 11.5244 5.38343 11.1882 5.14141C10.8521 4.89938 10.3834 4.97568 10.1414 5.31183L7.03706 9.62335L5.25956 7.97751C4.95563 7.69609 4.4811 7.71434 4.19968 8.01828C3.91826 8.32221 3.93651 8.79673 4.24045 9.07815L6.64045 11.3004C6.79816 11.4464 7.01095 11.5178 7.22481 11.4963C7.43868 11.4749 7.63307 11.3627 7.75865 11.1883L11.3587 6.18828Z" fill="#46A758"></path></svg>
|
|
SVG;
|
|
$cancelled = <<<SVG
|
|
<svg height="12px" width="12px" class="flex-shrink-0 flex-shrink-0 size-4" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" percentage="1"><path fill-rule="evenodd" clip-rule="evenodd" d="M8 15C11.866 15 15 11.866 15 8C15 4.13401 11.866 1 8 1C4.13401 1 1 4.13401 1 8C1 11.866 4.13401 15 8 15ZM11.1018 4.89826C11.3947 5.19115 11.3947 5.66603 11.1018 5.95892L9.06068 8.00002L11.1018 10.0411C11.3947 10.334 11.3947 10.8089 11.1018 11.1018C10.8089 11.3947 10.334 11.3947 10.0411 11.1018L8.00002 9.06068L5.95892 11.1018C5.66603 11.3947 5.19115 11.3947 4.89826 11.1018C4.60537 10.8089 4.60537 10.334 4.89826 10.0411L6.93936 8.00002L4.89826 5.95892C4.60537 5.66603 4.60537 5.19115 4.89826 4.89826C5.19115 4.60537 5.66603 4.60537 5.95892 4.89826L8.00002 6.93936L10.0411 4.89826C10.334 4.60537 10.8089 4.60537 11.1018 4.89826Z" fill="#9AA4BC"></path></svg>
|
|
SVG;
|
|
|
|
$state = [
|
|
1 => "$backlog",//backlog
|
|
2 => "$todo",//to do
|
|
3 => "$in_progress",// in progress
|
|
4 => "$done",//done
|
|
];
|
|
|
|
//$p_tasks = Pivot_Tasks::get();
|
|
|
|
|
|
?>
|
|
@extends('app')
|
|
|
|
@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-filters {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.tasks-filters .tasks-filter {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.tasks-filter-date {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
gap: 0.65rem;
|
|
}
|
|
|
|
.tasks-filter-date-head {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.tasks-filter-date-head select {
|
|
flex: 1;
|
|
min-width: 140px;
|
|
}
|
|
|
|
.tasks-filter-date-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
margin-top: 0.45rem;
|
|
}
|
|
|
|
.tasks-filter-date-controls {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.tasks-filter select,
|
|
.tasks-filter input[type="text"] {
|
|
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-date-field {
|
|
flex: 1 1 7rem;
|
|
min-width: 7rem;
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.tasks-filter-date-type {
|
|
min-width: 140px;
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
.tasks-filter-clear {
|
|
padding: 0.5rem 0.75rem;
|
|
font-size: 0.8125rem;
|
|
border-radius: 0.875rem;
|
|
border: 1px solid var(--border-input);
|
|
background: var(--surface-tabs);
|
|
color: var(--text-secondary);
|
|
white-space: nowrap;
|
|
cursor: pointer;
|
|
transition: background-color 0.2s ease, color 0.2s ease;
|
|
}
|
|
|
|
.tasks-filter-clear:hover {
|
|
background: var(--surface-tab-hover);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.tasks-filter select:focus,
|
|
.tasks-filter input[type="text"]: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 applyTaskFilters() {
|
|
const assigneeId = document.getElementById('assigneeFilter')?.value || '';
|
|
const dateField = document.getElementById('dateFilterField')?.value || 'target_date';
|
|
const fromDate = document.getElementById('dateFilterFromGregorian')?.value || '';
|
|
const toDate = document.getElementById('dateFilterToGregorian')?.value || '';
|
|
const hasDateFilter = Boolean(fromDate || toDate);
|
|
const dateAttrMap = {
|
|
target_date: 'targetDate',
|
|
start_date: 'startDate',
|
|
finished_at: 'finishedDate',
|
|
created_at: 'createdDate',
|
|
};
|
|
const dateAttr = dateAttrMap[dateField] || 'targetDate';
|
|
const hasAnyFilter = Boolean(assigneeId || hasDateFilter);
|
|
|
|
document.querySelectorAll('.task-row').forEach(row => {
|
|
let visible = true;
|
|
|
|
if (assigneeId) {
|
|
let ids = [];
|
|
try {
|
|
ids = JSON.parse(row.dataset.assigneeIds || '[]');
|
|
} catch (e) {
|
|
ids = [];
|
|
}
|
|
visible = ids.includes(String(assigneeId));
|
|
}
|
|
|
|
if (visible && hasDateFilter) {
|
|
const taskDate = row.dataset[dateAttr] || '';
|
|
if (!taskDate) {
|
|
visible = false;
|
|
} else {
|
|
if (fromDate && taskDate < fromDate) visible = false;
|
|
if (toDate && taskDate > toDate) visible = false;
|
|
}
|
|
}
|
|
|
|
row.classList.toggle('hidden', !visible);
|
|
});
|
|
|
|
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', !hasAnyFilter || !hasRows || visibleRows > 0);
|
|
}
|
|
});
|
|
|
|
updateTaskTabCounts();
|
|
}
|
|
|
|
function filterTasksByAssignee() {
|
|
applyTaskFilters();
|
|
}
|
|
|
|
function clearTaskDateFilter() {
|
|
const fromInput = document.getElementById('dateFilterFrom');
|
|
const toInput = document.getElementById('dateFilterTo');
|
|
const fromHidden = document.getElementById('dateFilterFromGregorian');
|
|
const toHidden = document.getElementById('dateFilterToGregorian');
|
|
|
|
if (fromInput) fromInput.value = '';
|
|
if (toInput) toInput.value = '';
|
|
if (fromHidden) fromHidden.value = '';
|
|
if (toHidden) toHidden.value = '';
|
|
|
|
applyTaskFilters();
|
|
}
|
|
</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-4">
|
|
<div class="tasks-filters">
|
|
@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="applyTaskFilters()"
|
|
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-filter tasks-filter-date">
|
|
<div class="tasks-filter-date-head">
|
|
<label for="dateFilterField" class="text-sm font-medium text-gray-700 whitespace-nowrap">
|
|
فیلتر بر اساس تاریخ:
|
|
</label>
|
|
<select id="dateFilterField"
|
|
onchange="applyTaskFilters()"
|
|
class="tasks-filter-date-type">
|
|
<option value="target_date">مهلت</option>
|
|
<option value="start_date">تاریخ شروع</option>
|
|
<option value="finished_at">تاریخ اتمام</option>
|
|
<option value="created_at">تاریخ ایجاد</option>
|
|
</select>
|
|
</div>
|
|
<div class="tasks-filter-date-row">
|
|
<input type="text"
|
|
id="dateFilterFrom"
|
|
class="tasks-filter-date-field"
|
|
placeholder="از تاریخ"
|
|
autocomplete="off"
|
|
readonly>
|
|
<input type="text"
|
|
id="dateFilterTo"
|
|
class="tasks-filter-date-field"
|
|
placeholder="تا تاریخ"
|
|
autocomplete="off"
|
|
readonly>
|
|
<button type="button"
|
|
id="dateFilterClear"
|
|
class="tasks-filter-clear"
|
|
onclick="clearTaskDateFilter()">
|
|
پاک کردن
|
|
</button>
|
|
<input type="hidden" id="dateFilterFromGregorian">
|
|
<input type="hidden" id="dateFilterToGregorian">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tasks-card mt-3">
|
|
<div class="tasks-tabs">
|
|
@foreach(array_keys($tasks) as $stateKey)
|
|
<button type="button" onclick="switchTaskTab('{{ $stateKey }}')"
|
|
class="task-tab {{ $loop->first ? 'task-tab-active' : '' }}"
|
|
data-tab="{{ $stateKey }}">
|
|
<span class="task-tab-label">{{ $stateKey }}</span>
|
|
<span class="task-tab-count">{{ count($tasks[$stateKey] ?? []) }}</span>
|
|
</button>
|
|
@endforeach
|
|
</div>
|
|
|
|
{{-- جداول هر وضعیت --}}
|
|
@foreach($tasks as $stateKey => $stateTasks)
|
|
<div id="tab-{{ $stateKey }}"
|
|
class="task-table-container tasks-table-wrap {{ $loop->first ? '' : 'hidden' }}">
|
|
@if(count($stateTasks) > 0)
|
|
<div class="tasks-cards-list">
|
|
@foreach($stateTasks as $task)
|
|
@php
|
|
$stateIcon = match((int)$task->state) {
|
|
1 => $backlog,
|
|
2 => $todo,
|
|
3 => $in_progress,
|
|
4 => $done,
|
|
default => '',
|
|
};
|
|
@endphp
|
|
<article class="task-card task-row {{ $task->active ? 'is-active' : '' }}"
|
|
data-task-id="{{ $task->id }}"
|
|
data-assignee-ids='@json($task->assignee_ids ?? [])'
|
|
data-start-date="{{ $task->start_date ? \Carbon\Carbon::parse($task->start_date)->format('Y-m-d') : '' }}"
|
|
data-target-date="{{ $task->target_date ? \Carbon\Carbon::parse($task->target_date)->format('Y-m-d') : '' }}"
|
|
data-finished-date="{{ $task->finished_at ? \Carbon\Carbon::parse($task->finished_at)->format('Y-m-d') : '' }}"
|
|
data-created-date="{{ $task->created_at ? \Carbon\Carbon::parse($task->created_at)->format('Y-m-d') : '' }}"
|
|
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>
|
|
|
|
@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
|
|
</div>
|
|
<div class="task-filter-empty hidden tasks-empty">
|
|
وظیفهای با این فیلترها در این وضعیت وجود ندارد.
|
|
</div>
|
|
@else
|
|
<div class="tasks-empty">
|
|
هیچ وظیفهای در این وضعیت وجود ندارد.
|
|
</div>
|
|
@endif
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
|
|
@else
|
|
<div class=" mt-[200px]">
|
|
<h1 class="text-xl font-bold text-gray-500">وظیفه ای ندارید</h1>
|
|
</div>
|
|
@endif
|
|
</div>
|
|
|
|
|
|
|
|
{{-- START TASK DETAILS MODAL --}}
|
|
<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-slate-200/70">
|
|
<h2 class="text-lg font-bold text-gray-800" id="modalTaskTitle">عنوان تسک</h2>
|
|
<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>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-4 space-y-4">
|
|
{{-- Tabs --}}
|
|
<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">
|
|
جزئیات
|
|
</button>
|
|
<button id="modalTabBtn-durations" type="button"
|
|
onclick="switchDetailsTab('durations')"
|
|
class="flex-1 px-3 py-2 text-sm rounded-lg text-gray-500 hover:bg-gray-50">
|
|
زمانها
|
|
</button>
|
|
<button id="modalTabBtn-comments" type="button"
|
|
onclick="switchDetailsTab('comments')"
|
|
class="flex-1 px-3 py-2 text-sm rounded-lg text-gray-500 hover:bg-gray-50">
|
|
کامنتها
|
|
</button>
|
|
</div>
|
|
|
|
{{-- Tab: info --}}
|
|
<div id="modalTab-info" class="space-y-4">
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">توضیحات</h3>
|
|
<p class="text-gray-800 text-sm leading-relaxed" id="modalTaskDescription">-</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<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="modalTaskAppName">-</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">اولویت</h3>
|
|
<span id="modalTaskPriorityView"
|
|
class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800 inline-block">-</span>
|
|
<select id="modalTaskPriorityEdit"
|
|
class="hidden mt-2 rounded-lg border border-gray-300 p-2 w-full text-sm">
|
|
<option value="none">ندارد</option>
|
|
<option value="low">کم</option>
|
|
<option value="medium">عادی</option>
|
|
<option value="high">زیاد</option>
|
|
<option value="urgent">فوری</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">وضعیت</h3>
|
|
<span id="modalTaskStateView"
|
|
class="px-2 py-1 text-xs rounded-full bg-gray-100 text-gray-800">-</span>
|
|
<select id="modalTaskStateEdit"
|
|
class="hidden mt-2 rounded-lg border border-gray-300 p-2 w-full text-sm">
|
|
<option value="1">انباشته</option>
|
|
<option value="2">برای انجام</option>
|
|
<option value="3">در حال انجام</option>
|
|
<option value="4">پایان یافته</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">تاریخ شروع</h3>
|
|
<p class="text-gray-800 text-sm" id="modalTaskStartDateView">-</p>
|
|
<input id="modalTaskStartDateEdit" type="text"
|
|
class="hidden mt-2 rounded-lg border border-gray-300 p-2 w-full text-sm"
|
|
placeholder="تاریخ شروع" autocomplete="off" readonly>
|
|
<input id="modalTaskStartDateGregorian" type="hidden">
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">مهلت</h3>
|
|
<p class="text-gray-800 text-sm" id="modalTaskTargetDateView">-</p>
|
|
<input id="modalTaskTargetDateEdit" type="text"
|
|
class="hidden mt-2 rounded-lg border border-gray-300 p-2 w-full text-sm"
|
|
placeholder="مهلت" autocomplete="off" readonly>
|
|
<input id="modalTaskTargetDateGregorian" type="hidden">
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">تاریخ اتمام</h3>
|
|
<p class="text-gray-800 text-sm" id="modalTaskFinishedAtView">-</p>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-sm font-medium text-gray-500 mb-1">زمان صرف شده</h3>
|
|
<p class="text-gray-800 text-sm font-mono" id="modalTaskDuration">00:00:00</p>
|
|
</div>
|
|
<div>
|
|
<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">
|
|
<button id="modalQuickEditBtn" type="button"
|
|
class="flex-1 px-4 py-2 rounded-lg font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 transition">
|
|
ویرایش سریع
|
|
</button>
|
|
<button id="modalEditTaskBtn" type="button"
|
|
class="px-4 py-2 rounded-lg font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 transition">
|
|
ویرایش کامل
|
|
</button>
|
|
<button id="modalDeleteTaskBtn" type="button"
|
|
class="px-4 py-2 rounded-lg font-medium text-gray-700 bg-red-100 hover:bg-gray-200 transition">
|
|
حذف
|
|
</button>
|
|
</div>
|
|
|
|
<div id="modalQuickEditActions" class="hidden flex gap-2">
|
|
<button id="modalQuickEditSaveBtn" type="button"
|
|
class="flex-1 px-4 py-2 rounded-lg font-medium text-white bg-cyan-600 hover:bg-cyan-700 transition">
|
|
ذخیره
|
|
</button>
|
|
<button id="modalQuickEditCancelBtn" type="button"
|
|
class="px-4 py-2 rounded-lg font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 transition">
|
|
انصراف
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Tab: durations --}}
|
|
<div id="modalTab-durations" class="hidden space-y-2">
|
|
<div class="text-xs text-gray-500" id="modalDurationsSummary">
|
|
-
|
|
</div>
|
|
<div class="flex gap-2 items-center">
|
|
<button id="modalDurationActionBtn" type="button"
|
|
class="flex-1 px-4 py-2 rounded-lg font-medium text-white bg-blue-600 hover:bg-blue-700 transition">
|
|
شروع
|
|
</button>
|
|
</div>
|
|
<div id="modalDurationsList"
|
|
class="space-y-2 max-h-[420px] overflow-y-auto border border-gray-100 rounded-lg p-2 bg-gray-50">
|
|
<div class="text-sm text-gray-400 text-center">در حال بارگذاری...</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Tab: comments --}}
|
|
<div id="modalTab-comments" class="hidden space-y-3">
|
|
<div id="modalCommentsList"
|
|
class="space-y-2 max-h-[300px] overflow-y-auto border border-gray-100 rounded-lg p-2 bg-gray-50">
|
|
<div class="text-sm text-gray-400 text-center">در حال بارگذاری...</div>
|
|
</div>
|
|
|
|
<form id="modalCommentForm" method="POST">
|
|
@csrf
|
|
<label for="modalCommentBody" class="font-medium text-sm text-gray-700">
|
|
ثبت کامنت (برای منشن از @ استفاده کنید)
|
|
</label>
|
|
|
|
<div class="relative">
|
|
<textarea id="modalCommentBody" name="body" rows="3"
|
|
class="rounded-lg border border-gray-300 p-2 w-full mt-2"
|
|
placeholder="مثلاً: @ali لطفاً این بخش رو بررسی کن" required></textarea>
|
|
|
|
<div id="mentionDropdown"
|
|
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>
|
|
|
|
<div class="flex justify-center mt-3">
|
|
<button type="submit"
|
|
class="bg-cyan-600 text-white py-2 px-4 rounded-lg hover:bg-cyan-700 transition">
|
|
ارسال کامنت
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
@endsection
|
|
@section('script')
|
|
<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 = {};
|
|
|
|
function openTaskModal(taskId, p_id, started_at, ended_at, actionType) {
|
|
currentTask = {taskId, p_id, started_at, ended_at, actionType};
|
|
|
|
const modal = document.getElementById('taskModal');
|
|
const actionBtn = document.getElementById('actionBtn');
|
|
const modalTitle = document.getElementById('modalTitle');
|
|
const modalDesc = document.getElementById('modalDesc');
|
|
|
|
// تعیین متن دکمه بر اساس actionType
|
|
actionBtn.textContent = actionType === 'start' ? 'شروع' : 'پایان';
|
|
if (actionType === 'start') {
|
|
actionBtn.classList.remove('bg-green-600', 'hover:bg-green-700');
|
|
actionBtn.classList.add('bg-blue-600', 'hover:bg-blue-700');
|
|
modalDesc.textContent = 'آیا میخواهید این وظیفه را شروع کنید؟';
|
|
} else {
|
|
actionBtn.classList.remove('bg-blue-600', 'hover:bg-blue-700');
|
|
actionBtn.classList.add('bg-green-600', 'hover:bg-green-700');
|
|
modalDesc.textContent = 'آیا میخواهید این وظیفه را پایان دهید؟';
|
|
}
|
|
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
|
|
// عملکرد دکمه شروع/پایان
|
|
actionBtn.onclick = () => handleTaskAction();
|
|
document.getElementById('cancelBtn').onclick = () => closeModal();
|
|
}
|
|
|
|
function closeModal() {
|
|
const modal = document.getElementById('taskModal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
function handleTaskAction() {
|
|
// ثبت زمان فعلی با فرمت Y-m-d H:i:s
|
|
const now = new Date();
|
|
const pad = n => n.toString().padStart(2, '0');
|
|
const formattedNow = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}`;
|
|
|
|
if (currentTask.actionType === 'start') {
|
|
currentTask.started_at = formattedNow;
|
|
} else {
|
|
currentTask.ended_at = formattedNow;
|
|
}
|
|
|
|
// مسیر API لاراول که باید POST بزنی
|
|
fetch('{{ url("/task-manager") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({
|
|
task_id: currentTask.taskId,
|
|
p_id: currentTask.p_id,
|
|
started_at: currentTask.started_at,
|
|
ended_at: currentTask.ended_at,
|
|
action_type: currentTask.actionType
|
|
})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log('Task updated', data);
|
|
closeModal();
|
|
} else {
|
|
alert('خطا در انجام عملیات!');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert('خطا در ارتباط با سرور!');
|
|
});
|
|
}
|
|
|
|
function togglerequests(elementId) {
|
|
document.getElementById(elementId).classList.toggle('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;
|
|
}
|
|
|
|
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);
|
|
updateTaskFinishedAtView(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;
|
|
});
|
|
}
|
|
|
|
// مقداردهی اولیه (در صورت نیاز)
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// میتوانید اولین تب را به عنوان پیشفرض فعال نگه دارید
|
|
});
|
|
|
|
let currentTaskDetails = {}; // برای نگهداری اطلاعات تسک فعلی در مودال جزئیات
|
|
|
|
function escapeHtml(str) {
|
|
return String(str ?? '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function renderTaskComments(task) {
|
|
const list = document.getElementById('modalCommentsList');
|
|
if (!list) return;
|
|
|
|
const comments = Array.isArray(task.comments) ? task.comments : [];
|
|
if (comments.length === 0) {
|
|
list.innerHTML = `<div class="text-sm text-gray-400 text-center">هنوز کامنتی ثبت نشده.</div>`;
|
|
return;
|
|
}
|
|
|
|
list.innerHTML = comments.map(c => {
|
|
const author = c.user?.name || 'نامشخص';
|
|
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="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>
|
|
</div>
|
|
<div class="text-sm text-gray-800 whitespace-pre-wrap mt-1">${body}</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
function sumDurationSeconds(durations) {
|
|
let total = 0;
|
|
(durations || []).forEach(d => {
|
|
if (!d.duration) return;
|
|
const parts = String(d.duration).split(':').map(Number);
|
|
if (parts.length !== 3 || parts.some(n => Number.isNaN(n))) return;
|
|
total += (parts[0] * 3600) + (parts[1] * 60) + parts[2];
|
|
});
|
|
return total;
|
|
}
|
|
|
|
function formatHms(totalSeconds) {
|
|
const s = Math.max(0, Number(totalSeconds) || 0);
|
|
const h = Math.floor(s / 3600);
|
|
const m = Math.floor((s % 3600) / 60);
|
|
const sec = Math.floor(s % 60);
|
|
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function updateModalTotalDuration(task) {
|
|
const el = document.getElementById('modalTaskDuration');
|
|
if (!el) return;
|
|
const durs = Array.isArray(task.duration) ? task.duration : [];
|
|
const filtered = isAdminLevel1
|
|
? durs
|
|
: durs.filter(d => String(d.user_id) === String(currentUserId));
|
|
const total = sumDurationSeconds(filtered);
|
|
el.textContent = formatHms(total);
|
|
|
|
const summary = document.getElementById('modalDurationsSummary');
|
|
if (summary) {
|
|
summary.textContent = isAdminLevel1
|
|
? `زمان کل همه کاربران روی این تسک: ${formatHms(total)}`
|
|
: `زمان کل شما روی این تسک: ${formatHms(total)}`;
|
|
}
|
|
}
|
|
|
|
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;
|
|
|
|
const durs = Array.isArray(task.duration) ? task.duration : [];
|
|
const filtered = isAdminLevel1
|
|
? durs
|
|
: durs.filter(d => String(d.user_id) === String(currentUserId));
|
|
|
|
if (filtered.length === 0) {
|
|
list.innerHTML = `<div class="text-sm text-gray-400 text-center">زمانی ثبت نشده.</div>`;
|
|
return;
|
|
}
|
|
|
|
const usersById = new Map((mentionUsers || []).map(u => [String(u.id), u]));
|
|
|
|
list.innerHTML = filtered
|
|
.slice()
|
|
.sort((a, b) => new Date(b.start || 0) - new Date(a.start || 0))
|
|
.map(d => {
|
|
const u = usersById.get(String(d.user_id));
|
|
const uname = u?.name || d.user_id;
|
|
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 ? '-' : '...');
|
|
|
|
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>
|
|
<span class="font-mono">${escapeHtml(dur)}</span>
|
|
</div>
|
|
<div class="text-xs text-gray-600 mt-1">
|
|
شروع: ${escapeHtml(start)}<br/>
|
|
پایان: ${escapeHtml(end)}
|
|
</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) {
|
|
const tabs = ['info', 'durations', 'comments'];
|
|
tabs.forEach(t => {
|
|
const el = document.getElementById(`modalTab-${t}`);
|
|
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);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setupCommentForm(task) {
|
|
const form = document.getElementById('modalCommentForm');
|
|
const textarea = document.getElementById('modalCommentBody');
|
|
if (!form || !textarea) return;
|
|
|
|
form.action = `/task/${task.id}/comments`;
|
|
textarea.value = '';
|
|
}
|
|
|
|
function getMentionQuery(text, caretPos) {
|
|
const before = text.slice(0, caretPos);
|
|
const at = before.lastIndexOf('@');
|
|
if (at === -1) return null;
|
|
if (at > 0 && /\S/.test(before[at - 1])) return null; // باید ابتدای کلمه باشد
|
|
const query = before.slice(at + 1);
|
|
if (/\s/.test(query)) return null;
|
|
return {atIndex: at, query};
|
|
}
|
|
|
|
function insertMention(textarea, atIndex, username) {
|
|
const text = textarea.value;
|
|
const caretPos = textarea.selectionStart;
|
|
const before = text.slice(0, atIndex);
|
|
const after = text.slice(caretPos);
|
|
const mention = `@${username} `;
|
|
textarea.value = before + mention + after;
|
|
const newPos = (before + mention).length;
|
|
textarea.setSelectionRange(newPos, newPos);
|
|
textarea.focus();
|
|
}
|
|
|
|
function setupMentionAutocomplete() {
|
|
const textarea = document.getElementById('modalCommentBody');
|
|
const dropdown = document.getElementById('mentionDropdown');
|
|
if (!textarea || !dropdown) return;
|
|
|
|
let state = {open: false, atIndex: -1, query: ''};
|
|
|
|
function close() {
|
|
dropdown.classList.add('hidden');
|
|
dropdown.innerHTML = '';
|
|
state.open = false;
|
|
}
|
|
|
|
function open(items, atIndex, query) {
|
|
state = {open: true, atIndex, query};
|
|
dropdown.innerHTML = items.map(u => {
|
|
const name = escapeHtml(u.name);
|
|
const uname = escapeHtml(u.user_name);
|
|
return `
|
|
<button type="button"
|
|
class="w-full text-right px-3 py-2 hover:bg-gray-50 text-sm flex justify-between"
|
|
data-username="${uname}">
|
|
<span class="font-medium text-gray-800">${name}</span>
|
|
<span class="text-xs text-gray-500">@${uname}</span>
|
|
</button>
|
|
`;
|
|
}).join('');
|
|
dropdown.classList.remove('hidden');
|
|
}
|
|
|
|
textarea.addEventListener('input', () => {
|
|
const caretPos = textarea.selectionStart;
|
|
const q = getMentionQuery(textarea.value, caretPos);
|
|
if (!q) return close();
|
|
|
|
const query = (q.query || '').toLowerCase();
|
|
const filtered = (mentionUsers || []).filter(u => {
|
|
const n = (u.name || '').toLowerCase();
|
|
const un = (u.user_name || '').toLowerCase();
|
|
return n.includes(query) || un.includes(query);
|
|
}).slice(0, 8);
|
|
|
|
if (filtered.length === 0) return close();
|
|
open(filtered, q.atIndex, q.query);
|
|
});
|
|
|
|
textarea.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') close();
|
|
});
|
|
|
|
dropdown.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('button[data-username]');
|
|
if (!btn) return;
|
|
const uname = btn.getAttribute('data-username');
|
|
insertMention(textarea, state.atIndex, uname);
|
|
close();
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target === textarea || dropdown.contains(e.target)) return;
|
|
close();
|
|
});
|
|
}
|
|
|
|
function resolveFinishedAtFromTask(task) {
|
|
if (task?.finished_at) {
|
|
return task.finished_at;
|
|
}
|
|
|
|
const durs = Array.isArray(task?.duration) ? task.duration : [];
|
|
const ends = durs
|
|
.map(d => d.end)
|
|
.filter(Boolean)
|
|
.sort((a, b) => new Date(b) - new Date(a));
|
|
|
|
return ends[0] || null;
|
|
}
|
|
|
|
function updateTaskFinishedAtView(task) {
|
|
const finishedView = document.getElementById('modalTaskFinishedAtView');
|
|
if (!finishedView) {
|
|
return;
|
|
}
|
|
|
|
const finishedAt = resolveFinishedAtFromTask(task);
|
|
finishedView.textContent = finishedAt ? formatDateTime(finishedAt) : '-';
|
|
}
|
|
|
|
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);
|
|
updateTaskDateViews(task);
|
|
}
|
|
|
|
// باز کردن مودال جزئیات تسک
|
|
function openTaskDetailsModal(taskOrId) {
|
|
const task = typeof taskOrId === 'object'
|
|
? taskOrId
|
|
: tasksById[taskOrId] ?? tasksById[String(taskOrId)];
|
|
|
|
if (!task) {
|
|
alert('اطلاعات این وظیفه در دسترس نیست.');
|
|
return;
|
|
}
|
|
|
|
currentTaskDetails = task;
|
|
|
|
// پر کردن فیلدهای مودال
|
|
document.getElementById('modalTaskTitle').textContent = task.title || 'بدون عنوان';
|
|
document.getElementById('modalTaskDescription').innerHTML = task.description || 'بدون توضیحات';
|
|
updateTaskDateViews(task);
|
|
updateTaskFinishedAtView(task);
|
|
updateModalInfoFields(task);
|
|
|
|
// اپ
|
|
const appNameEl = document.getElementById('modalTaskAppName');
|
|
const appTypes = {1: 'خانگی', 2: 'همکار'};
|
|
if (appNameEl) {
|
|
const app = (appsList || []).find(a => String(a.id) === String(task.app));
|
|
appNameEl.textContent = app ? `${app.name_fa} (${appTypes[app.type]})` : '-';
|
|
}
|
|
|
|
// اولویت
|
|
const priorityLabels = {'urgent': 'فوری', 'high': 'بالا', 'medium': 'متوسط', 'low': 'پایین'};
|
|
const priorityColors = {
|
|
'urgent': 'bg-red-100 text-red-800',
|
|
'high': 'bg-orange-100 text-orange-800',
|
|
'medium': 'bg-yellow-100 text-yellow-800',
|
|
'low': 'bg-green-100 text-green-800'
|
|
};
|
|
const prioritySpan = document.getElementById('modalTaskPriorityView');
|
|
if (prioritySpan) {
|
|
prioritySpan.textContent = priorityLabels[task.priority] || (task.priority || 'ندارد');
|
|
prioritySpan.className = `px-2 py-1 text-xs rounded-full ${priorityColors[task.priority] || 'bg-gray-100 text-gray-800'} inline-block`;
|
|
}
|
|
|
|
// وضعیت
|
|
const stateLabels = {1: 'انباشته', 2: 'برای انجام', 3: 'در حال انجام', 4: 'پایان یافته'};
|
|
const stateColors = {
|
|
1: 'bg-gray-100 text-gray-800',
|
|
2: 'bg-orange-100 text-orange-800',
|
|
3: 'bg-cyan-100 text-cyan-800',
|
|
4: 'bg-green-100 text-green-800'
|
|
};
|
|
const stateSpan = document.getElementById('modalTaskStateView');
|
|
if (stateSpan) {
|
|
stateSpan.textContent = stateLabels[task.state] || 'نامشخص';
|
|
stateSpan.className = `px-2 py-1 text-xs rounded-full ${stateColors[task.state] || 'bg-gray-100 text-gray-800'}`;
|
|
}
|
|
|
|
// مجموع زمان صرفشده (محاسبه از duration در صورت وجود)
|
|
updateModalTotalDuration(task);
|
|
|
|
// تنظیم دکمه شروع/پایان داخل تب زمانها (بدون رفرش)
|
|
const durationBtn = document.getElementById('modalDurationActionBtn');
|
|
if (durationBtn) {
|
|
const actionType = task.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`;
|
|
|
|
durationBtn.onclick = () => {
|
|
durationBtn.disabled = true;
|
|
fetch('{{ url("/active_task") }}', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
body: JSON.stringify({task_id: task.id})
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (!data.status) {
|
|
alert('خطا در درخواست!');
|
|
return;
|
|
}
|
|
|
|
const durs = Array.isArray(currentTaskDetails.duration) ? currentTaskDetails.duration : [];
|
|
|
|
if (data.action === 'start') {
|
|
currentTaskDetails.active = true;
|
|
durs.push({
|
|
id: data.id || null,
|
|
task_id: currentTaskDetails.id,
|
|
user_id: currentUserId,
|
|
start: data.start,
|
|
end: null,
|
|
duration: null,
|
|
});
|
|
} else if (data.action === 'ended') {
|
|
currentTaskDetails.active = false;
|
|
for (let i = durs.length - 1; i >= 0; i--) {
|
|
if (String(durs[i].user_id) === String(currentUserId) && !durs[i].end) {
|
|
durs[i].end = data.end;
|
|
durs[i].duration = data.duration;
|
|
break;
|
|
}
|
|
}
|
|
currentTaskDetails.finished_at = data.finished_at || data.end || currentTaskDetails.finished_at;
|
|
}
|
|
|
|
currentTaskDetails.duration = durs;
|
|
renderTaskDurations(currentTaskDetails);
|
|
updateModalTotalDuration(currentTaskDetails);
|
|
updateTaskFinishedAtView(currentTaskDetails);
|
|
|
|
const row = document.querySelector(`.task-row[data-task-id="${currentTaskDetails.id}"]`);
|
|
if (row && currentTaskDetails.finished_at) {
|
|
row.dataset.finishedDate = String(currentTaskDetails.finished_at).slice(0, 10);
|
|
}
|
|
|
|
tasksById[currentTaskDetails.id] = currentTaskDetails;
|
|
tasksById[String(currentTaskDetails.id)] = currentTaskDetails;
|
|
window.tasksById = tasksById;
|
|
|
|
const nextAction = currentTaskDetails.active ? 'end' : 'start';
|
|
durationBtn.textContent = nextAction === 'start' ? 'شروع' : 'پایان';
|
|
durationBtn.className = `flex-1 px-4 py-2 rounded-lg font-medium text-white ${nextAction === 'start' ? 'bg-blue-600 hover:bg-blue-700' : 'bg-green-600 hover:bg-green-700'} transition`;
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert('خطا در ارتباط با سرور!');
|
|
})
|
|
.finally(() => {
|
|
durationBtn.disabled = false;
|
|
});
|
|
};
|
|
}
|
|
|
|
// دکمه ویرایش تسک
|
|
document.getElementById('modalEditTaskBtn').onclick = () => {
|
|
// هدایت به صفحه ویرایش (فرض میکنیم route با نام tasks.edit وجود دارد)
|
|
window.location.href = `/task/${task.id}`; // مسیر را مطابق پروژه تنظیم کنید
|
|
};
|
|
document.getElementById('modalDeleteTaskBtn').onclick = () => {
|
|
fetch(`/task/delete/${task.id}`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': '{{ csrf_token() }}'
|
|
},
|
|
})
|
|
.then(res => {
|
|
// ابتدا بررسی وضعیت HTTP
|
|
if (!res.ok) {
|
|
// اگر کد وضعیت 4xx یا 5xx باشد
|
|
throw new Error(`HTTP error! status: ${res.status}`);
|
|
}
|
|
// در صورت موفقیت (200)، تلاش برای خواندن JSON
|
|
return res.json();
|
|
})
|
|
.then(data => {
|
|
// 💡 اصلاح: بررسی دقیق فیلد success
|
|
if (data && data.success === true) {
|
|
console.log('✅ Task updated successfully:', data);
|
|
window.location.reload();
|
|
} else {
|
|
// سرور با کد 200 جواب داده اما success: false بوده یا ساختار پاسخ مشکل دارد.
|
|
console.error('❌ Operation failed based on server data:', data);
|
|
alert('❌ خطا در انجام عملیات! پاسخ سرور را در کنسول بررسی کنید.');
|
|
}
|
|
})
|
|
.catch(err => {
|
|
// شامل خطای ارتباط، خطای HTTP و خطای JSON نامعتبر
|
|
console.error('⚠️ Server or JSON parsing error:', err);
|
|
alert(`⚠️ خطا در ارتباط با سرور یا پردازش داده!`);
|
|
});
|
|
|
|
}
|
|
|
|
|
|
// نمایش مودال
|
|
const modal = document.getElementById('taskDetailsModal');
|
|
modal.classList.remove('hidden');
|
|
modal.style.display = 'flex';
|
|
|
|
switchDetailsTab('info');
|
|
setQuickEditMode(false);
|
|
setTaskDetailsLoading();
|
|
setupCommentForm(task);
|
|
setupMentionAutocomplete();
|
|
fetchTaskDetails(task.id);
|
|
}
|
|
|
|
function setQuickEditMode(on) {
|
|
const btn = document.getElementById('modalQuickEditBtn');
|
|
const actions = document.getElementById('modalQuickEditActions');
|
|
|
|
const prView = document.getElementById('modalTaskPriorityView');
|
|
const prEdit = document.getElementById('modalTaskPriorityEdit');
|
|
|
|
const stView = document.getElementById('modalTaskStateView');
|
|
const stEdit = document.getElementById('modalTaskStateEdit');
|
|
|
|
const sdView = document.getElementById('modalTaskStartDateView');
|
|
const sdEdit = document.getElementById('modalTaskStartDateEdit');
|
|
const sdGreg = document.getElementById('modalTaskStartDateGregorian');
|
|
|
|
const tdView = document.getElementById('modalTaskTargetDateView');
|
|
const tdEdit = document.getElementById('modalTaskTargetDateEdit');
|
|
const tdGreg = document.getElementById('modalTaskTargetDateGregorian');
|
|
|
|
if (!btn || !actions || !prView || !prEdit || !stView || !stEdit || !sdView || !sdEdit || !tdView || !tdEdit) return;
|
|
|
|
actions.classList.toggle('hidden', !on);
|
|
prView.classList.toggle('hidden', on);
|
|
prEdit.classList.toggle('hidden', !on ? true : false);
|
|
|
|
stView.classList.toggle('hidden', on);
|
|
stEdit.classList.toggle('hidden', !on ? true : false);
|
|
|
|
sdView.classList.toggle('hidden', on);
|
|
sdEdit.classList.toggle('hidden', !on ? true : false);
|
|
|
|
tdView.classList.toggle('hidden', on);
|
|
tdEdit.classList.toggle('hidden', !on ? true : false);
|
|
|
|
btn.textContent = on ? 'در حال ویرایش' : 'ویرایش سریع';
|
|
btn.classList.toggle('bg-yellow-50', on);
|
|
|
|
if (on) {
|
|
prEdit.value = currentTaskDetails.priority || 'none';
|
|
stEdit.value = String(currentTaskDetails.state || 1);
|
|
sdEdit.value = currentTaskDetails.start_date ? formatJalaliDate(currentTaskDetails.start_date) : '';
|
|
tdEdit.value = currentTaskDetails.target_date ? formatJalaliDate(currentTaskDetails.target_date) : '';
|
|
|
|
if (sdGreg) sdGreg.value = normalizeDateValue(currentTaskDetails.start_date);
|
|
if (tdGreg) tdGreg.value = normalizeDateValue(currentTaskDetails.target_date);
|
|
|
|
initModalDatepickers();
|
|
}
|
|
}
|
|
|
|
function initModalDatepickers() {
|
|
const sdEdit = $('#modalTaskStartDateEdit');
|
|
const tdEdit = $('#modalTaskTargetDateEdit');
|
|
|
|
const pickerOptions = {
|
|
format: 'YYYY/MM/DD',
|
|
autoClose: true,
|
|
toolbox: {
|
|
calendarSwitch: {enabled: false},
|
|
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
|
}
|
|
};
|
|
|
|
if (sdEdit.length) {
|
|
try {
|
|
sdEdit.persianDatepicker('destroy');
|
|
} catch (e) {
|
|
}
|
|
sdEdit.persianDatepicker({
|
|
...pickerOptions,
|
|
onSelect: function (unix) {
|
|
let gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
|
let dateStr = gregorian.format('YYYY-MM-DD');
|
|
const sdGreg = document.getElementById('modalTaskStartDateGregorian');
|
|
if (sdGreg) sdGreg.value = dateStr;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (tdEdit.length) {
|
|
try {
|
|
tdEdit.persianDatepicker('destroy');
|
|
} catch (e) {
|
|
}
|
|
tdEdit.persianDatepicker({
|
|
...pickerOptions,
|
|
onSelect: function (unix) {
|
|
let gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
|
let dateStr = gregorian.format('YYYY-MM-DD');
|
|
const tdGreg = document.getElementById('modalTaskTargetDateGregorian');
|
|
if (tdGreg) tdGreg.value = dateStr;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Quick edit events
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const quickBtn = document.getElementById('modalQuickEditBtn');
|
|
const saveBtn = document.getElementById('modalQuickEditSaveBtn');
|
|
const cancelBtn = document.getElementById('modalQuickEditCancelBtn');
|
|
|
|
if (quickBtn) quickBtn.onclick = () => setQuickEditMode(true);
|
|
if (cancelBtn) cancelBtn.onclick = () => setQuickEditMode(false);
|
|
|
|
if (saveBtn) {
|
|
saveBtn.onclick = () => {
|
|
const prEdit = document.getElementById('modalTaskPriorityEdit');
|
|
const stEdit = document.getElementById('modalTaskStateEdit');
|
|
const sdGreg = document.getElementById('modalTaskStartDateGregorian');
|
|
const tdGreg = document.getElementById('modalTaskTargetDateGregorian');
|
|
|
|
const fd = new FormData();
|
|
fd.append('_token', '{{ csrf_token() }}');
|
|
fd.append('id', currentTaskDetails.id);
|
|
fd.append('priority', prEdit?.value || 'none');
|
|
fd.append('state', stEdit?.value || String(currentTaskDetails.state || 1));
|
|
fd.append('start_date', sdGreg?.value || '');
|
|
fd.append('target_date', tdGreg?.value || '');
|
|
|
|
saveBtn.disabled = true;
|
|
fetch('{{ route('task.edit') }}', {method: 'POST', body: fd})
|
|
.then(() => {
|
|
// backend redirect میدهد، ما فقط UI را آپدیت میکنیم
|
|
currentTaskDetails.priority = prEdit?.value || 'none';
|
|
currentTaskDetails.start_date = sdGreg?.value || null;
|
|
currentTaskDetails.target_date = tdGreg?.value || null;
|
|
|
|
const sdView = document.getElementById('modalTaskStartDateView');
|
|
const tdView = document.getElementById('modalTaskTargetDateView');
|
|
if (sdView) sdView.textContent = currentTaskDetails.start_date ? formatJalaliDate(currentTaskDetails.start_date) : '-';
|
|
if (tdView) tdView.textContent = currentTaskDetails.target_date ? formatJalaliDate(currentTaskDetails.target_date) : '-';
|
|
|
|
// priority view
|
|
const priorityLabels = {
|
|
'urgent': 'فوری',
|
|
'high': 'بالا',
|
|
'medium': 'متوسط',
|
|
'low': 'پایین'
|
|
};
|
|
const priorityColors = {
|
|
'urgent': 'bg-red-100 text-red-800',
|
|
'high': 'bg-orange-100 text-orange-800',
|
|
'medium': 'bg-yellow-100 text-yellow-800',
|
|
'low': 'bg-green-100 text-green-800'
|
|
};
|
|
const prView = document.getElementById('modalTaskPriorityView');
|
|
if (prView) {
|
|
prView.textContent = priorityLabels[currentTaskDetails.priority] || (currentTaskDetails.priority || 'ندارد');
|
|
prView.className = `px-2 py-1 text-xs rounded-full ${priorityColors[currentTaskDetails.priority] || 'bg-gray-100 text-gray-800'} inline-block`;
|
|
}
|
|
|
|
// state view
|
|
currentTaskDetails.state = parseInt(stEdit?.value || currentTaskDetails.state || 1, 10);
|
|
const stateLabels = {1: 'انباشته', 2: 'برای انجام', 3: 'در حال انجام', 4: 'پایان یافته'};
|
|
const stateColors = {
|
|
1: 'bg-gray-100 text-gray-800',
|
|
2: 'bg-orange-100 text-orange-800',
|
|
3: 'bg-cyan-100 text-cyan-800',
|
|
4: 'bg-green-100 text-green-800'
|
|
};
|
|
const stView = document.getElementById('modalTaskStateView');
|
|
if (stView) {
|
|
stView.textContent = stateLabels[currentTaskDetails.state] || 'نامشخص';
|
|
stView.className = `px-2 py-1 text-xs rounded-full ${stateColors[currentTaskDetails.state] || 'bg-gray-100 text-gray-800'}`;
|
|
}
|
|
|
|
setQuickEditMode(false);
|
|
})
|
|
.catch(() => alert('خطا در ذخیره تغییرات'))
|
|
.finally(() => {
|
|
saveBtn.disabled = false;
|
|
});
|
|
};
|
|
}
|
|
});
|
|
|
|
function closeDetailsModal() {
|
|
const modal = document.getElementById('taskDetailsModal');
|
|
if (!modal) return;
|
|
modal.classList.add('hidden');
|
|
modal.style.display = 'none';
|
|
}
|
|
|
|
function formatJalaliDate(dateStr) {
|
|
if (!dateStr) return '-';
|
|
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) {
|
|
if (!dateStr) return '-';
|
|
const d = new Date(dateStr);
|
|
return d.toLocaleString('fa-IR');
|
|
}
|
|
|
|
// NOTE: مدیریت زمان از مودال جزئیات حذف شده و فقط در تب "زمانها" انجام میشود.
|
|
|
|
$(document).ready(function () {
|
|
const datePickerOptions = {
|
|
format: 'YYYY/MM/DD',
|
|
autoClose: true,
|
|
initialValue: false,
|
|
initialValueType: 'gregorian',
|
|
toolbox: {
|
|
calendarSwitch: {enabled: false},
|
|
todayButton: {enabled: true, text: {fa: 'امروز'}}
|
|
}
|
|
};
|
|
|
|
$('#dateFilterFrom').persianDatepicker({
|
|
...datePickerOptions,
|
|
onSelect: function (unix) {
|
|
const gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
|
$('#dateFilterFromGregorian').val(gregorian.format('YYYY-MM-DD'));
|
|
applyTaskFilters();
|
|
}
|
|
});
|
|
|
|
$('#dateFilterTo').persianDatepicker({
|
|
...datePickerOptions,
|
|
onSelect: function (unix) {
|
|
const gregorian = new persianDate(unix).toCalendar('gregorian').toLocale('en');
|
|
$('#dateFilterToGregorian').val(gregorian.format('YYYY-MM-DD'));
|
|
applyTaskFilters();
|
|
}
|
|
});
|
|
|
|
clearTaskDateFilter();
|
|
});
|
|
|
|
</script>
|
|
|
|
@endsection
|