taskunity 2026.1__py3-none-any.whl
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.
- taskunity/__init__.py +1 -0
- taskunity/app.py +1268 -0
- taskunity/cli.py +43 -0
- taskunity/models.py +103 -0
- taskunity/render.py +371 -0
- taskunity/static/app.css +1394 -0
- taskunity/static/chart.umd.min.js +14 -0
- taskunity/static/chartjs-adapter-date-fns.bundle.min.js +7 -0
- taskunity/static/htmx.min.js +68 -0
- taskunity/task_store.py +598 -0
- taskunity/templates/base.html +397 -0
- taskunity/templates/index.html +4 -0
- taskunity/templates/partials/board.html +24 -0
- taskunity/templates/partials/calendar.html +25 -0
- taskunity/templates/partials/main.html +283 -0
- taskunity/templates/partials/milestone_banner.html +34 -0
- taskunity/templates/partials/milestone_panel.html +222 -0
- taskunity/templates/partials/milestones.html +55 -0
- taskunity/templates/partials/projects.html +41 -0
- taskunity/templates/partials/task_list.html +52 -0
- taskunity/templates/partials/task_panel.html +310 -0
- taskunity/templates/partials/timeline.html +24 -0
- taskunity-2026.1.dist-info/METADATA +238 -0
- taskunity-2026.1.dist-info/RECORD +27 -0
- taskunity-2026.1.dist-info/WHEEL +5 -0
- taskunity-2026.1.dist-info/entry_points.txt +2 -0
- taskunity-2026.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<section class="panel">
|
|
2
|
+
<div class="panel-heading">
|
|
3
|
+
<h2>Projects</h2>
|
|
4
|
+
<span>{{ projects|length }} project{{ '' if projects|length == 1 else 's' }}</span>
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
{% if projects %}
|
|
8
|
+
<div class="project-cards">
|
|
9
|
+
{% for p in projects %}
|
|
10
|
+
<div class="project-card" style="border-left-color: {{ p.color }}">
|
|
11
|
+
<div class="project-card-head">
|
|
12
|
+
<span class="swatch" style="background: {{ p.color }}"></span>
|
|
13
|
+
<strong>{{ p.name }}</strong>
|
|
14
|
+
</div>
|
|
15
|
+
{% if p.description %}<p>{{ p.description }}</p>{% else %}<p class="muted">No description</p>{% endif %}
|
|
16
|
+
<details class="project-edit">
|
|
17
|
+
<summary>Edit</summary>
|
|
18
|
+
<form hx-post="/projects" hx-target="#app-main" hx-swap="outerHTML" class="project-form">
|
|
19
|
+
<input type="hidden" name="name" value="{{ p.name }}">
|
|
20
|
+
<label>Description<input name="description" value="{{ p.description }}"></label>
|
|
21
|
+
<label>Color<input type="color" name="color" value="{{ p.color }}"></label>
|
|
22
|
+
<button type="submit">Save</button>
|
|
23
|
+
</form>
|
|
24
|
+
</details>
|
|
25
|
+
</div>
|
|
26
|
+
{% endfor %}
|
|
27
|
+
</div>
|
|
28
|
+
{% else %}
|
|
29
|
+
<p class="muted">No projects yet. Add one below.</p>
|
|
30
|
+
{% endif %}
|
|
31
|
+
|
|
32
|
+
<form hx-post="/projects" hx-target="#app-main" hx-swap="outerHTML" class="project-form new-project">
|
|
33
|
+
<h3>Add project</h3>
|
|
34
|
+
<div class="project-form-row">
|
|
35
|
+
<label>Name<input name="name" placeholder="e.g. Apollo" required></label>
|
|
36
|
+
<label>Color<input type="color" name="color" value="#2e6fd8"></label>
|
|
37
|
+
</div>
|
|
38
|
+
<label>Description<input name="description" placeholder="Optional description"></label>
|
|
39
|
+
<button type="submit">Add Project</button>
|
|
40
|
+
</form>
|
|
41
|
+
</section>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<section class="panel">
|
|
2
|
+
<div class="panel-heading">
|
|
3
|
+
<h2>Task List</h2>
|
|
4
|
+
<span>{{ model.total }} task{{ '' if model.total == 1 else 's' }}</span>
|
|
5
|
+
</div>
|
|
6
|
+
{% if model.tasks %}
|
|
7
|
+
<table class="task-table">
|
|
8
|
+
<thead>
|
|
9
|
+
<tr>
|
|
10
|
+
<th>
|
|
11
|
+
<button class="table-sort{% if filters.sort == 'title' %} active{% endif %}" hx-get="/partials/main?view=list&sort=title{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Title</button>
|
|
12
|
+
</th>
|
|
13
|
+
<th>
|
|
14
|
+
<button class="table-sort{% if filters.sort == 'project' %} active{% endif %}" hx-get="/partials/main?view=list&sort=project{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Project</button>
|
|
15
|
+
</th>
|
|
16
|
+
<th>
|
|
17
|
+
<button class="table-sort{% if filters.sort == 'status' %} active{% endif %}" hx-get="/partials/main?view=list&sort=status{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Status</button>
|
|
18
|
+
</th>
|
|
19
|
+
<th>
|
|
20
|
+
<button class="table-sort{% if filters.sort == 'priority' %} active{% endif %}" hx-get="/partials/main?view=list&sort=priority{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Priority</button>
|
|
21
|
+
</th>
|
|
22
|
+
<th>
|
|
23
|
+
<button class="table-sort{% if filters.sort == 'due_date' %} active{% endif %}" hx-get="/partials/main?view=list&sort=due_date{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Due</button>
|
|
24
|
+
</th>
|
|
25
|
+
<th>
|
|
26
|
+
<button class="table-sort{% if filters.sort == 'percent_complete' %} active{% endif %}" hx-get="/partials/main?view=list&sort=percent_complete{% if filters.query_no_sort %}&{{ filters.query_no_sort }}{% endif %}" hx-target="#app-main" hx-swap="outerHTML">Progress</button>
|
|
27
|
+
</th>
|
|
28
|
+
</tr>
|
|
29
|
+
</thead>
|
|
30
|
+
<tbody>
|
|
31
|
+
{% for task in model.tasks %}
|
|
32
|
+
<tr class="task-row {{ task.status }}"
|
|
33
|
+
hx-get="/tasks/{{ task.id }}/panel?view={{ filters.view }}{% if filters.query %}&{{ filters.query }}{% endif %}"
|
|
34
|
+
hx-target="#task-panel" hx-swap="innerHTML">
|
|
35
|
+
<td>{{ task.title }}</td>
|
|
36
|
+
<td>{% if task.project %}<span class="project" style="background: {{ project_colors.get(task.project, '#2e6fd8') }}22; color: {{ project_colors.get(task.project, '#2e6fd8') }}">{{ task.project }}</span>{% endif %}</td>
|
|
37
|
+
<td><span class="status-pill {{ task.status }}">{{ task.status }}</span></td>
|
|
38
|
+
<td><span class="priority-tag {{ task.priority }}">{{ task.priority }}</span></td>
|
|
39
|
+
<td>{{ task.due_date or '—' }}</td>
|
|
40
|
+
<td>
|
|
41
|
+
<div class="mini-progress" title="{{ task.percent_complete }}% complete">
|
|
42
|
+
<div style="width: {{ task.percent_complete }}%"></div>
|
|
43
|
+
</div>
|
|
44
|
+
</td>
|
|
45
|
+
</tr>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
</tbody>
|
|
48
|
+
</table>
|
|
49
|
+
{% else %}
|
|
50
|
+
<p class="muted">No tasks match the current filters.</p>
|
|
51
|
+
{% endif %}
|
|
52
|
+
</section>
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
<div class="panel-scroll">
|
|
2
|
+
<div class="panel-heading sticky">
|
|
3
|
+
<div>
|
|
4
|
+
<h2>{{ selected_task.title }}</h2>
|
|
5
|
+
</div>
|
|
6
|
+
<form hx-post="/tasks/{{ selected_task.id }}/complete" hx-target="#app-main" hx-swap="outerHTML">
|
|
7
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
8
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
9
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
10
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
11
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
12
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
13
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
14
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
15
|
+
{% if selected_task.status == 'done' %}
|
|
16
|
+
<button type="submit" class="reopen-btn">Reopen task</button>
|
|
17
|
+
{% else %}
|
|
18
|
+
<button type="submit" class="complete-btn">✓ Complete task</button>
|
|
19
|
+
{% endif %}
|
|
20
|
+
</form>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<form hx-post="/tasks/{{ selected_task.id }}/save" hx-target="#app-main" hx-swap="outerHTML" class="task-form">
|
|
24
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
25
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
26
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
27
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
28
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
29
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
30
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
31
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
32
|
+
<label>Title<input name="title" value="{{ selected_task.title }}" required></label>
|
|
33
|
+
<div class="form-grid">
|
|
34
|
+
<label>Status
|
|
35
|
+
<select name="status">
|
|
36
|
+
{% for status in statuses %}<option value="{{ status }}" {% if selected_task.status == status %}selected{% endif %}>{{ status }}</option>{% endfor %}
|
|
37
|
+
</select>
|
|
38
|
+
</label>
|
|
39
|
+
<label>Priority
|
|
40
|
+
<select name="priority">
|
|
41
|
+
{% for priority in ['low', 'normal', 'high', 'critical'] %}<option value="{{ priority }}" {% if selected_task.priority == priority %}selected{% endif %}>{{ priority }}</option>{% endfor %}
|
|
42
|
+
</select>
|
|
43
|
+
</label>
|
|
44
|
+
</div>
|
|
45
|
+
<label class="field-label">Project</label>
|
|
46
|
+
<div class="project-picker" data-project-picker>
|
|
47
|
+
<input type="hidden" name="project" value="{{ selected_task.project }}">
|
|
48
|
+
<details class="project-dd">
|
|
49
|
+
<summary>
|
|
50
|
+
<span class="project-dd-current">
|
|
51
|
+
{% if selected_task.project %}
|
|
52
|
+
<span class="swatch" style="background: {{ project_colors.get(selected_task.project, '#3567e0') }}"></span>{{ selected_task.project }}
|
|
53
|
+
{% else %}
|
|
54
|
+
<span class="muted">Select a project…</span>
|
|
55
|
+
{% endif %}
|
|
56
|
+
</span>
|
|
57
|
+
<span class="chev" aria-hidden="true">▾</span>
|
|
58
|
+
</summary>
|
|
59
|
+
<div class="project-dd-menu">
|
|
60
|
+
{% for p in projects %}
|
|
61
|
+
<button type="button" class="project-dd-item {% if p.name == selected_task.project %}selected{% endif %}" data-value="{{ p.name }}" data-color="{{ p.color }}">
|
|
62
|
+
<span class="swatch" style="background: {{ p.color }}"></span>{{ p.name }}
|
|
63
|
+
</button>
|
|
64
|
+
{% endfor %}
|
|
65
|
+
<div class="project-dd-new">
|
|
66
|
+
<input type="text" class="project-dd-input" placeholder="New project name, press Enter…">
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</details>
|
|
70
|
+
</div>
|
|
71
|
+
<label>Summary<textarea name="summary" rows="2">{{ selected_task.summary }}</textarea></label>
|
|
72
|
+
<label>Description<textarea name="description" rows="5">{{ selected_task.description }}</textarea></label>
|
|
73
|
+
<div class="form-grid">
|
|
74
|
+
<label>Start date<input type="date" name="start_date" value="{{ selected_task.start_date or '' }}"></label>
|
|
75
|
+
<label>Due date<input type="date" name="due_date" value="{{ selected_task.due_date or '' }}"></label>
|
|
76
|
+
<label>Completed date<input type="date" name="completed_date" value="{{ selected_task.completed_date or '' }}"></label>
|
|
77
|
+
<label>Percent<input type="number" min="0" max="100" name="percent_complete" value="{{ selected_task.percent_complete }}"></label>
|
|
78
|
+
</div>
|
|
79
|
+
<label>Tags, comma-separated<input name="tags" value="{{ selected_task.tags|join(', ') }}"></label>
|
|
80
|
+
<div class="dep-picker" data-dep-picker>
|
|
81
|
+
<label class="field-label">Depends on</label>
|
|
82
|
+
<input type="hidden" name="depends_on" value="{{ selected_task.depends_on|join(', ') }}">
|
|
83
|
+
<div class="dep-chips">
|
|
84
|
+
{% for dep in selected_task.depends_on %}
|
|
85
|
+
<span class="dep-chip" data-id="{{ dep }}">{{ task_titles.get(dep, dep) }}<button type="button" class="dep-chip-x" data-dep-remove aria-label="Remove dependency">×</button></span>
|
|
86
|
+
{% endfor %}
|
|
87
|
+
</div>
|
|
88
|
+
<div class="dep-search">
|
|
89
|
+
<input type="text" class="dep-search-input" placeholder="Search tasks by name…" autocomplete="off">
|
|
90
|
+
<div class="dep-menu" hidden>
|
|
91
|
+
{% for t in task_index %}
|
|
92
|
+
{% if t.id != selected_task.id %}
|
|
93
|
+
<button type="button" class="dep-option" data-id="{{ t.id }}" data-search="{{ (t.title ~ ' ' ~ t.id ~ ' ' ~ t.project)|lower }}"{% if t.id in selected_task.depends_on %} hidden{% endif %}>
|
|
94
|
+
<span class="dep-option-title">{{ t.title }}</span>
|
|
95
|
+
<span class="dep-option-meta">
|
|
96
|
+
<span class="dep-status {{ t.status }}">{{ t.status }}</span>
|
|
97
|
+
{% if t.project %}<span class="dep-proj">{{ t.project }}</span>{% endif %}
|
|
98
|
+
{% if t.due_date %}<span class="dep-due">due {{ t.due_date }}</span>{% endif %}
|
|
99
|
+
<span class="dep-id">{{ t.id }}</span>
|
|
100
|
+
</span>
|
|
101
|
+
</button>
|
|
102
|
+
{% endif %}
|
|
103
|
+
{% endfor %}
|
|
104
|
+
<p class="dep-empty muted" hidden>No matching tasks.</p>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
<button type="submit">Save Task</button>
|
|
109
|
+
</form>
|
|
110
|
+
|
|
111
|
+
<section class="subsection checklist-section">
|
|
112
|
+
<div class="checklist-head">
|
|
113
|
+
<h3>Checklist</h3>
|
|
114
|
+
<span class="muted">Click to mark done</span>
|
|
115
|
+
</div>
|
|
116
|
+
<ul class="checklist-items">
|
|
117
|
+
{% for item in selected_task.checklist %}
|
|
118
|
+
<li class="checklist-item {% if item.done %}done{% endif %}">
|
|
119
|
+
<form hx-post="/tasks/{{ selected_task.id }}/checklist/{{ loop.index0 }}/toggle" hx-target="#app-main" hx-swap="outerHTML" class="checklist-toggle-form">
|
|
120
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
121
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
122
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
123
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
124
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
125
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
126
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
127
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
128
|
+
<button type="submit" class="check-toggle" aria-label="Toggle checklist item">
|
|
129
|
+
<span class="check-mark">{% if item.done %}✓{% endif %}</span>
|
|
130
|
+
<span class="check-text">{{ item.text }}</span>
|
|
131
|
+
</button>
|
|
132
|
+
</form>
|
|
133
|
+
<form hx-post="/tasks/{{ selected_task.id }}/checklist/{{ loop.index0 }}/delete" hx-target="#app-main" hx-swap="outerHTML" class="checklist-delete-form">
|
|
134
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
135
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
136
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
137
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
138
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
139
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
140
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
141
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
142
|
+
<button type="submit" class="check-delete" aria-label="Remove checklist item">×</button>
|
|
143
|
+
</form>
|
|
144
|
+
</li>
|
|
145
|
+
{% else %}
|
|
146
|
+
<li class="muted">No checklist items yet.</li>
|
|
147
|
+
{% endfor %}
|
|
148
|
+
</ul>
|
|
149
|
+
<form hx-post="/tasks/{{ selected_task.id }}/checklist/add" hx-target="#app-main" hx-swap="outerHTML" class="checklist-add-form">
|
|
150
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
151
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
152
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
153
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
154
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
155
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
156
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
157
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
158
|
+
<input name="item_text" placeholder="Add checklist item" required>
|
|
159
|
+
<button type="submit">Add item</button>
|
|
160
|
+
</form>
|
|
161
|
+
</section>
|
|
162
|
+
|
|
163
|
+
<section class="subsection activity-section">
|
|
164
|
+
<div class="notes-head">
|
|
165
|
+
<h3>Activity Log <span class="notes-count-badge">{{ task_activity_entries|length }}</span></h3>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="activity-timeline">
|
|
168
|
+
{% for entry in task_activity_entries %}
|
|
169
|
+
<article class="activity-item">
|
|
170
|
+
<div class="activity-item-icon" aria-hidden="true">
|
|
171
|
+
{% if entry.kind == 'progress_update' %}📈{% elif entry.kind == 'note' %}📝{% elif entry.is_image %}🖼️{% else %}📄{% endif %}
|
|
172
|
+
</div>
|
|
173
|
+
<div class="activity-item-body">
|
|
174
|
+
<div class="activity-item-meta">
|
|
175
|
+
<span class="activity-item-type">
|
|
176
|
+
{% if entry.kind == 'progress_update' %}Progress{% elif entry.kind == 'note' %}Note{% elif entry.is_image %}Image{% else %}File{% endif %}
|
|
177
|
+
</span>
|
|
178
|
+
<time datetime="{{ entry.created_at }}">{{ entry.created_at }}</time>
|
|
179
|
+
</div>
|
|
180
|
+
{% if entry.kind == 'progress_update' %}
|
|
181
|
+
<div class="activity-progress">Progress: {{ entry.progress_before }}% → {{ entry.progress_after }}%</div>
|
|
182
|
+
{% elif entry.kind == 'note' %}
|
|
183
|
+
<div class="markdown">{{ (entry.body or '')|markdown|safe }}</div>
|
|
184
|
+
{% else %}
|
|
185
|
+
<div class="attachment-item activity-asset">
|
|
186
|
+
{% if entry.is_image and entry.path %}
|
|
187
|
+
<a href="/{{ entry.path }}" target="_blank" class="attachment-thumb"><img src="/{{ entry.path }}" alt="{{ entry.filename or 'Attachment' }}"></a>
|
|
188
|
+
{% else %}
|
|
189
|
+
<span class="attachment-icon" aria-hidden="true">📄</span>
|
|
190
|
+
{% endif %}
|
|
191
|
+
<div class="attachment-info">
|
|
192
|
+
{% if entry.path %}
|
|
193
|
+
<a href="/{{ entry.path }}" target="_blank" class="attachment-name">{{ entry.filename or 'Attachment' }}</a>
|
|
194
|
+
{% else %}
|
|
195
|
+
<span class="attachment-name">{{ entry.filename or 'Attachment' }}</span>
|
|
196
|
+
{% endif %}
|
|
197
|
+
{% if entry.body %}<div class="attachment-desc markdown">{{ entry.body|markdown|safe }}</div>{% endif %}
|
|
198
|
+
{% if entry.path %}
|
|
199
|
+
<div class="attachment-meta"><a href="/{{ entry.path }}" download class="attachment-dl">Download</a></div>
|
|
200
|
+
{% endif %}
|
|
201
|
+
</div>
|
|
202
|
+
</div>
|
|
203
|
+
{% endif %}
|
|
204
|
+
</div>
|
|
205
|
+
</article>
|
|
206
|
+
{% else %}
|
|
207
|
+
<p class="muted">No activity yet. Add your first update below.</p>
|
|
208
|
+
{% endfor %}
|
|
209
|
+
</div>
|
|
210
|
+
<div class="activity-composer" data-activity-composer>
|
|
211
|
+
<div class="composer-switch">
|
|
212
|
+
<button type="button" class="composer-tab active" data-activity-tab="note">Add Note</button>
|
|
213
|
+
<button type="button" class="composer-tab" data-activity-tab="upload">Upload</button>
|
|
214
|
+
</div>
|
|
215
|
+
<form hx-post="/tasks/{{ selected_task.id }}/note" hx-target="#task-panel" hx-swap="innerHTML" class="inline-form note-add" data-activity-pane="note">
|
|
216
|
+
<label class="field-label">Add a note</label>
|
|
217
|
+
<textarea name="body" rows="3" placeholder="Write a note using Markdown…"></textarea>
|
|
218
|
+
<button type="submit">Add Note</button>
|
|
219
|
+
</form>
|
|
220
|
+
<form hx-post="/tasks/{{ selected_task.id }}/attachment" hx-target="#task-panel" hx-swap="innerHTML" enctype="multipart/form-data" class="attachment-form" data-activity-pane="upload" hidden>
|
|
221
|
+
<input type="file" name="attachment" required>
|
|
222
|
+
<input type="text" name="description" placeholder="Description (optional)">
|
|
223
|
+
<button type="submit">Upload</button>
|
|
224
|
+
</form>
|
|
225
|
+
</div>
|
|
226
|
+
</section>
|
|
227
|
+
|
|
228
|
+
<section class="subsection">
|
|
229
|
+
<h3>Task Burndown Chart</h3>
|
|
230
|
+
<canvas id="task-burndown-{{ selected_task.id }}" class="burndown-canvas" height="180"></canvas>
|
|
231
|
+
<script>
|
|
232
|
+
(function () {
|
|
233
|
+
var canvas = document.getElementById('task-burndown-{{ selected_task.id }}');
|
|
234
|
+
if (!canvas || typeof Chart === 'undefined') return;
|
|
235
|
+
window.taskunityCharts = window.taskunityCharts || {};
|
|
236
|
+
if (window.taskunityCharts[canvas.id]) window.taskunityCharts[canvas.id].destroy();
|
|
237
|
+
fetch('/tasks/{{ selected_task.id }}/burndown.json')
|
|
238
|
+
.then(function (response) { return response.json(); })
|
|
239
|
+
.then(function (payload) {
|
|
240
|
+
var points = (payload.points || []).map(function (point) {
|
|
241
|
+
// Convert ISO string to ms timestamp so Chart.js time scale parses it reliably
|
|
242
|
+
return { x: new Date(point.x || new Date()).getTime(), y: point.y, label: point.label };
|
|
243
|
+
});
|
|
244
|
+
window.taskunityCharts[canvas.id] = new Chart(canvas, {
|
|
245
|
+
type: 'line',
|
|
246
|
+
data: {
|
|
247
|
+
datasets: [{
|
|
248
|
+
label: 'Remaining work',
|
|
249
|
+
data: points,
|
|
250
|
+
borderColor: '#3567e0',
|
|
251
|
+
backgroundColor: 'rgba(53, 103, 224, 0.12)',
|
|
252
|
+
fill: true,
|
|
253
|
+
tension: 0.25,
|
|
254
|
+
stepped: 'before'
|
|
255
|
+
}]
|
|
256
|
+
},
|
|
257
|
+
options: {
|
|
258
|
+
parsing: false,
|
|
259
|
+
plugins: {
|
|
260
|
+
legend: { display: false },
|
|
261
|
+
tooltip: {
|
|
262
|
+
callbacks: {
|
|
263
|
+
label: function (ctx) {
|
|
264
|
+
return (ctx.raw && ctx.raw.label ? ctx.raw.label + ' • ' : '') + 'Remaining ' + ctx.raw.y + '%';
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
scales: {
|
|
270
|
+
x: { type: 'time', time: { unit: 'day', displayFormats: { day: 'MMM d' }, tooltipFormat: 'MMM d, yyyy' } },
|
|
271
|
+
y: { beginAtZero: true, max: 100, title: { display: true, text: 'Remaining %' } }
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
})
|
|
276
|
+
.catch(function () {});
|
|
277
|
+
})();
|
|
278
|
+
</script>
|
|
279
|
+
</section>
|
|
280
|
+
|
|
281
|
+
<details class="subsection">
|
|
282
|
+
<summary>Raw JSON editor</summary>
|
|
283
|
+
<form class="raw-json-form" hx-post="/tasks/{{ selected_task.id }}/raw" hx-target="#app-main" hx-swap="outerHTML">
|
|
284
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
285
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
286
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
287
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
288
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
289
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
290
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
291
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
292
|
+
<textarea name="raw_json" rows="18" class="code">{{ selected_task.model_dump_json(indent=2) }}</textarea>
|
|
293
|
+
<button type="submit">Save Raw JSON</button>
|
|
294
|
+
</form>
|
|
295
|
+
</details>
|
|
296
|
+
|
|
297
|
+
<form action="/tasks/{{ selected_task.id }}/delete" method="post" onsubmit="return confirm('Delete {{ selected_task.id }}?')" class="danger-zone">
|
|
298
|
+
{% for p in filters.projects %}<input type="hidden" name="f_project" value="{{ p }}">{% endfor %}
|
|
299
|
+
<input type="hidden" name="f_from" value="{{ filters.date_from }}">
|
|
300
|
+
<input type="hidden" name="f_to" value="{{ filters.date_to }}">
|
|
301
|
+
<input type="hidden" name="f_q" value="{{ filters.q }}">
|
|
302
|
+
<input type="hidden" name="f_view" value="{{ filters.view }}">
|
|
303
|
+
<input type="hidden" name="f_milestone" value="{{ filters.milestone }}">
|
|
304
|
+
<input type="hidden" name="f_show_closed" value="{% if filters.show_closed %}1{% endif %}">
|
|
305
|
+
<input type="hidden" name="f_stale_days" value="{{ filters.stale_days }}">
|
|
306
|
+
<button type="submit">Delete Task</button>
|
|
307
|
+
</form>
|
|
308
|
+
|
|
309
|
+
<p class="task-id-footer">Task ID: <span class="mono">{{ selected_task.id }}</span></p>
|
|
310
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<div class="schedule-body">
|
|
2
|
+
{% if model.timeline.start %}<p class="schedule-range">{{ model.timeline.start }} → {{ model.timeline.end }}</p>{% endif %}
|
|
3
|
+
{% if model.timeline.rows %}
|
|
4
|
+
<div class="timeline">
|
|
5
|
+
{% for item in model.timeline.rows %}
|
|
6
|
+
<button class="timeline-row" hx-get="/tasks/{{ item.task.id }}/panel?view={{ filters.view }}{% if filters.query %}&{{ filters.query }}{% endif %}" hx-target="#task-panel" hx-swap="innerHTML">
|
|
7
|
+
<span class="timeline-title">
|
|
8
|
+
{{ item.task.title }}
|
|
9
|
+
{% if item.task.project %}<small class="project" style="background: {{ project_colors.get(item.task.project, '#2e6fd8') }}22; color: {{ project_colors.get(item.task.project, '#2e6fd8') }}">{{ item.task.project }}</small>{% endif %}
|
|
10
|
+
{% if item.task.depends_on %}<small class="timeline-deps">↳ after {% for d in item.task.depends_on %}{{ task_titles.get(d, d) }}{% if not loop.last %}, {% endif %}{% endfor %}</small>{% endif %}
|
|
11
|
+
</span>
|
|
12
|
+
<span class="timeline-track">
|
|
13
|
+
<span class="timeline-bar {{ item.task.status }}"{% if item.task.project %} style="left: {{ item.left }}%; width: {{ item.width }}%; background: {{ project_colors.get(item.task.project, '#2e6fd8') }}"{% else %} style="left: {{ item.left }}%; width: {{ item.width }}%"{% endif %}></span>
|
|
14
|
+
{% for mark in item.dep_marks %}
|
|
15
|
+
<span class="timeline-dep-mark" style="left: {{ mark.pos }}%" title="Depends on {{ task_titles.get(mark.id, mark.id) }} (ends here)"></span>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</span>
|
|
18
|
+
</button>
|
|
19
|
+
{% endfor %}
|
|
20
|
+
</div>
|
|
21
|
+
{% else %}
|
|
22
|
+
<p class="muted">Add start or due dates to tasks to populate the timeline.</p>
|
|
23
|
+
{% endif %}
|
|
24
|
+
</div>
|