mirror of
https://github.com/DRYTRIX/TimeTracker.git
synced 2026-05-06 20:40:38 -05:00
14ae197266
- share a centralized timezone list across admin and user settings - allow admins to pick from the same list when setting the system default - let users clear their personal override to fall back to the global default - add regression tests covering the new helper and reset path
662 lines
33 KiB
HTML
662 lines
33 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}{{ _('Edit Task') }} - {{ task.name }} - Time Tracker{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css">
|
|
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/theme/toastui-editor-dark.css">
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold">{{ _('Edit Task') }}</h1>
|
|
<p class="text-text-muted-light dark:text-text-muted-dark">{{ _('Update task details and settings for "%(task)s"', task=task.name) }}</p>
|
|
</div>
|
|
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="bg-gray-200 dark:bg-gray-700 px-4 py-2 rounded-lg mt-4 md:mt-0">{{ _('Back to Task') }}</a>
|
|
</div>
|
|
|
|
<!-- Header -->
|
|
<div class="bg-card-light dark:bg-card-dark p-6 rounded-lg shadow mb-6">
|
|
<div class="flex items-center">
|
|
<div class="flex items-center justify-center mr-3 rounded-full bg-yellow-100 dark:bg-yellow-900/30" style="width:48px;height:48px;">
|
|
<i class="fas fa-edit text-yellow-600"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-semibold">{{ _('Edit Task') }}</h2>
|
|
<p class="text-text-muted-light dark:text-text-muted-dark">{{ _('Update task details and settings for "%(task)s"', task=task.name) }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Task Form -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<div class="lg:col-span-2">
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow">
|
|
<div class="border-b border-border-light dark:border-border-dark p-4">
|
|
<h6 class="font-semibold flex items-center gap-2"><i class="fas fa-edit text-yellow-600"></i>{{ _('Task Information') }}</h6>
|
|
</div>
|
|
<div class="p-4">
|
|
<form method="POST" id="editTaskForm" novalidate data-validate-form>
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<!-- Task Name -->
|
|
<div class="mb-4">
|
|
<label for="name" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
<i class="fas fa-tag mr-2 text-primary"></i>{{ _('Task Name') }} <span class="text-red-500">*</span>
|
|
</label>
|
|
<input type="text" class="form-input" id="name" name="name"
|
|
value="{{ task.name }}" placeholder="{{ _('Enter a descriptive task name') }}" required>
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Choose a clear, descriptive name that explains what needs to be done') }}</p>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="mb-4">
|
|
<label for="description" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
<span><i class="fas fa-align-left mr-2 text-primary"></i>{{ _('Description') }}</span>
|
|
</label>
|
|
<div class="markdown-editor-wrapper">
|
|
<textarea class="form-input hidden" id="description" name="description" rows="12" placeholder="{{ _('Provide detailed information about the task, requirements, and any specific instructions...') }}">{{ task.description or '' }}</textarea>
|
|
<div id="description_editor"></div>
|
|
</div>
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Optional: Add context, requirements, or specific instructions for the task') }}</p>
|
|
</div>
|
|
|
|
<!-- Project Selection -->
|
|
<div class="mb-4">
|
|
<label for="project_id" class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
<i class="fas fa-project-diagram mr-2 text-primary"></i>{{ _('Project') }} <span class="text-red-500">*</span>
|
|
</label>
|
|
<select class="form-input" id="project_id" name="project_id" required>
|
|
<option value="">{{ _('Select a project') }}</option>
|
|
{% for project in projects %}
|
|
<option value="{{ project.id }}" {% if task.project_id == project.id %}selected{% endif %}>
|
|
{{ project.name }}{% if project.code_display %} [{{ project.code_display }}]{% endif %} ({{ project.client }})
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Select the project this task belongs to') }}</p>
|
|
</div>
|
|
|
|
<!-- Priority and Status -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-2">
|
|
<div>
|
|
<label for="priority" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Priority') }}</label>
|
|
<select id="priority" name="priority" class="form-input">
|
|
<option value="low" {% if task.priority == 'low' %}selected{% endif %}>{{ _('Low') }}</option>
|
|
<option value="medium" {% if task.priority == 'medium' %}selected{% endif %}>{{ _('Medium') }}</option>
|
|
<option value="high" {% if task.priority == 'high' %}selected{% endif %}>{{ _('High') }}</option>
|
|
<option value="urgent" {% if task.priority == 'urgent' %}selected{% endif %}>{{ _('Urgent') }}</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="status" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Status') }}</label>
|
|
<select id="status" name="status" class="form-input">
|
|
<option value="todo" {% if task.status == 'todo' %}selected{% endif %}>{{ _('To Do') }}</option>
|
|
<option value="in_progress" {% if task.status == 'in_progress' %}selected{% endif %}>{{ _('In Progress') }}</option>
|
|
<option value="review" {% if task.status == 'review' %}selected{% endif %}>{{ _('Review') }}</option>
|
|
<option value="done" {% if task.status == 'done' %}selected{% endif %}>{{ _('Done') }}</option>
|
|
<option value="cancelled" {% if task.status == 'cancelled' %}selected{% endif %}>{{ _('Cancelled') }}</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 mb-4 text-xs">
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Preview') }}:</span>
|
|
<span id="priorityPreview" class="priority-badge priority-{{ task.priority }}">
|
|
{{ task.priority_display }}
|
|
</span>
|
|
<span id="statusPreview" class="status-badge status-{{ task.status }}">
|
|
<span class="whitespace-nowrap">{{ task.status_display }}</span>
|
|
</span>
|
|
</div>
|
|
|
|
<!-- Due Date and Estimated Hours -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<div>
|
|
<label for="due_date" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Due Date') }}</label>
|
|
<input type="date" id="due_date" name="due_date" value="{{ task.due_date.strftime('%Y-%m-%d') if task.due_date else '' }}" class="form-input">
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Optional: Set a deadline for this task') }}</p>
|
|
</div>
|
|
<div>
|
|
<label for="estimated_hours" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Estimated Hours') }}</label>
|
|
<input type="number" id="estimated_hours" name="estimated_hours" value="{{ task.estimated_hours or '' }}" step="0.5" min="0" placeholder="0.0" class="form-input">
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Optional: Estimate how long this task will take') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assignment -->
|
|
<div class="mb-4">
|
|
<label for="assigned_to" class="block text-sm font-medium text-gray-700 dark:text-gray-300">{{ _('Assign To') }}</label>
|
|
<select id="assigned_to" name="assigned_to" class="form-input">
|
|
<option value="">{{ _('Unassigned') }}</option>
|
|
{% for user in users %}
|
|
<option value="{{ user.id }}" {% if task.assigned_to == user.id %}selected{% endif %}>{{ user.display_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
<p class="text-xs text-text-muted-light dark:text-text-muted-dark mt-1">{{ _('Optional: Assign this task to a team member') }}</p>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="mt-6 flex justify-end gap-3 border-t border-border-light dark:border-border-dark pt-4">
|
|
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="bg-gray-200 dark:bg-gray-700 px-4 py-2 rounded-lg">{{ _('Cancel') }}</a>
|
|
<button type="submit" class="bg-primary text-white px-4 py-2 rounded-lg">{{ _('Update Task') }}</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="lg:col-span-1 space-y-4">
|
|
<!-- Progress -->
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow">
|
|
<div class="border-b border-border-light dark:border-border-dark p-4">
|
|
<h6 class="font-semibold flex items-center gap-2"><i class="fas fa-chart-line text-primary"></i>{{ _('Progress') }}</h6>
|
|
</div>
|
|
<div class="p-4" data-testid="task-edit-progress">
|
|
<div class="flex items-center justify-between text-sm mb-2">
|
|
<span class="text-text-muted-light dark:text-text-muted-dark">{{ _('Estimate used') }}</span>
|
|
<span class="font-medium">{{ task.progress_percentage }}%</span>
|
|
</div>
|
|
<div class="h-2 rounded bg-border-light dark:bg-border-dark overflow-hidden">
|
|
<div class="h-2 bg-primary rounded" style="width: {{ task.progress_percentage }}%"></div>
|
|
</div>
|
|
<div class="mt-3 text-xs text-text-muted-light dark:text-text-muted-dark">
|
|
<span>{{ _('Actual') }}: {{ task.total_hours }} {{ _('h') }}</span>
|
|
{% if task.estimated_hours %}
|
|
<span class="ml-2">· {{ _('Estimated') }}: {{ task.estimated_hours }} {{ _('h') }}</span>
|
|
{% endif %}
|
|
{% if task.total_billable_hours %}
|
|
<span class="ml-2">· {{ _('Billable') }}: {{ task.total_billable_hours }} {{ _('h') }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Current Task Info -->
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow mb-4">
|
|
<div class="border-b border-border-light dark:border-border-dark p-4">
|
|
<h6 class="font-semibold flex items-center gap-2"><i class="fas fa-info-circle text-sky-600"></i>{{ _('Current Task Info') }}</h6>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Current Status') }}</small>
|
|
<span class="status-badge status-{{ task.status }}">
|
|
{{ task.status_display }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Current Priority') }}</small>
|
|
<span class="priority-badge priority-{{ task.priority }}">
|
|
{{ task.priority_display }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Project') }}</small>
|
|
<div class="flex items-center">
|
|
<div class="rounded-full flex items-center justify-center mr-2 bg-sky-500/10" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-project-diagram text-sky-600 fa-xs"></i>
|
|
</div>
|
|
<span>{{ task.project.name }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{% if task.assigned_user %}
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Currently Assigned To') }}</small>
|
|
<div class="flex items-center">
|
|
<div class="rounded-full flex items-center justify-center mr-2 bg-cyan-500/10" style="width: 24px; height:24px;">
|
|
<i class="fas fa-user text-cyan-600 fa-xs"></i>
|
|
</div>
|
|
<span>{{ task.assigned_user.display_name }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if task.due_date %}
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Current Due Date') }}</small>
|
|
<div class="flex items-center">
|
|
<div class="rounded-full flex items-center justify-center mr-2 {% if task.is_overdue %}bg-rose-500/10{% else %}bg-slate-500/10{% endif %}" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-calendar {% if task.is_overdue %}text-rose-600 dark:text-rose-400{% else %}text-slate-500 dark:text-slate-400{% endif %} fa-xs"></i>
|
|
</div>
|
|
<span class="{% if task.is_overdue %}text-rose-600 dark:text-rose-400 font-semibold{% endif %}">
|
|
{{ task.due_date.strftime('%B %d, %Y') }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if task.estimated_hours %}
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Current Estimate') }}</small>
|
|
<div class="flex items-center">
|
|
<div class="rounded-full flex items-center justify-center mr-2 bg-amber-500/10" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-clock text-amber-600 dark:text-amber-400 fa-xs"></i>
|
|
</div>
|
|
<span>{{ task.estimated_hours }} {{ _('hours') }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if task.total_hours > 0 %}
|
|
<div class="task-info-item mb-3">
|
|
<small class="text-text-muted-light dark:text-text-muted-dark block mb-1">{{ _('Actual Hours') }}</small>
|
|
<div class="flex items-center">
|
|
<div class="rounded-full flex items-center justify-center mr-2 bg-emerald-500/10" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-stopwatch text-emerald-600 fa-xs"></i>
|
|
</div>
|
|
<span>{{ task.total_hours }} {{ _('hours') }}</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Meta -->
|
|
<div class="task-info-item">
|
|
<div class="grid grid-cols-2 gap-2 text-xs text-text-muted-light dark:text-text-muted-dark">
|
|
<div><span class="block">{{ _('Created') }}</span><span class="text-text-light dark:text-text-dark">{{ task.created_at|user_datetime('%b %d, %Y %H:%M') if task.created_at else '-' }}</span></div>
|
|
<div><span class="block">{{ _('Updated') }}</span><span class="text-text-light dark:text-text-dark">{{ task.updated_at|user_datetime('%b %d, %Y %H:%M') if task.updated_at else '-' }}</span></div>
|
|
<div><span class="block">{{ _('Started') }}</span><span class="text-text-light dark:text-text-dark">{{ task.started_at|user_datetime('%b %d, %Y %H:%M') if task.started_at else '-' }}</span></div>
|
|
<div><span class="block">{{ _('Completed') }}</span><span class="text-text-light dark:text-text-dark">{{ task.completed_at|user_datetime('%b %d, %Y %H:%M') if task.completed_at else '-' }}</span></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow mb-4">
|
|
<div class="border-b border-border-light dark:border-border-dark p-4">
|
|
<h6 class="font-semibold flex items-center gap-2"><i class="fas fa-bolt text-yellow-600"></i>{{ _('Quick Actions') }}</h6>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="grid grid-cols-1 gap-2">
|
|
<a href="{{ url_for('tasks.view_task', task_id=task.id) }}" class="inline-flex items-center justify-center px-3 py-2 text-sm rounded-md border border-primary text-primary hover:bg-primary/10">
|
|
<i class="fas fa-eye mr-2"></i>{{ _('View Task') }}
|
|
</a>
|
|
|
|
{% if task.status == 'todo' or task.status == 'in_progress' %}
|
|
<a href="{{ url_for('timer.start_timer_for_project', project_id=task.project_id, task_id=task.id) }}" class="inline-flex items-center justify-center px-3 py-2 text-sm rounded-md bg-emerald-600 text-white hover:bg-emerald-700">
|
|
<i class="fas fa-play mr-2"></i>{{ _('Start Timer') }}
|
|
</a>
|
|
{% endif %}
|
|
|
|
<a href="{{ url_for('tasks.list_tasks') }}" class="inline-flex items-center justify-center px-3 py-2 text-sm rounded-md border border-border-light dark:border-border-dark hover:bg-background-light dark:hover:bg-background-dark">
|
|
<i class="fas fa-list mr-2"></i>{{ _('Back to Tasks') }}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Tips -->
|
|
<div class="bg-card-light dark:bg-card-dark rounded-lg shadow" data-testid="task-edit-tips">
|
|
<div class="border-b border-border-light dark:border-border-dark p-4">
|
|
<h6 class="font-semibold flex items-center gap-2"><i class="fas fa-lightbulb text-yellow-600"></i>{{ _('Edit Tips') }}</h6>
|
|
</div>
|
|
<div class="p-4">
|
|
<div class="tip-item mb-3">
|
|
<div class="d-flex align-items-start">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-info text-sky-600 dark:text-sky-400 fa-xs"></i>
|
|
</div>
|
|
<div>
|
|
<small class="fw-semibold d-block">{{ _('Status Changes') }}</small>
|
|
<small class="text-text-muted-light dark:text-text-muted-dark">{{ _('Changing status may affect time tracking and progress calculations') }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tip-item mb-3">
|
|
<div class="d-flex align-items-start">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-exclamation-triangle text-amber-600 dark:text-amber-400 fa-xs"></i>
|
|
</div>
|
|
<div>
|
|
<small class="fw-semibold d-block">{{ _('Due Date Updates') }}</small>
|
|
<small class="text-text-muted-light dark:text-text-muted-dark">{{ _('Consider team workload when adjusting deadlines') }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="tip-item">
|
|
<div class="d-flex align-items-start">
|
|
<div class="rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 24px; height: 24px;">
|
|
<i class="fas fa-check text-emerald-600 dark:text-emerald-400 fa-xs"></i>
|
|
</div>
|
|
<div>
|
|
<small class="fw-semibold d-block">{{ _('Assignment Changes') }}</small>
|
|
<small class="text-text-muted-light dark:text-text-muted-dark">{{ _('Notify team members when reassigning tasks') }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
/* Priority Badges */
|
|
.priority-badge {
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.priority-low {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.priority-medium {
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.priority-high {
|
|
background-color: #fed7aa;
|
|
color: #c2410c;
|
|
}
|
|
|
|
.priority-urgent {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* Status Badges */
|
|
.status-badge {
|
|
padding: 0.25rem 0.75rem;
|
|
border-radius: 20px;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.status-todo {
|
|
background-color: #e2e8f0;
|
|
color: #475569;
|
|
}
|
|
|
|
.status-in_progress {
|
|
background-color: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.status-review {
|
|
background-color: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.status-done {
|
|
background-color: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.status-cancelled {
|
|
background-color: #fee2e2;
|
|
color: #991b1b;
|
|
}
|
|
|
|
/* Task Info Items */
|
|
.task-info-item {
|
|
padding: 0.75rem;
|
|
border-radius: 8px;
|
|
background-color: #f8fafc;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.task-info-item:hover {
|
|
background-color: #f1f5f9;
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
/* Tip Items */
|
|
.tip-item {
|
|
padding: 0.75rem;
|
|
border-radius: 8px;
|
|
background-color: #f8fafc;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.tip-item:hover {
|
|
background-color: #f1f5f9;
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
/* Dark mode for task info blocks to match view page */
|
|
.dark .task-info-item,
|
|
.dark .tip-item {
|
|
background-color: #0f172a;
|
|
}
|
|
.dark .task-info-item:hover,
|
|
.dark .tip-item:hover {
|
|
background-color: #0b1220;
|
|
}
|
|
|
|
/* Form Styling */
|
|
.form-control:focus,
|
|
.form-select:focus {
|
|
border-color: var(--primary-color);
|
|
box-shadow: 0 0 0 0.2rem rgba(59, 130, 246, 0.25);
|
|
}
|
|
|
|
.form-control-lg {
|
|
min-height: 56px;
|
|
}
|
|
|
|
.form-select-lg {
|
|
min-height: 56px;
|
|
}
|
|
|
|
/* Enhanced markdown editor styles are now centralized in base.css */
|
|
|
|
/* Mobile Optimizations */
|
|
@media (max-width: 768px) {
|
|
.card-header {
|
|
padding: 1rem 1rem 0.75rem 1rem;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 0.75rem 1rem;
|
|
}
|
|
|
|
.task-info-item:hover,
|
|
.tip-item:hover {
|
|
transform: none;
|
|
}
|
|
}
|
|
|
|
/* Hover Effects */
|
|
.btn:hover {
|
|
transform: none;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
|
}
|
|
|
|
.task-info-item:hover,
|
|
.tip-item:hover {
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
</style>
|
|
|
|
<!-- Toast UI Editor -->
|
|
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
|
|
|
|
<script>
|
|
// Form validation and enhancement
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const form = document.getElementById('editTaskForm');
|
|
const nameInput = document.getElementById('name');
|
|
const descriptionInput = document.getElementById('description');
|
|
const prioritySelect = document.getElementById('priority');
|
|
const statusSelect = document.getElementById('status');
|
|
const priorityPreview = document.getElementById('priorityPreview');
|
|
const statusPreview = document.getElementById('statusPreview');
|
|
let mdEditor = null;
|
|
|
|
// Initialize Toast UI Editor
|
|
if (descriptionInput && window.toastui && window.toastui.Editor) {
|
|
const theme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
|
mdEditor = new toastui.Editor({
|
|
el: document.getElementById('description_editor'),
|
|
height: '380px',
|
|
initialEditType: 'wysiwyg',
|
|
previewStyle: 'vertical',
|
|
usageStatistics: false,
|
|
hideModeSwitch: false,
|
|
placeholder: '{{ _('Provide detailed information about the task, requirements, and any specific instructions...') }}',
|
|
theme: theme,
|
|
toolbarItems: [
|
|
['heading', 'bold', 'italic', 'strike'],
|
|
['hr', 'quote'],
|
|
['ul', 'ol', 'task'],
|
|
['link', 'code', 'codeblock', 'table'],
|
|
['image'],
|
|
['scrollSync']
|
|
],
|
|
initialValue: descriptionInput.value || ''
|
|
});
|
|
|
|
// Apply theme changes dynamically if supported
|
|
const observer = new MutationObserver(function(mutations) {
|
|
mutations.forEach(function(mutation) {
|
|
if (mutation.type === 'attributes' && mutation.attributeName === 'class' && mdEditor) {
|
|
const nextTheme = document.documentElement.classList.contains('dark') ? 'dark' : 'light';
|
|
try {
|
|
if (typeof mdEditor.setTheme === 'function') {
|
|
mdEditor.setTheme(nextTheme);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
});
|
|
});
|
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
|
|
// Image upload hook
|
|
mdEditor.removeHook && mdEditor.removeHook('addImageBlobHook');
|
|
mdEditor.addHook && mdEditor.addHook('addImageBlobHook', async (blob, callback) => {
|
|
try {
|
|
const formData = new FormData();
|
|
formData.append('image', blob, blob.name || 'upload.png');
|
|
const res = await fetch('{{ url_for('api.upload_editor_image') }}', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
const data = await res.json();
|
|
if (data && data.url) {
|
|
callback(data.url, blob.name || 'image');
|
|
} else {
|
|
console.warn('Upload failed', data);
|
|
}
|
|
} catch (e) { console.error('Image upload error', e); }
|
|
});
|
|
|
|
// Autosave to localStorage
|
|
const autosaveKey = 'tt-task-edit-description-{{ task.id }}';
|
|
mdEditor.on && mdEditor.on('change', () => {
|
|
try { localStorage.setItem(autosaveKey, mdEditor.getMarkdown()); } catch (e) {}
|
|
});
|
|
// Restore if textarea is empty (rare on edit), otherwise keep DB value
|
|
if (!descriptionInput.value) {
|
|
const cached = localStorage.getItem(autosaveKey);
|
|
if (cached) { try { mdEditor.setMarkdown(cached); } catch(e) {} }
|
|
}
|
|
}
|
|
|
|
// Form submission enhancement
|
|
form.addEventListener('submit', function(e) {
|
|
const name = nameInput.value.trim();
|
|
if (!name) {
|
|
e.preventDefault();
|
|
nameInput.focus();
|
|
nameInput.classList.add('is-invalid');
|
|
return false;
|
|
}
|
|
// Sync markdown content back to hidden textarea
|
|
if (mdEditor && descriptionInput) {
|
|
try { descriptionInput.value = mdEditor.getMarkdown(); } catch (err) {}
|
|
}
|
|
|
|
// Show loading state
|
|
const submitBtn = form.querySelector('button[type="submit"]');
|
|
const originalText = submitBtn.innerHTML;
|
|
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>{{ _('Updating...') }}';
|
|
submitBtn.disabled = true;
|
|
|
|
// Re-enable after a delay (in case of validation errors)
|
|
setTimeout(() => {
|
|
submitBtn.innerHTML = originalText;
|
|
submitBtn.disabled = false;
|
|
}, 5000);
|
|
});
|
|
|
|
// Real-time validation feedback
|
|
nameInput.addEventListener('input', function() {
|
|
if (this.value.trim()) {
|
|
this.classList.remove('is-invalid');
|
|
this.classList.add('is-valid');
|
|
} else {
|
|
this.classList.remove('is-valid');
|
|
}
|
|
});
|
|
|
|
// Highlight current values
|
|
const currentStatus = '{{ task.status }}';
|
|
const currentPriority = '{{ task.priority }}';
|
|
|
|
// Add visual indicators for current values
|
|
if (statusSelect) {
|
|
statusSelect.addEventListener('change', function() {
|
|
this.classList.remove('border-success', 'border-warning');
|
|
if (this.value === currentStatus) {
|
|
this.classList.add('border-success');
|
|
} else {
|
|
this.classList.add('border-warning');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (prioritySelect) {
|
|
prioritySelect.addEventListener('change', function() {
|
|
this.classList.remove('border-success', 'border-warning');
|
|
if (this.value === currentPriority) {
|
|
this.classList.add('border-success');
|
|
} else {
|
|
this.classList.add('border-warning');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Live badge previews
|
|
function updateBadge(element, value, type) {
|
|
if (!element) return;
|
|
const map = type === 'priority' ? {
|
|
low: { text: '{{ _('Low') }}', class: 'priority-low' },
|
|
medium: { text: '{{ _('Medium') }}', class: 'priority-medium' },
|
|
high: { text: '{{ _('High') }}', class: 'priority-high' },
|
|
urgent: { text: '{{ _('Urgent') }}', class: 'priority-urgent' },
|
|
} : {
|
|
todo: { text: '{{ _('To Do') }}', class: 'status-todo' },
|
|
in_progress: { text: '{{ _('In Progress') }}', class: 'status-in_progress' },
|
|
review: { text: '{{ _('Review') }}', class: 'status-review' },
|
|
done: { text: '{{ _('Done') }}', class: 'status-done' },
|
|
cancelled: { text: '{{ _('Cancelled') }}', class: 'status-cancelled' },
|
|
};
|
|
const def = map[value] || Object.values(map)[0];
|
|
element.className = element.className.split(' ').filter(c => !c.startsWith(type === 'priority' ? 'priority-' : 'status-')).join(' ').trim();
|
|
element.classList.add(def.class);
|
|
element.textContent = def.text;
|
|
}
|
|
function handlePreviewChange(){
|
|
updateBadge(priorityPreview, prioritySelect?.value || currentPriority, 'priority');
|
|
updateBadge(statusPreview, statusSelect?.value || currentStatus, 'status');
|
|
}
|
|
prioritySelect && prioritySelect.addEventListener('change', handlePreviewChange);
|
|
statusSelect && statusSelect.addEventListener('change', handlePreviewChange);
|
|
handlePreviewChange();
|
|
});
|
|
</script>
|
|
{% endblock %}
|