trackops 2.0.3 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -0
- package/lib/init.js +2 -2
- package/lib/locale.js +41 -17
- package/lib/opera-bootstrap.js +68 -7
- package/lib/opera.js +10 -2
- package/lib/registry.js +18 -0
- package/lib/server.js +312 -207
- package/locales/en.json +4 -0
- package/locales/es.json +4 -0
- package/package.json +1 -1
- package/skills/trackops/locales/en/references/activation.md +15 -0
- package/skills/trackops/locales/en/references/troubleshooting.md +12 -0
- package/skills/trackops/references/activation.md +15 -0
- package/skills/trackops/references/troubleshooting.md +12 -0
- package/skills/trackops/skill.json +2 -2
- package/ui/css/base.css +19 -1
- package/ui/css/charts.css +106 -8
- package/ui/css/components.css +554 -17
- package/ui/css/onboarding.css +133 -0
- package/ui/css/panels.css +345 -406
- package/ui/css/terminal.css +125 -0
- package/ui/css/timeline.css +58 -0
- package/ui/css/tokens.css +170 -113
- package/ui/index.html +3 -0
- package/ui/js/api.js +49 -13
- package/ui/js/app.js +28 -32
- package/ui/js/charts.js +526 -0
- package/ui/js/filters.js +247 -0
- package/ui/js/icons.js +82 -57
- package/ui/js/keyboard.js +229 -0
- package/ui/js/onboarding.js +33 -42
- package/ui/js/router.js +20 -3
- package/ui/js/views/board.js +84 -114
- package/ui/js/views/dashboard.js +870 -0
- package/ui/js/views/projects.js +745 -0
- package/ui/js/views/scrum.js +476 -0
- package/ui/js/views/settings.js +197 -247
- package/ui/js/views/sidebar.js +37 -31
- package/ui/js/views/tasks.js +218 -101
- package/ui/js/views/timeline.js +265 -0
- package/ui/js/views/topbar.js +94 -107
- package/ui/app.js +0 -950
- package/ui/js/views/insights.js +0 -340
- package/ui/js/views/overview.js +0 -369
- package/ui/styles.css +0 -688
package/ui/js/views/tasks.js
CHANGED
|
@@ -1,116 +1,236 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tasks.js —
|
|
2
|
+
* tasks.js — Unified Task Management View (Kanban | Scrum | List)
|
|
3
|
+
*
|
|
4
|
+
* Three agile modes toggled via tab pills, with shared filter bar.
|
|
5
|
+
* List mode preserves the original split-view task editor.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
|
-
import { icon } from '../icons.js';
|
|
6
8
|
import * as state from '../state.js';
|
|
7
9
|
import * as api from '../api.js';
|
|
10
|
+
import * as router from '../router.js';
|
|
11
|
+
import { icon } from '../icons.js';
|
|
12
|
+
import { t } from '../i18n.js';
|
|
13
|
+
import { esc, formatDate, formatDuration, splitLines, extractHistory } from '../utils.js';
|
|
14
|
+
import * as filters from '../filters.js';
|
|
15
|
+
import { renderKanban, bindKanbanEvents } from './board.js';
|
|
16
|
+
import { renderScrum, bindScrumEvents } from './scrum.js';
|
|
8
17
|
import { flash } from './flash.js';
|
|
9
|
-
import { esc, splitLines, formatDate } from '../utils.js';
|
|
10
18
|
import * as timeTracker from '../time-tracker.js';
|
|
11
|
-
|
|
19
|
+
|
|
20
|
+
/* ─────────────────────────── Mode state ─────────────────────────── */
|
|
21
|
+
|
|
22
|
+
let _activeMode = localStorage.getItem('ops-tasks-mode') || 'kanban';
|
|
23
|
+
|
|
24
|
+
/* ─────────────────────────── Public API ─────────────────────────── */
|
|
12
25
|
|
|
13
26
|
export async function render() {
|
|
14
27
|
const payload = state.getPayload();
|
|
15
28
|
if (!payload) return `<div class="empty-state" style="margin:3rem">${t('ui.tasks.noData', {}, 'No project data.')}</div>`;
|
|
16
29
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
const
|
|
30
|
+
const projectName = payload.control?.project?.name || payload.control?.name || '';
|
|
31
|
+
const activeFilters = filters.load('tasks');
|
|
32
|
+
const allTasks = payload.derived?.tasks || [];
|
|
33
|
+
const filteredTasks = filters.apply(allTasks, activeFilters);
|
|
34
|
+
const phases = state.getPhases();
|
|
35
|
+
const streams = [...new Set(allTasks.map(t => t.stream).filter(Boolean))];
|
|
21
36
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
const modeContent = _renderModeContent(filteredTasks, phases, payload);
|
|
38
|
+
|
|
39
|
+
return `
|
|
40
|
+
<section class="view-section view-enter">
|
|
41
|
+
<div class="section-header" style="display:flex;justify-content:space-between;align-items:center;flex-wrap:wrap;gap:var(--space-3)">
|
|
42
|
+
<div>
|
|
43
|
+
${projectName ? `<p class="section-eyebrow">${esc(projectName)}</p>` : ''}
|
|
44
|
+
<h2 class="section-title">${t('ui.tasks.title', {}, 'Tasks')}</h2>
|
|
45
|
+
</div>
|
|
46
|
+
<div style="display:flex;align-items:center;gap:var(--space-3)">
|
|
47
|
+
<div class="tab-pills">
|
|
48
|
+
<button class="tab-pill ${_activeMode === 'kanban' ? 'is-active' : ''}" data-mode="kanban">Kanban</button>
|
|
49
|
+
<button class="tab-pill ${_activeMode === 'scrum' ? 'is-active' : ''}" data-mode="scrum">Scrum</button>
|
|
50
|
+
<button class="tab-pill ${_activeMode === 'list' ? 'is-active' : ''}" data-mode="list">List</button>
|
|
51
|
+
</div>
|
|
52
|
+
<button class="btn btn-primary btn-sm" id="new-task-btn" type="button">
|
|
53
|
+
${icon('plus', 14)} ${t('ui.tasks.new', {}, 'New task')}
|
|
54
|
+
</button>
|
|
28
55
|
</div>
|
|
29
|
-
<button class="btn btn-primary btn-sm" id="new-task-btn-top" type="button">
|
|
30
|
-
${icon('plus', 14)} ${t('ui.tasks.new', {}, 'New task')}
|
|
31
|
-
</button>
|
|
32
56
|
</div>
|
|
33
57
|
|
|
34
|
-
<div
|
|
58
|
+
<div id="tasks-filter-bar">
|
|
59
|
+
${filters.renderBar('tasks', activeFilters, { phases, streams })}
|
|
60
|
+
</div>
|
|
35
61
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
<div id="tasks-mode-content">
|
|
63
|
+
${modeContent}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
66
|
+
`;
|
|
67
|
+
}
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
69
|
+
export function bindEvents() {
|
|
70
|
+
// Filter bar
|
|
71
|
+
filters.bindBar('tasks', () => _refreshModeContent());
|
|
72
|
+
|
|
73
|
+
// Mode toggle pills
|
|
74
|
+
document.querySelectorAll('[data-mode]').forEach(btn => {
|
|
75
|
+
btn.addEventListener('click', () => {
|
|
76
|
+
const mode = btn.dataset.mode;
|
|
77
|
+
if (mode === _activeMode) return;
|
|
78
|
+
_activeMode = mode;
|
|
79
|
+
localStorage.setItem('ops-tasks-mode', mode);
|
|
80
|
+
|
|
81
|
+
// Toggle active class on pills
|
|
82
|
+
document.querySelectorAll('.tab-pill[data-mode]').forEach(p => {
|
|
83
|
+
p.classList.toggle('is-active', p.dataset.mode === mode);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
_refreshModeContent();
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// New Task button — switch to list mode and open blank editor
|
|
91
|
+
document.getElementById('new-task-btn')?.addEventListener('click', () => {
|
|
92
|
+
_activeMode = 'list';
|
|
93
|
+
localStorage.setItem('ops-tasks-mode', 'list');
|
|
94
|
+
|
|
95
|
+
document.querySelectorAll('.tab-pill[data-mode]').forEach(p => {
|
|
96
|
+
p.classList.toggle('is-active', p.dataset.mode === 'list');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
state.update('selectedTaskId', null);
|
|
100
|
+
_refreshModeContent();
|
|
101
|
+
|
|
102
|
+
// Focus the title field once rendered
|
|
103
|
+
setTimeout(() => document.getElementById('task-title')?.focus(), 0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Bind active mode events
|
|
107
|
+
_bindActiveModeEvents();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* ────────────────────── Mode content rendering ──────────────────── */
|
|
111
|
+
|
|
112
|
+
function _renderModeContent(filteredTasks, phases, payload) {
|
|
113
|
+
switch (_activeMode) {
|
|
114
|
+
case 'kanban':
|
|
115
|
+
return renderKanban(filteredTasks, phases, { showCompleted: false });
|
|
116
|
+
case 'scrum': {
|
|
117
|
+
const milestones = payload?.control?.milestones || [];
|
|
118
|
+
return renderScrum(filteredTasks, milestones, phases, {});
|
|
119
|
+
}
|
|
120
|
+
case 'list':
|
|
121
|
+
return _renderListMode(filteredTasks, phases);
|
|
122
|
+
default:
|
|
123
|
+
return renderKanban(filteredTasks, phases, { showCompleted: false });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function _refreshModeContent() {
|
|
128
|
+
const payload = state.getPayload();
|
|
129
|
+
if (!payload) return;
|
|
130
|
+
|
|
131
|
+
const activeFilters = filters.load('tasks');
|
|
132
|
+
const allTasks = payload.derived?.tasks || [];
|
|
133
|
+
const filteredTasks = filters.apply(allTasks, activeFilters);
|
|
134
|
+
const phases = state.getPhases();
|
|
135
|
+
|
|
136
|
+
const container = document.getElementById('tasks-mode-content');
|
|
137
|
+
if (container) {
|
|
138
|
+
container.innerHTML = _renderModeContent(filteredTasks, phases, payload);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
_bindActiveModeEvents();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function _bindActiveModeEvents() {
|
|
145
|
+
switch (_activeMode) {
|
|
146
|
+
case 'kanban':
|
|
147
|
+
bindKanbanEvents();
|
|
148
|
+
break;
|
|
149
|
+
case 'scrum':
|
|
150
|
+
bindScrumEvents();
|
|
151
|
+
break;
|
|
152
|
+
case 'list':
|
|
153
|
+
_bindListEvents();
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
159
|
+
LIST MODE — Split-view task list + editor
|
|
160
|
+
═══════════════════════════════════════════════════════════════════ */
|
|
161
|
+
|
|
162
|
+
function _renderListMode(tasks, phases) {
|
|
163
|
+
const selTask = state.findTask(state.get('selectedTaskId'));
|
|
164
|
+
const statusLabels = state.getStatusLabels();
|
|
165
|
+
const payload = state.getPayload();
|
|
166
|
+
const totals = payload?.derived?.totals || {};
|
|
167
|
+
|
|
168
|
+
return `
|
|
169
|
+
<div class="grid-split">
|
|
170
|
+
|
|
171
|
+
<!-- Task list panel -->
|
|
172
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
173
|
+
<!-- Quick status filter chips -->
|
|
174
|
+
<div style="display:flex;gap:var(--space-2);flex-wrap:wrap" role="group" aria-label="${t('ui.tasks.filters', {}, 'Status filters')}">
|
|
175
|
+
${_renderStatusFilters(statusLabels, totals)}
|
|
46
176
|
</div>
|
|
47
177
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
${_renderEditor(selTask, phases)}
|
|
178
|
+
<div class="stack stack-sm" id="task-list" aria-label="${t('ui.tasks.list', {}, 'Task list')}" role="list">
|
|
179
|
+
${_renderTaskList(tasks, statusLabels, phases)}
|
|
51
180
|
</div>
|
|
181
|
+
</div>
|
|
52
182
|
|
|
183
|
+
<!-- Task editor panel -->
|
|
184
|
+
<div class="panel" id="task-editor" aria-label="${t('ui.tasks.editor', {}, 'Task editor')}" aria-live="polite">
|
|
185
|
+
${_renderEditor(selTask, phases)}
|
|
53
186
|
</div>
|
|
187
|
+
|
|
54
188
|
</div>
|
|
55
189
|
`;
|
|
56
|
-
|
|
57
|
-
setTimeout(() => _bindEvents(), 0);
|
|
58
|
-
return html;
|
|
59
190
|
}
|
|
60
191
|
|
|
61
|
-
|
|
62
|
-
const query = state.get('searchQuery')?.toLowerCase();
|
|
63
|
-
const filter = sessionStorage.getItem('tasks-filter') || '';
|
|
64
|
-
let list = [...tasks];
|
|
65
|
-
if (query) {
|
|
66
|
-
list = list.filter(t =>
|
|
67
|
-
t.title.toLowerCase().includes(query) ||
|
|
68
|
-
(t.summary || '').toLowerCase().includes(query) ||
|
|
69
|
-
t.id.toLowerCase().includes(query)
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
if (filter) list = list.filter(t => t.status === filter);
|
|
73
|
-
return list;
|
|
74
|
-
}
|
|
192
|
+
/* ─── Status filter chips ─── */
|
|
75
193
|
|
|
76
194
|
function _renderStatusFilters(statusLabels, totals) {
|
|
77
|
-
const
|
|
78
|
-
{ id: '', label: t('ui.tasks.all', {}, 'All'),
|
|
79
|
-
{ id: 'pending', label: statusLabels.pending
|
|
80
|
-
{ id: 'in_progress', label: statusLabels.in_progress || t('status.in_progress', {}, 'In progress'),
|
|
81
|
-
{ id: 'in_review', label: statusLabels.in_review
|
|
82
|
-
{ id: 'blocked', label: statusLabels.blocked
|
|
83
|
-
{ id: 'completed', label: statusLabels.completed
|
|
195
|
+
const chipFilters = [
|
|
196
|
+
{ id: '', label: t('ui.tasks.all', {}, 'All'), count: totals.all },
|
|
197
|
+
{ id: 'pending', label: statusLabels.pending || t('status.pending', {}, 'Pending'), count: totals.pending },
|
|
198
|
+
{ id: 'in_progress', label: statusLabels.in_progress || t('status.in_progress', {}, 'In progress'), count: totals.inProgress },
|
|
199
|
+
{ id: 'in_review', label: statusLabels.in_review || t('status.in_review', {}, 'In review'), count: totals.inReview },
|
|
200
|
+
{ id: 'blocked', label: statusLabels.blocked || t('status.blocked', {}, 'Blocked'), count: totals.blocked },
|
|
201
|
+
{ id: 'completed', label: statusLabels.completed || t('status.completed', {}, 'Completed'), count: totals.completed },
|
|
84
202
|
];
|
|
85
203
|
const active = sessionStorage.getItem('tasks-filter') || '';
|
|
86
|
-
return
|
|
204
|
+
return chipFilters.map(f => `
|
|
87
205
|
<button class="chip ${f.id === active ? 'is-active' : ''}"
|
|
88
206
|
type="button" data-task-filter="${esc(f.id)}"
|
|
89
207
|
aria-pressed="${f.id === active}">
|
|
90
|
-
${esc(f.label)} <span class="badge badge-muted" style="font-size:0.65rem">${f.count}</span>
|
|
208
|
+
${esc(f.label)} <span class="badge badge-muted" style="font-size:0.65rem">${f.count ?? 0}</span>
|
|
91
209
|
</button>
|
|
92
210
|
`).join('');
|
|
93
211
|
}
|
|
94
212
|
|
|
213
|
+
/* ─── Task list cards ─── */
|
|
214
|
+
|
|
95
215
|
function _renderTaskList(tasks, statusLabels, phases) {
|
|
96
216
|
if (!tasks.length) return `<div class="empty-state">${t('ui.tasks.noMatch', {}, 'No matching tasks.')}</div>`;
|
|
97
217
|
const selectedId = state.get('selectedTaskId');
|
|
98
218
|
const priorityVariant = { P0: 'danger', P1: 'warning', P2: 'accent', P3: 'muted' };
|
|
99
219
|
|
|
100
|
-
return tasks.map(
|
|
101
|
-
const phase = phases.find(p => p.id ===
|
|
220
|
+
return tasks.map(tk => {
|
|
221
|
+
const phase = phases.find(p => p.id === tk.phase);
|
|
102
222
|
return `
|
|
103
|
-
<div class="task-card ${
|
|
104
|
-
data-task-id="${esc(
|
|
223
|
+
<div class="task-card ${tk.id === selectedId ? 'is-selected' : ''}"
|
|
224
|
+
data-task-id="${esc(tk.id)}"
|
|
105
225
|
role="listitem" tabindex="0"
|
|
106
|
-
aria-selected="${
|
|
107
|
-
aria-label="${esc(
|
|
226
|
+
aria-selected="${tk.id === selectedId}"
|
|
227
|
+
aria-label="${esc(tk.title)}"
|
|
108
228
|
>
|
|
109
|
-
<strong class="task-card-title">${esc(
|
|
110
|
-
<span class="task-card-id">${esc(
|
|
229
|
+
<strong class="task-card-title">${esc(tk.title)}</strong>
|
|
230
|
+
<span class="task-card-id">${esc(tk.id)}</span>
|
|
111
231
|
<div class="task-card-meta" style="margin-top:var(--space-2)">
|
|
112
|
-
<span class="badge badge-${priorityVariant[
|
|
113
|
-
<span class="badge status-${
|
|
232
|
+
<span class="badge badge-${priorityVariant[tk.priority] || 'muted'}">${esc(tk.priority)}</span>
|
|
233
|
+
<span class="badge status-${tk.status}">${esc(statusLabels[tk.status] || tk.status)}</span>
|
|
114
234
|
${phase ? `<span class="badge badge-muted">${esc(phase.label)}</span>` : ''}
|
|
115
235
|
</div>
|
|
116
236
|
</div>
|
|
@@ -118,19 +238,21 @@ function _renderTaskList(tasks, statusLabels, phases) {
|
|
|
118
238
|
}).join('');
|
|
119
239
|
}
|
|
120
240
|
|
|
241
|
+
/* ─── Task editor ─── */
|
|
242
|
+
|
|
121
243
|
function _renderEditor(task, phases) {
|
|
122
244
|
const isNew = !task;
|
|
123
|
-
const
|
|
245
|
+
const phasesOpts = phases.map(p =>
|
|
124
246
|
`<option value="${esc(p.id)}" ${!isNew && task.phase === p.id ? 'selected' : ''}>${esc(p.id)} — ${esc(p.label)}</option>`
|
|
125
247
|
).join('');
|
|
126
248
|
|
|
127
249
|
const statuses = [
|
|
128
|
-
{ id: 'pending', label: t('status.pending',
|
|
250
|
+
{ id: 'pending', label: t('status.pending', {}, 'Pending') },
|
|
129
251
|
{ id: 'in_progress', label: t('status.in_progress', {}, 'In progress') },
|
|
130
|
-
{ id: 'in_review', label: t('status.in_review',
|
|
131
|
-
{ id: 'blocked', label: t('status.blocked',
|
|
132
|
-
{ id: 'completed', label: t('status.completed',
|
|
133
|
-
{ id: 'cancelled', label: t('status.cancelled',
|
|
252
|
+
{ id: 'in_review', label: t('status.in_review', {}, 'In review') },
|
|
253
|
+
{ id: 'blocked', label: t('status.blocked', {}, 'Blocked') },
|
|
254
|
+
{ id: 'completed', label: t('status.completed', {}, 'Completed') },
|
|
255
|
+
{ id: 'cancelled', label: t('status.cancelled', {}, 'Cancelled') },
|
|
134
256
|
];
|
|
135
257
|
|
|
136
258
|
return `
|
|
@@ -154,7 +276,7 @@ function _renderEditor(task, phases) {
|
|
|
154
276
|
</div>
|
|
155
277
|
</div>
|
|
156
278
|
|
|
157
|
-
<!--
|
|
279
|
+
<!-- Quick action strip -->
|
|
158
280
|
${!isNew ? `
|
|
159
281
|
<div class="panel-footer" style="display:flex;gap:var(--space-2);flex-wrap:wrap" role="group" aria-label="${t('ui.tasks.quickActions', {}, 'Quick task actions')}">
|
|
160
282
|
<button class="chip is-active" type="button" data-task-action="start" aria-label="${t('ui.tasks.start', {}, 'Start task')}">${t('ui.tasks.startLabel', {}, 'Start')}</button>
|
|
@@ -179,7 +301,7 @@ function _renderEditor(task, phases) {
|
|
|
179
301
|
<div class="field-row">
|
|
180
302
|
<div class="field">
|
|
181
303
|
<label for="task-phase">${t('ui.tasks.field.phase', {}, 'Phase')}</label>
|
|
182
|
-
<select id="task-phase" name="phase">${
|
|
304
|
+
<select id="task-phase" name="phase">${phasesOpts}</select>
|
|
183
305
|
</div>
|
|
184
306
|
<div class="field">
|
|
185
307
|
<label for="task-priority">${t('ui.tasks.field.priority', {}, 'Priority')}</label>
|
|
@@ -264,8 +386,10 @@ function _renderEditor(task, phases) {
|
|
|
264
386
|
`;
|
|
265
387
|
}
|
|
266
388
|
|
|
267
|
-
|
|
268
|
-
|
|
389
|
+
/* ─── List mode event binding ─── */
|
|
390
|
+
|
|
391
|
+
function _bindListEvents() {
|
|
392
|
+
// Select task from list
|
|
269
393
|
document.getElementById('task-list')?.addEventListener('click', e => {
|
|
270
394
|
const card = e.target.closest('[data-task-id]');
|
|
271
395
|
if (!card) return;
|
|
@@ -273,36 +397,26 @@ function _bindEvents() {
|
|
|
273
397
|
state.update('selectedTaskId', id);
|
|
274
398
|
const editor = document.getElementById('task-editor');
|
|
275
399
|
if (editor) {
|
|
276
|
-
const phases
|
|
277
|
-
const selTask
|
|
400
|
+
const phases = state.getPhases();
|
|
401
|
+
const selTask = state.findTask(id);
|
|
278
402
|
editor.innerHTML = _renderEditor(selTask, phases);
|
|
279
403
|
_bindEditorForm();
|
|
280
404
|
}
|
|
281
|
-
//
|
|
282
|
-
document.querySelectorAll('[data-task-id]').forEach(c => {
|
|
405
|
+
// Update selection highlight in list
|
|
406
|
+
document.querySelectorAll('#task-list [data-task-id]').forEach(c => {
|
|
283
407
|
c.classList.toggle('is-selected', c.dataset.taskId === id);
|
|
284
408
|
c.setAttribute('aria-selected', c.dataset.taskId === id);
|
|
285
409
|
});
|
|
286
410
|
});
|
|
287
411
|
|
|
288
|
-
//
|
|
412
|
+
// Status filter chips
|
|
289
413
|
document.querySelectorAll('[data-task-filter]').forEach(btn => {
|
|
290
414
|
btn.addEventListener('click', () => {
|
|
291
415
|
sessionStorage.setItem('tasks-filter', btn.dataset.taskFilter);
|
|
292
|
-
|
|
416
|
+
_refreshModeContent();
|
|
293
417
|
});
|
|
294
418
|
});
|
|
295
419
|
|
|
296
|
-
// Nueva tarea
|
|
297
|
-
document.getElementById('new-task-btn-top')?.addEventListener('click', () => {
|
|
298
|
-
state.update('selectedTaskId', null);
|
|
299
|
-
const editor = document.getElementById('task-editor');
|
|
300
|
-
if (editor) {
|
|
301
|
-
editor.innerHTML = _renderEditor(null, state.getPhases());
|
|
302
|
-
_bindEditorForm();
|
|
303
|
-
}
|
|
304
|
-
});
|
|
305
|
-
|
|
306
420
|
_bindEditorForm();
|
|
307
421
|
}
|
|
308
422
|
|
|
@@ -322,7 +436,10 @@ function _bindEditorForm() {
|
|
|
322
436
|
state.update('selectedTaskId', null);
|
|
323
437
|
const editor = document.getElementById('task-editor');
|
|
324
438
|
if (editor) {
|
|
325
|
-
editor.innerHTML = _renderEditor(
|
|
439
|
+
editor.innerHTML = _renderEditor(
|
|
440
|
+
{ ...task, title: `${task.title} (${t('ui.tasks.copySuffix', {}, 'copy')})`, status: 'pending', history: [] },
|
|
441
|
+
state.getPhases()
|
|
442
|
+
);
|
|
326
443
|
_bindEditorForm();
|
|
327
444
|
document.getElementById('task-title')?.focus();
|
|
328
445
|
}
|
|
@@ -330,18 +447,18 @@ function _bindEditorForm() {
|
|
|
330
447
|
|
|
331
448
|
// Timer quick start
|
|
332
449
|
document.getElementById('timer-quick-btn')?.addEventListener('click', async () => {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
450
|
+
const task = state.findTask(state.get('selectedTaskId'));
|
|
451
|
+
if (!task) return;
|
|
452
|
+
await timeTracker.start(task.id, task.title);
|
|
336
453
|
});
|
|
337
454
|
|
|
338
|
-
//
|
|
455
|
+
// Quick action strip
|
|
339
456
|
document.querySelectorAll('[data-task-action]').forEach(btn => {
|
|
340
457
|
btn.addEventListener('click', async () => {
|
|
341
458
|
const taskId = state.get('selectedTaskId');
|
|
342
459
|
if (!taskId) { flash(t('ui.tasks.selectFirst', {}, 'Select a task first.'), 'warning'); return; }
|
|
343
460
|
const action = btn.dataset.taskAction;
|
|
344
|
-
const note
|
|
461
|
+
const note = document.getElementById('task-note')?.value?.trim() || '';
|
|
345
462
|
try {
|
|
346
463
|
await api.taskAction(taskId, action, note || t('ui.tasks.defaultActionNote', { action }, `Change to "${action}" from the board.`));
|
|
347
464
|
flash(t('ui.tasks.updated', {}, 'Status updated.'), 'success');
|
|
@@ -384,7 +501,7 @@ async function _submitForm() {
|
|
|
384
501
|
};
|
|
385
502
|
|
|
386
503
|
if (!payload.title) {
|
|
387
|
-
flash('
|
|
504
|
+
flash(t('ui.tasks.titleRequired', {}, 'Title is required.'), 'error');
|
|
388
505
|
get('task-title')?.focus();
|
|
389
506
|
return;
|
|
390
507
|
}
|
|
@@ -393,11 +510,11 @@ async function _submitForm() {
|
|
|
393
510
|
try {
|
|
394
511
|
if (selectedId) {
|
|
395
512
|
await api.updateTask(selectedId, payload);
|
|
396
|
-
flash('
|
|
513
|
+
flash(t('ui.tasks.taskUpdated', {}, 'Task updated.'), 'success');
|
|
397
514
|
} else {
|
|
398
515
|
const result = await api.createTask(payload);
|
|
399
516
|
state.update('selectedTaskId', result.task?.id);
|
|
400
|
-
flash('
|
|
517
|
+
flash(t('ui.tasks.taskCreated', {}, 'Task created.'), 'success');
|
|
401
518
|
}
|
|
402
519
|
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
403
520
|
} catch (err) {
|