mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-07 21:10:46 -05:00
9b7aa3a938
Affected modules: Projects, Clients, Tasks, Invoices, Comments, Admin, Search - All HTML forms now include csrf_token hidden input - JavaScript forms retrieve token from meta tag in base.html - API endpoints properly exempted for JSON operations - 58 POST forms + 4 dynamic JS forms now protected Security impact: HIGH - Closes critical CSRF vulnerability Files modified: 20 templates
1204 lines
39 KiB
HTML
1204 lines
39 KiB
HTML
{# Reusable Kanban board for tasks. Expects `tasks` in context. #}
|
|
{% set kanban_statuses = [
|
|
{'key': 'todo', 'label': _('To Do'), 'icon': 'fas fa-list-check', 'accent': 'secondary'},
|
|
{'key': 'in_progress', 'label': _('In Progress'), 'icon': 'fas fa-spinner', 'accent': 'warning'},
|
|
{'key': 'review', 'label': _('Review'), 'icon': 'fas fa-user-check', 'accent': 'info'},
|
|
{'key': 'done', 'label': _('Done'), 'icon': 'fas fa-check-circle', 'accent': 'success'}
|
|
] %}
|
|
|
|
<div class="kanban-board-wrapper">
|
|
<div class="kanban-toolbar d-flex justify-content-between align-items-center mb-4">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="kanban-toolbar-icon">
|
|
<i class="fas fa-columns"></i>
|
|
</div>
|
|
<h5 class="kanban-toolbar-title mb-0">{{ _('Kanban Board') }}</h5>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="kanbanBoard" class="kanban-board">
|
|
{% for col in kanban_statuses %}
|
|
<div class="kanban-column" data-status="{{ col.key }}">
|
|
<div class="kanban-column-header">
|
|
<div class="d-flex align-items-center justify-content-between">
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="kanban-status-icon kanban-status-{{ col.key }}">
|
|
<i class="{{ col.icon }}"></i>
|
|
</div>
|
|
<h6 class="kanban-column-title mb-0">{{ col.label }}</h6>
|
|
</div>
|
|
<span class="kanban-count kanban-count-{{ col.key }}">
|
|
{{ tasks|selectattr('status', 'equalto', col.key)|list|length }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="kanban-column-body" data-status="{{ col.key }}">
|
|
{% for task in tasks if task.status == col.key %}
|
|
<div class="kanban-card {% if current_user.active_timer and current_user.active_timer.task_id == task.id %}kanban-card-active{% endif %}"
|
|
draggable="true"
|
|
data-task-id="{{ task.id }}"
|
|
data-status="{{ task.status }}"
|
|
data-priority="{{ task.priority }}">
|
|
|
|
<!-- Card Header with ID and Actions -->
|
|
<div class="kanban-card-header">
|
|
<div class="kanban-card-id">#{{ task.id }}</div>
|
|
<div class="kanban-card-actions">
|
|
{% if current_user.active_timer and current_user.active_timer.task_id == task.id %}
|
|
<form method="POST" action="{{ url_for('timer.stop_timer') }}" class="d-inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="kanban-action-btn kanban-action-stop" title="{{ _('Stop Timer') }}">
|
|
<i class="fas fa-stop"></i>
|
|
</button>
|
|
</form>
|
|
{% else %}
|
|
<form method="POST" action="{{ url_for('timer.start_timer') }}" class="d-inline">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="project_id" value="{{ task.project_id }}">
|
|
<input type="hidden" name="task_id" value="{{ task.id }}">
|
|
<button type="submit" class="kanban-action-btn kanban-action-play" title="{{ _('Start Timer') }}">
|
|
<i class="fas fa-play"></i>
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
<a href="{{ url_for('tasks.edit_task', task_id=task.id) }}" class="kanban-action-btn kanban-action-edit" title="{{ _('Edit Task') }}">
|
|
<i class="fas fa-pen"></i>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Priority Indicator -->
|
|
<div class="kanban-card-priority kanban-priority-{{ task.priority }}"></div>
|
|
|
|
<!-- Card Content -->
|
|
<div class="kanban-card-content">
|
|
<h6 class="kanban-card-title">
|
|
<a href="javascript:void(0);" onclick="openTaskModal({{ task.id }})">{{ task.name }}</a>
|
|
</h6>
|
|
|
|
{% if task.description %}
|
|
<p class="kanban-card-description">
|
|
{{ task.description[:90] }}{% if task.description|length > 90 %}...{% endif %}
|
|
</p>
|
|
{% endif %}
|
|
|
|
<!-- Badges Row -->
|
|
<div class="kanban-card-badges">
|
|
<span class="kanban-badge kanban-badge-priority kanban-badge-priority-{{ task.priority }}">
|
|
{{ task.priority_display }}
|
|
</span>
|
|
{% if current_user.active_timer and current_user.active_timer.task_id == task.id %}
|
|
<span class="kanban-badge kanban-badge-active">
|
|
<i class="fas fa-circle-dot"></i> {{ _('Active') }}
|
|
</span>
|
|
{% endif %}
|
|
{% if task.is_overdue %}
|
|
<span class="kanban-badge kanban-badge-overdue">
|
|
<i class="fas fa-exclamation-triangle"></i> {{ _('Overdue') }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Progress Bar -->
|
|
{% if task.estimated_hours %}
|
|
<div class="kanban-card-progress">
|
|
<div class="kanban-progress-header">
|
|
<span class="kanban-progress-label">{{ _('Progress') }}</span>
|
|
<span class="kanban-progress-value">{{ task.progress_percentage }}%</span>
|
|
</div>
|
|
<div class="kanban-progress-bar">
|
|
<div class="kanban-progress-fill" style="width: {{ task.progress_percentage }}%"></div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Card Footer -->
|
|
<div class="kanban-card-footer">
|
|
<div class="kanban-card-meta">
|
|
{% if task.assigned_user %}
|
|
<div class="kanban-meta-item kanban-meta-assignee" title="{{ task.assigned_user.display_name }}">
|
|
<i class="fas fa-user"></i>
|
|
<span>{{ task.assigned_user.display_name }}</span>
|
|
</div>
|
|
{% endif %}
|
|
{% if task.due_date %}
|
|
<div class="kanban-meta-item kanban-meta-date {% if task.is_overdue %}kanban-meta-overdue{% endif %}" title="{{ _('Due Date') }}">
|
|
<i class="fas fa-calendar"></i>
|
|
<span>{{ task.due_date.strftime('%b %d') }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* ============================================
|
|
KANBAN BOARD - MODERN REDESIGN
|
|
============================================ */
|
|
|
|
/* Toolbar */
|
|
.kanban-board-wrapper {
|
|
overflow-x: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
padding-bottom: 12px;
|
|
}
|
|
|
|
.kanban-toolbar {
|
|
padding: 0;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.kanban-toolbar-icon {
|
|
width: 42px;
|
|
height: 42px;
|
|
border-radius: 10px;
|
|
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-size: 18px;
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
|
}
|
|
|
|
.kanban-toolbar-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
letter-spacing: -0.02em;
|
|
}
|
|
|
|
/* Board Layout */
|
|
.kanban-board {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
align-items: start;
|
|
}
|
|
|
|
/* Column Styles */
|
|
.kanban-column {
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
min-height: 150px;
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
transition: box-shadow 0.2s ease;
|
|
}
|
|
|
|
.kanban-column:hover {
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
}
|
|
|
|
.kanban-column-header {
|
|
padding: 16px 18px;
|
|
border-bottom: 2px solid #f3f4f6;
|
|
background: linear-gradient(to bottom, #ffffff 0%, #fafbfc 100%);
|
|
border-top-left-radius: 12px;
|
|
border-top-right-radius: 12px;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 10;
|
|
}
|
|
|
|
.kanban-column-title {
|
|
font-size: 0.9375rem;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
letter-spacing: -0.01em;
|
|
}
|
|
|
|
/* Status Icons */
|
|
.kanban-status-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 14px;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.kanban-status-todo {
|
|
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 100%);
|
|
color: #64748b;
|
|
}
|
|
|
|
.kanban-status-in_progress {
|
|
background: linear-gradient(135deg, #fef3c7 0%, #fde68a 100%);
|
|
color: #d97706;
|
|
}
|
|
|
|
.kanban-status-review {
|
|
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
|
color: #2563eb;
|
|
}
|
|
|
|
.kanban-status-done {
|
|
background: linear-gradient(135deg, #d1fae5 0%, #a7f3d0 100%);
|
|
color: #059669;
|
|
}
|
|
|
|
/* Count Badge */
|
|
.kanban-count {
|
|
min-width: 28px;
|
|
height: 28px;
|
|
padding: 0 10px;
|
|
border-radius: 14px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.8125rem;
|
|
font-weight: 600;
|
|
background: #f3f4f6;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.kanban-count-todo { background: #f1f5f9; color: #64748b; }
|
|
.kanban-count-in_progress { background: #fef3c7; color: #d97706; }
|
|
.kanban-count-review { background: #dbeafe; color: #2563eb; }
|
|
.kanban-count-done { background: #d1fae5; color: #059669; }
|
|
|
|
/* Column Body */
|
|
.kanban-column-body {
|
|
padding: 16px;
|
|
min-height: 150px;
|
|
max-height: 75vh;
|
|
overflow-y: auto;
|
|
overflow-x: hidden;
|
|
background: #fafbfc;
|
|
border-bottom-left-radius: 12px;
|
|
border-bottom-right-radius: 12px;
|
|
}
|
|
|
|
.kanban-column-body::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
|
|
.kanban-column-body::-webkit-scrollbar-track {
|
|
background: transparent;
|
|
}
|
|
|
|
.kanban-column-body::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
.kanban-column-body::-webkit-scrollbar-thumb:hover {
|
|
background: #94a3b8;
|
|
}
|
|
|
|
/* Drag and Drop States */
|
|
.kanban-column-body.drag-over {
|
|
background: rgba(59, 130, 246, 0.05);
|
|
outline: 2px dashed #3b82f6;
|
|
outline-offset: -8px;
|
|
}
|
|
|
|
/* ============================================
|
|
KANBAN CARDS - ENHANCED DESIGN
|
|
============================================ */
|
|
|
|
.kanban-card {
|
|
background: #ffffff;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 10px;
|
|
margin-bottom: 12px;
|
|
cursor: grab;
|
|
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.kanban-card:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
|
|
border-color: #cbd5e1;
|
|
}
|
|
|
|
.kanban-card:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.kanban-card.dragging {
|
|
opacity: 0.5;
|
|
cursor: grabbing;
|
|
transform: rotate(3deg);
|
|
}
|
|
|
|
/* Active Card State */
|
|
.kanban-card-active {
|
|
border-color: #3b82f6 !important;
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1), 0 4px 12px rgba(59, 130, 246, 0.15);
|
|
}
|
|
|
|
.kanban-card-active:hover {
|
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15), 0 8px 20px rgba(59, 130, 246, 0.2);
|
|
}
|
|
|
|
/* Card Header */
|
|
.kanban-card-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 10px 14px 8px 14px;
|
|
background: linear-gradient(to bottom, #fafbfc 0%, #ffffff 100%);
|
|
border-bottom: 1px solid #f3f4f6;
|
|
}
|
|
|
|
.kanban-card-id {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #9ca3af;
|
|
letter-spacing: 0.02em;
|
|
}
|
|
|
|
.kanban-card-actions {
|
|
display: flex;
|
|
gap: 4px;
|
|
opacity: 0;
|
|
transition: opacity 0.2s ease;
|
|
}
|
|
|
|
.kanban-card:hover .kanban-card-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.kanban-action-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: none;
|
|
border-radius: 6px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
background: #f3f4f6;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.kanban-action-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.kanban-action-play {
|
|
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
|
color: white;
|
|
}
|
|
|
|
.kanban-action-play:hover {
|
|
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
|
}
|
|
|
|
.kanban-action-stop {
|
|
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
|
|
color: white;
|
|
}
|
|
|
|
.kanban-action-stop:hover {
|
|
box-shadow: 0 4px 12px rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
.kanban-action-edit:hover {
|
|
background: #e5e7eb;
|
|
color: #374151;
|
|
}
|
|
|
|
/* Priority Indicator */
|
|
.kanban-card-priority {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
border-top-left-radius: 10px;
|
|
border-bottom-left-radius: 10px;
|
|
}
|
|
|
|
.kanban-priority-low {
|
|
background: linear-gradient(to bottom, #10b981 0%, #059669 100%);
|
|
}
|
|
|
|
.kanban-priority-medium {
|
|
background: linear-gradient(to bottom, #f59e0b 0%, #d97706 100%);
|
|
}
|
|
|
|
.kanban-priority-high {
|
|
background: linear-gradient(to bottom, #f97316 0%, #ea580c 100%);
|
|
}
|
|
|
|
.kanban-priority-urgent {
|
|
background: linear-gradient(to bottom, #ef4444 0%, #dc2626 100%);
|
|
box-shadow: 0 0 8px rgba(239, 68, 68, 0.3);
|
|
}
|
|
|
|
/* Card Content */
|
|
.kanban-card-content {
|
|
padding: 14px;
|
|
}
|
|
|
|
.kanban-card-title {
|
|
font-size: 0.9375rem;
|
|
font-weight: 600;
|
|
line-height: 1.4;
|
|
margin-bottom: 8px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.kanban-card-title a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
transition: color 0.15s ease;
|
|
}
|
|
|
|
.kanban-card-title a:hover {
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.kanban-card-description {
|
|
font-size: 0.8125rem;
|
|
line-height: 1.5;
|
|
color: #6b7280;
|
|
margin-bottom: 12px;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* Badges */
|
|
.kanban-card-badges {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.kanban-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 0.6875rem;
|
|
font-weight: 600;
|
|
letter-spacing: 0.02em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.kanban-badge i {
|
|
font-size: 10px;
|
|
}
|
|
|
|
.kanban-badge-priority-low {
|
|
background: #d1fae5;
|
|
color: #065f46;
|
|
}
|
|
|
|
.kanban-badge-priority-medium {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.kanban-badge-priority-high {
|
|
background: #fed7aa;
|
|
color: #9a3412;
|
|
}
|
|
|
|
.kanban-badge-priority-urgent {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
.kanban-badge-active {
|
|
background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%);
|
|
color: #1e40af;
|
|
animation: pulse 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 1; }
|
|
50% { opacity: 0.8; }
|
|
}
|
|
|
|
.kanban-badge-overdue {
|
|
background: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* Progress Bar */
|
|
.kanban-card-progress {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.kanban-progress-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.kanban-progress-label {
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
color: #6b7280;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
}
|
|
|
|
.kanban-progress-value {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #3b82f6;
|
|
}
|
|
|
|
.kanban-progress-bar {
|
|
height: 6px;
|
|
background: #e5e7eb;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.kanban-progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #3b82f6 0%, #2563eb 100%);
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
/* Card Footer */
|
|
.kanban-card-footer {
|
|
padding: 0 14px 14px 14px;
|
|
}
|
|
|
|
.kanban-card-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
}
|
|
|
|
.kanban-meta-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 0.8125rem;
|
|
color: #6b7280;
|
|
padding: 6px 10px;
|
|
background: #f9fafb;
|
|
border-radius: 6px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.kanban-meta-item i {
|
|
font-size: 12px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.kanban-meta-assignee {
|
|
color: #0ea5e9;
|
|
background: #f0f9ff;
|
|
}
|
|
|
|
.kanban-meta-assignee i {
|
|
color: #0ea5e9;
|
|
}
|
|
|
|
.kanban-meta-date {
|
|
color: #6b7280;
|
|
}
|
|
|
|
.kanban-meta-overdue {
|
|
color: #dc2626;
|
|
background: #fef2f2;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.kanban-meta-overdue i {
|
|
color: #dc2626;
|
|
}
|
|
|
|
/* Responsive Design */
|
|
@media (max-width: 768px) {
|
|
.kanban-board {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.kanban-card-meta {
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
}
|
|
|
|
.kanban-meta-item {
|
|
width: 100%;
|
|
}
|
|
}
|
|
|
|
/* Dark Mode Support */
|
|
[data-theme="dark"] .kanban-toolbar-title,
|
|
[data-theme="dark"] .kanban-column-title {
|
|
color: #f1f5f9;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-column {
|
|
background: #0f172a;
|
|
border-color: #1e293b;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-column-header {
|
|
background: linear-gradient(to bottom, #0f172a 0%, #0a0f1e 100%);
|
|
border-bottom-color: #1e293b;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-column-body {
|
|
background: #0a0f1e;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card {
|
|
background: #1e293b;
|
|
border-color: #334155;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card:hover {
|
|
border-color: #475569;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card-header {
|
|
background: linear-gradient(to bottom, #1e293b 0%, #1e293b 100%);
|
|
border-bottom-color: #334155;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card-id {
|
|
color: #64748b;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card-title {
|
|
color: #f1f5f9;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-card-description {
|
|
color: #94a3b8;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-action-btn {
|
|
background: #334155;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-action-edit:hover {
|
|
background: #475569;
|
|
color: #e2e8f0;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-meta-item {
|
|
background: #0f172a;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-progress-bar {
|
|
background: #334155;
|
|
}
|
|
|
|
[data-theme="dark"] .kanban-count {
|
|
background: #1e293b;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
/* Professional Task Modal Styles */
|
|
.task-modal-content { border-radius: 16px; border: none; box-shadow: 0 20px 50px rgba(0,0,0,0.15); overflow: hidden; }
|
|
.task-modal-icon { width: 40px; height: 40px; border-radius: 10px; background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 18px; box-shadow: 0 4px 12px rgba(59, 130, 246, 0.25); }
|
|
.task-modal-title { font-size: 1.25rem; font-weight: 600; color: var(--text-primary); margin: 0; line-height: 1.4; }
|
|
.task-modal-label { display: block; font-size: 0.8125rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
|
.task-modal-field { padding: 12px; background: var(--light-color, #f8fafc); border-radius: 10px; transition: all 0.2s ease; }
|
|
.task-modal-field:hover { background: var(--surface-hover, #f1f5f9); }
|
|
.task-modal-value { font-size: 0.9375rem; color: var(--text-primary); font-weight: 500; }
|
|
.task-modal-section { padding: 0; }
|
|
.task-modal-description { padding: 16px; background: var(--light-color, #f8fafc); border-radius: 10px; color: var(--text-primary); line-height: 1.6; border-left: 3px solid var(--primary-color); }
|
|
.task-modal-project-card { padding: 14px; background: var(--light-color, #f8fafc); border-radius: 10px; border-left: 3px solid var(--primary-color); display: flex; align-items: center; gap: 12px; transition: all 0.2s ease; }
|
|
.task-modal-project-card:hover { background: var(--surface-hover, #f1f5f9); transform: translateX(2px); }
|
|
.task-modal-project-icon { width: 36px; height: 36px; border-radius: 8px; background: var(--primary-color); background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 14px; }
|
|
.task-modal-progress { height: 10px; border-radius: 10px; background: var(--light-color, #e5e7eb); overflow: hidden; }
|
|
.task-modal-progress .progress-bar { background: linear-gradient(90deg, var(--primary-color) 0%, #2563eb 100%); border-radius: 10px; transition: width 0.4s ease; }
|
|
.task-modal-progress-text { font-size: 0.875rem; font-weight: 600; color: var(--primary-color); min-width: 45px; text-align: right; }
|
|
.task-modal-stat { display: flex; align-items: center; gap: 12px; padding: 14px; background: var(--light-color, #f8fafc); border-radius: 10px; transition: all 0.2s ease; }
|
|
.task-modal-stat:hover { background: var(--surface-hover, #f1f5f9); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.05); }
|
|
.task-modal-stat-icon { width: 40px; height: 40px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; }
|
|
.task-modal-stat-content { flex-grow: 1; min-width: 0; }
|
|
.task-modal-stat-value { font-size: 1.125rem; font-weight: 700; color: var(--text-primary); line-height: 1.2; }
|
|
.task-modal-stat-label { font-size: 0.75rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 2px; }
|
|
.bg-primary-subtle { background-color: rgba(59, 130, 246, 0.1); }
|
|
.bg-info-subtle { background-color: rgba(14, 165, 233, 0.1); }
|
|
.bg-success-subtle { background-color: rgba(34, 197, 94, 0.1); }
|
|
|
|
/* Dark Mode Support */
|
|
[data-theme="dark"] .task-modal-content { background: #0f172a; }
|
|
[data-theme="dark"] .task-modal-title { color: var(--text-secondary); }
|
|
[data-theme="dark"] .task-modal-field { background: #1e293b; }
|
|
[data-theme="dark"] .task-modal-field:hover { background: #334155; }
|
|
[data-theme="dark"] .task-modal-description { background: #1e293b; color: var(--text-secondary); }
|
|
[data-theme="dark"] .task-modal-project-card { background: #1e293b; }
|
|
[data-theme="dark"] .task-modal-project-card:hover { background: #334155; }
|
|
[data-theme="dark"] .task-modal-progress { background: #1e293b; }
|
|
[data-theme="dark"] .task-modal-stat { background: #1e293b; }
|
|
[data-theme="dark"] .task-modal-stat:hover { background: #334155; }
|
|
[data-theme="dark"] .task-modal-value { color: var(--text-secondary); }
|
|
[data-theme="dark"] .modal-header { background: #0f172a; border-color: #1e293b; }
|
|
[data-theme="dark"] .modal-body { background: #0f172a; }
|
|
[data-theme="dark"] .modal-footer { background: #0f172a; border-color: #1e293b; }
|
|
|
|
/* Responsive */
|
|
@media (max-width: 576px) {
|
|
.task-modal-stat { padding: 10px; }
|
|
.task-modal-stat-icon { width: 32px; height: 32px; font-size: 14px; }
|
|
.task-modal-stat-value { font-size: 1rem; }
|
|
.task-modal-stat-label { font-size: 0.7rem; }
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function(){
|
|
const board = document.getElementById('kanbanBoard');
|
|
if (!board) return;
|
|
|
|
const statusLabels = {
|
|
todo: '{{ _('To Do') }}',
|
|
in_progress: '{{ _('In Progress') }}',
|
|
review: '{{ _('Review') }}',
|
|
done: '{{ _('Done') }}',
|
|
cancelled: '{{ _('Cancelled') }}'
|
|
};
|
|
const updateUrlTemplate = "{{ url_for('tasks.api_update_status', task_id=0) }}"; // will replace 0 with actual id
|
|
|
|
|
|
function updateCounts() {
|
|
document.querySelectorAll('.kanban-column').forEach(col => {
|
|
const body = col.querySelector('.kanban-column-body');
|
|
const count = body ? body.querySelectorAll('.kanban-card').length : 0;
|
|
const badge = col.querySelector('.kanban-count');
|
|
if (badge) badge.textContent = count;
|
|
});
|
|
}
|
|
|
|
let dragCard = null;
|
|
board.addEventListener('dragstart', (e) => {
|
|
const card = e.target.closest('.kanban-card');
|
|
if (!card) return;
|
|
dragCard = card;
|
|
card.classList.add('dragging');
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
e.dataTransfer.setData('text/plain', card.dataset.taskId);
|
|
});
|
|
|
|
board.addEventListener('dragend', () => {
|
|
if (dragCard) dragCard.classList.remove('dragging');
|
|
dragCard = null;
|
|
document.querySelectorAll('.kanban-column-body').forEach(b => b.classList.remove('drag-over'));
|
|
});
|
|
|
|
document.querySelectorAll('.kanban-column-body').forEach(body => {
|
|
body.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
e.dataTransfer.dropEffect = 'move';
|
|
body.classList.add('drag-over');
|
|
});
|
|
body.addEventListener('dragleave', () => body.classList.remove('drag-over'));
|
|
body.addEventListener('drop', async (e) => {
|
|
e.preventDefault();
|
|
body.classList.remove('drag-over');
|
|
const targetStatus = body.dataset.status;
|
|
const taskId = e.dataTransfer.getData('text/plain');
|
|
const card = dragCard || board.querySelector(`.kanban-card[data-task-id="${taskId}"]`);
|
|
if (!card) return;
|
|
const originalParent = card.parentElement;
|
|
const originalStatus = card.dataset.status;
|
|
if (targetStatus === originalStatus) {
|
|
body.appendChild(card);
|
|
return;
|
|
}
|
|
body.appendChild(card);
|
|
updateCounts();
|
|
// optimistic UI: update card status
|
|
card.dataset.status = targetStatus;
|
|
|
|
const url = updateUrlTemplate.replace('/0/', `/${taskId}/`);
|
|
try {
|
|
const resp = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
|
|
body: JSON.stringify({ status: targetStatus })
|
|
});
|
|
if (!resp.ok) {
|
|
throw new Error('{{ _('Failed to update status') }}');
|
|
}
|
|
const data = await resp.json();
|
|
if (!data.success) {
|
|
throw new Error(data.error || 'Update rejected');
|
|
}
|
|
} catch (err) {
|
|
// revert
|
|
if (originalParent) originalParent.appendChild(card);
|
|
card.dataset.status = originalStatus;
|
|
updateCounts();
|
|
alert('{{ _('Failed to update task status') }}');
|
|
}
|
|
});
|
|
});
|
|
|
|
updateCounts();
|
|
})();
|
|
</script>
|
|
|
|
<!-- Task Modal JavaScript -->
|
|
<script>
|
|
// Global function to open task modal
|
|
window.openTaskModal = async function(taskId) {
|
|
const modal = new bootstrap.Modal(document.getElementById('taskQuickViewModal'));
|
|
const modalEl = document.getElementById('taskQuickViewModal');
|
|
|
|
// Show loading state
|
|
document.querySelector('.task-modal-loading').style.display = 'block';
|
|
document.querySelector('.task-modal-content-wrapper').style.display = 'none';
|
|
|
|
modal.show();
|
|
|
|
try {
|
|
// Fetch task details from API
|
|
const response = await fetch(`/api/tasks/${taskId}`);
|
|
if (!response.ok) throw new Error('Failed to load task');
|
|
|
|
const task = await response.json();
|
|
|
|
// Populate modal with task data
|
|
document.getElementById('taskModalName').textContent = task.name;
|
|
document.getElementById('taskModalId').textContent = `#${task.id}`;
|
|
|
|
// Status badge
|
|
const statusBadge = `<span class="status-badge status-${task.status}">${task.status_display || task.status}</span>`;
|
|
document.getElementById('taskModalStatus').innerHTML = statusBadge;
|
|
|
|
// Priority badge
|
|
const priorityBadge = `<span class="priority-badge priority-${task.priority}">${task.priority_display || task.priority}</span>`;
|
|
document.getElementById('taskModalPriority').innerHTML = priorityBadge;
|
|
|
|
// Project info
|
|
const projectHtml = `
|
|
<div class="task-modal-project-icon">
|
|
<i class="fas fa-project-diagram"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold" style="color: var(--text-primary);">${task.project?.name || 'N/A'}</div>
|
|
<small class="text-muted">${task.project?.client || ''}</small>
|
|
</div>
|
|
`;
|
|
document.getElementById('taskModalProject').innerHTML = projectHtml;
|
|
|
|
// Description
|
|
const descSection = document.getElementById('taskModalDescriptionSection');
|
|
if (task.description && task.description.trim()) {
|
|
descSection.style.display = 'block';
|
|
document.getElementById('taskModalDescription').textContent = task.description;
|
|
} else {
|
|
descSection.style.display = 'none';
|
|
}
|
|
|
|
// Assigned user
|
|
const assignedSection = document.getElementById('taskModalAssignedSection');
|
|
if (task.assigned_user) {
|
|
assignedSection.style.display = 'block';
|
|
const assignedHtml = `
|
|
<div class="d-flex align-items-center gap-2">
|
|
<div class="bg-primary bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center" style="width: 28px; height: 28px;">
|
|
<i class="fas fa-user text-primary" style="font-size: 12px;"></i>
|
|
</div>
|
|
<span>${task.assigned_user.display_name || task.assigned_user.username}</span>
|
|
</div>
|
|
`;
|
|
document.getElementById('taskModalAssigned').innerHTML = assignedHtml;
|
|
} else {
|
|
assignedSection.style.display = 'none';
|
|
}
|
|
|
|
// Due date
|
|
const dueDateSection = document.getElementById('taskModalDueDateSection');
|
|
if (task.due_date) {
|
|
dueDateSection.style.display = 'block';
|
|
const dueDate = new Date(task.due_date);
|
|
const isOverdue = task.is_overdue || (dueDate < new Date() && task.status !== 'done');
|
|
const dueDateHtml = `
|
|
<span class="${isOverdue ? 'text-danger fw-semibold' : ''}">
|
|
<i class="fas fa-calendar me-1"></i>
|
|
${dueDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })}
|
|
${isOverdue ? '<span class="badge bg-danger ms-2">Overdue</span>' : ''}
|
|
</span>
|
|
`;
|
|
document.getElementById('taskModalDueDate').innerHTML = dueDateHtml;
|
|
} else {
|
|
dueDateSection.style.display = 'none';
|
|
}
|
|
|
|
// Progress
|
|
const progressSection = document.getElementById('taskModalProgressSection');
|
|
const estimatedSection2 = document.getElementById('taskModalEstimatedSection2');
|
|
if (task.estimated_hours && task.estimated_hours > 0) {
|
|
progressSection.style.display = 'block';
|
|
estimatedSection2.style.display = 'block';
|
|
|
|
const progress = task.progress_percentage || 0;
|
|
document.getElementById('taskModalProgressBar').style.width = `${progress}%`;
|
|
document.getElementById('taskModalProgressText').textContent = `${progress}%`;
|
|
|
|
const tracked = task.total_hours || 0;
|
|
document.getElementById('taskModalHoursInfo').textContent =
|
|
`${tracked.toFixed(1)}h tracked / ${task.estimated_hours.toFixed(1)}h estimated`;
|
|
document.getElementById('taskModalEstimated').textContent = `${task.estimated_hours.toFixed(1)}h`;
|
|
} else {
|
|
progressSection.style.display = 'none';
|
|
estimatedSection2.style.display = 'none';
|
|
}
|
|
|
|
// Tags
|
|
const tagsSection = document.getElementById('taskModalTagsSection');
|
|
if (task.tags && task.tags.length > 0) {
|
|
tagsSection.style.display = 'block';
|
|
const tagsHtml = task.tags.map(tag =>
|
|
`<span class="badge bg-light text-dark border">${tag}</span>`
|
|
).join('');
|
|
document.getElementById('taskModalTags').innerHTML = tagsHtml;
|
|
} else {
|
|
tagsSection.style.display = 'none';
|
|
}
|
|
|
|
// Stats
|
|
document.getElementById('taskModalTotalHours').textContent =
|
|
`${(task.total_hours || 0).toFixed(1)}h`;
|
|
|
|
// Created date
|
|
const createdDate = new Date(task.created_at);
|
|
document.getElementById('taskModalCreated').textContent =
|
|
createdDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
|
|
// Completed date
|
|
const completedSection = document.getElementById('taskModalCompletedSection');
|
|
if (task.completed_at) {
|
|
completedSection.style.display = 'block';
|
|
const completedDate = new Date(task.completed_at);
|
|
document.getElementById('taskModalCompleted').textContent =
|
|
completedDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
} else {
|
|
completedSection.style.display = 'none';
|
|
}
|
|
|
|
// Update action buttons
|
|
document.getElementById('taskModalViewFullBtn').href = `/tasks/${taskId}`;
|
|
document.getElementById('taskModalEditBtn').href = `/tasks/${taskId}/edit`;
|
|
|
|
// Hide loading, show content
|
|
document.querySelector('.task-modal-loading').style.display = 'none';
|
|
document.querySelector('.task-modal-content-wrapper').style.display = 'block';
|
|
|
|
} catch (error) {
|
|
console.error('Error loading task:', error);
|
|
document.querySelector('.task-modal-loading').innerHTML = `
|
|
<div class="text-danger">
|
|
<i class="fas fa-exclamation-triangle fa-2x mb-3"></i>
|
|
<p>Failed to load task details</p>
|
|
<button class="btn btn-sm btn-secondary" onclick="bootstrap.Modal.getInstance(document.getElementById('taskQuickViewModal')).hide();">Close</button>
|
|
</div>
|
|
`;
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<!-- Professional Task Modal -->
|
|
<div class="modal fade" id="taskQuickViewModal" tabindex="-1" aria-labelledby="taskModalTitle" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
|
|
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable modal-lg">
|
|
<div class="modal-content task-modal-content">
|
|
<!-- Modal Header -->
|
|
<div class="modal-header border-bottom-0 pb-2">
|
|
<div class="d-flex align-items-center gap-2 flex-grow-1">
|
|
<div class="task-modal-icon">
|
|
<i class="fas fa-tasks"></i>
|
|
</div>
|
|
<div class="flex-grow-1">
|
|
<h5 class="modal-title task-modal-title" id="taskModalTitle">
|
|
<span id="taskModalName">Loading...</span>
|
|
</h5>
|
|
<small class="text-muted" id="taskModalId"></small>
|
|
</div>
|
|
</div>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
|
|
<!-- Modal Body -->
|
|
<div class="modal-body pt-2" id="taskModalBody">
|
|
<div class="task-modal-loading text-center py-5">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="text-muted mt-3">{{ _('Loading task details...') }}</p>
|
|
</div>
|
|
|
|
<div class="task-modal-content-wrapper" style="display: none;">
|
|
<!-- Status & Priority Row -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-sm-6">
|
|
<div class="task-modal-field">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-flag me-2"></i>{{ _('Status') }}
|
|
</label>
|
|
<div id="taskModalStatus"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6">
|
|
<div class="task-modal-field">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-exclamation-circle me-2"></i>{{ _('Priority') }}
|
|
</label>
|
|
<div id="taskModalPriority"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Project Info -->
|
|
<div class="task-modal-section mb-4">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-project-diagram me-2"></i>{{ _('Project') }}
|
|
</label>
|
|
<div class="task-modal-project-card" id="taskModalProject"></div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="task-modal-section mb-4" id="taskModalDescriptionSection" style="display: none;">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-align-left me-2"></i>{{ _('Description') }}
|
|
</label>
|
|
<div class="task-modal-description" id="taskModalDescription"></div>
|
|
</div>
|
|
|
|
<!-- Details Grid -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-sm-6" id="taskModalAssignedSection" style="display: none;">
|
|
<div class="task-modal-field">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-user me-2"></i>{{ _('Assigned To') }}
|
|
</label>
|
|
<div id="taskModalAssigned" class="task-modal-value"></div>
|
|
</div>
|
|
</div>
|
|
<div class="col-sm-6" id="taskModalDueDateSection" style="display: none;">
|
|
<div class="task-modal-field">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-calendar me-2"></i>{{ _('Due Date') }}
|
|
</label>
|
|
<div id="taskModalDueDate" class="task-modal-value"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress Bar (if estimated hours exist) -->
|
|
<div class="task-modal-section mb-4" id="taskModalProgressSection" style="display: none;">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-chart-line me-2"></i>{{ _('Progress') }}
|
|
</label>
|
|
<div class="d-flex align-items-center gap-3">
|
|
<div class="flex-grow-1">
|
|
<div class="progress task-modal-progress">
|
|
<div class="progress-bar" role="progressbar" id="taskModalProgressBar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<span class="task-modal-progress-text" id="taskModalProgressText">0%</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mt-2 small text-muted">
|
|
<span id="taskModalHoursInfo"></span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tags -->
|
|
<div class="task-modal-section mb-4" id="taskModalTagsSection" style="display: none;">
|
|
<label class="task-modal-label">
|
|
<i class="fas fa-tags me-2"></i>{{ _('Tags') }}
|
|
</label>
|
|
<div id="taskModalTags" class="d-flex flex-wrap gap-2"></div>
|
|
</div>
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-6 col-md-3">
|
|
<div class="task-modal-stat">
|
|
<div class="task-modal-stat-icon bg-primary-subtle">
|
|
<i class="fas fa-clock text-primary"></i>
|
|
</div>
|
|
<div class="task-modal-stat-content">
|
|
<div class="task-modal-stat-value" id="taskModalTotalHours">0h</div>
|
|
<div class="task-modal-stat-label">{{ _('Tracked') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3" id="taskModalEstimatedSection2">
|
|
<div class="task-modal-stat">
|
|
<div class="task-modal-stat-icon bg-info-subtle">
|
|
<i class="fas fa-hourglass-half text-info"></i>
|
|
</div>
|
|
<div class="task-modal-stat-content">
|
|
<div class="task-modal-stat-value" id="taskModalEstimated">0h</div>
|
|
<div class="task-modal-stat-label">{{ _('Estimated') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<div class="task-modal-stat">
|
|
<div class="task-modal-stat-icon bg-success-subtle">
|
|
<i class="fas fa-calendar-plus text-success"></i>
|
|
</div>
|
|
<div class="task-modal-stat-content">
|
|
<div class="task-modal-stat-value" id="taskModalCreated">-</div>
|
|
<div class="task-modal-stat-label">{{ _('Created') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3" id="taskModalCompletedSection" style="display: none;">
|
|
<div class="task-modal-stat">
|
|
<div class="task-modal-stat-icon bg-success-subtle">
|
|
<i class="fas fa-check-circle text-success"></i>
|
|
</div>
|
|
<div class="task-modal-stat-content">
|
|
<div class="task-modal-stat-value" id="taskModalCompleted">-</div>
|
|
<div class="task-modal-stat-label">{{ _('Completed') }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Footer -->
|
|
<div class="modal-footer border-top-0 pt-2 flex-wrap gap-2">
|
|
<a href="#" id="taskModalViewFullBtn" class="btn btn-outline-secondary btn-sm" target="_blank">
|
|
<i class="fas fa-external-link-alt me-2"></i>{{ _('View Full Details') }}
|
|
</a>
|
|
<a href="#" id="taskModalEditBtn" class="btn btn-primary btn-sm">
|
|
<i class="fas fa-edit me-2"></i>{{ _('Edit Task') }}
|
|
</a>
|
|
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>{{ _('Close') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|