@mongoosejs/studio 0.2.12 → 0.2.13

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.
@@ -0,0 +1,15 @@
1
+ <div class="p-4 space-y-6">
2
+ <div v-if="status === 'init'">
3
+ <img src="images/loader.gif" alt="Loading" />
4
+ </div>
5
+ <div v-else-if="status === 'error'" class="text-red-600">
6
+ {{ errorMessage }}
7
+ </div>
8
+ <task-details
9
+ v-else-if="taskGroup"
10
+ :task-group="taskGroup"
11
+ :back-to="{ name: 'tasks' }"
12
+ @task-created="onTaskCreated"
13
+ @task-cancelled="onTaskCancelled"
14
+ ></task-details>
15
+ </div>
@@ -0,0 +1,78 @@
1
+ 'use strict';
2
+
3
+ // Page: all tasks with a given name. Reuses task-details to render the list (many tasks).
4
+ const template = require('./task-by-name.html');
5
+ const api = require('../api');
6
+
7
+ function buildTaskGroup(name, tasks) {
8
+ const statusCounts = { pending: 0, succeeded: 0, failed: 0, cancelled: 0, in_progress: 0, unknown: 0 };
9
+ let lastRun = null;
10
+ tasks.forEach(task => {
11
+ const status = task.status || 'unknown';
12
+ if (Object.prototype.hasOwnProperty.call(statusCounts, status)) {
13
+ statusCounts[status]++;
14
+ } else {
15
+ statusCounts.unknown++;
16
+ }
17
+ const taskTime = new Date(task.scheduledAt || task.createdAt || 0);
18
+ if (!lastRun || taskTime > lastRun) lastRun = taskTime;
19
+ });
20
+ return {
21
+ name,
22
+ tasks,
23
+ statusCounts,
24
+ totalCount: tasks.length,
25
+ lastRun
26
+ };
27
+ }
28
+
29
+ module.exports = app => app.component('task-by-name', {
30
+ template,
31
+ data: () => ({
32
+ status: 'init',
33
+ taskGroup: null,
34
+ errorMessage: ''
35
+ }),
36
+ computed: {
37
+ taskName() {
38
+ return this.$route.params.name || '';
39
+ }
40
+ },
41
+ watch: {
42
+ taskName: {
43
+ immediate: true,
44
+ handler() {
45
+ this.loadTasks();
46
+ }
47
+ }
48
+ },
49
+ methods: {
50
+ async loadTasks() {
51
+ if (!this.taskName) return;
52
+ this.status = 'init';
53
+ this.taskGroup = null;
54
+ this.errorMessage = '';
55
+ const start = new Date();
56
+ start.setHours(start.getHours() - 1);
57
+ const end = new Date();
58
+ try {
59
+ const { tasks } = await api.Task.getTasks({
60
+ name: this.taskName,
61
+ start,
62
+ end
63
+ });
64
+ this.taskGroup = buildTaskGroup(this.taskName, tasks);
65
+ this.status = 'loaded';
66
+ } catch (err) {
67
+ this.status = 'error';
68
+ this.errorMessage = err?.response?.data?.message || err.message || 'Failed to load tasks';
69
+ }
70
+ },
71
+ async onTaskCreated() {
72
+ await this.loadTasks();
73
+ },
74
+ async onTaskCancelled() {
75
+ await this.loadTasks();
76
+ }
77
+ }
78
+ });
@@ -0,0 +1,157 @@
1
+ <div class="p-4 space-y-6">
2
+ <div v-if="status === 'init'">
3
+ <img src="images/loader.gif" alt="Loading" />
4
+ </div>
5
+ <div v-else-if="status === 'error'" class="text-red-600">
6
+ {{ errorMessage }}
7
+ </div>
8
+ <div v-else-if="status === 'notfound'" class="text-gray-600">
9
+ Task not found.
10
+ </div>
11
+ <div v-else-if="task" class="max-w-4xl">
12
+ <button @click="goBack" class="text-gray-500 hover:text-gray-700 mb-4 flex items-center gap-1">
13
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
14
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
15
+ </svg>
16
+ Back to {{ task.name }}
17
+ </button>
18
+ <h1 class="text-2xl font-bold text-gray-700 mb-1">{{ task.name }}</h1>
19
+ <p class="text-gray-500 mb-6">Task details</p>
20
+
21
+ <div class="bg-white rounded-lg shadow p-6 md:p-8">
22
+ <div class="flex items-center gap-3 mb-6">
23
+ <span class="text-sm font-medium text-gray-900">ID: {{ task.id }}</span>
24
+ <span
25
+ class="text-xs px-2 py-1 rounded-full font-medium"
26
+ :class="getStatusColor(task.status)"
27
+ >
28
+ {{ task?.status }}
29
+ </span>
30
+ </div>
31
+
32
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
33
+ <div>
34
+ <label class="block text-sm font-medium text-gray-700 mb-1">Scheduled At</label>
35
+ <div class="text-sm text-gray-900">{{ formatDate(task.scheduledAt) }}</div>
36
+ </div>
37
+ <div v-if="task?.startedAt">
38
+ <label class="block text-sm font-medium text-gray-700 mb-1">Started At</label>
39
+ <div class="text-sm text-gray-900">{{ formatDate(task.startedAt) }}</div>
40
+ </div>
41
+ <div v-if="task?.completedAt">
42
+ <label class="block text-sm font-medium text-gray-700 mb-1">Completed At</label>
43
+ <div class="text-sm text-gray-900">{{ formatDate(task.completedAt) }}</div>
44
+ </div>
45
+ </div>
46
+
47
+ <div v-if="task?.params" class="mb-6">
48
+ <label class="block text-sm font-medium text-gray-700 mb-2">Params</label>
49
+ <div class="bg-gray-50 rounded-md p-4">
50
+ <list-json :value="task.params"></list-json>
51
+ </div>
52
+ </div>
53
+
54
+ <div v-if="task?.result" class="mb-6">
55
+ <label class="block text-sm font-medium text-gray-700 mb-2">Result</label>
56
+ <div class="bg-gray-50 rounded-md p-4">
57
+ <list-json :value="task.result"></list-json>
58
+ </div>
59
+ </div>
60
+
61
+ <div v-if="task?.error" class="mb-6">
62
+ <label class="block text-sm font-medium text-gray-700 mb-2">Error</label>
63
+ <div class="bg-gray-50 rounded-md p-4">
64
+ <list-json :value="task.error"></list-json>
65
+ </div>
66
+ </div>
67
+
68
+ <div class="flex flex-wrap gap-3 pt-4 border-t border-gray-200">
69
+ <button
70
+ @click="showRescheduleConfirmation(task)"
71
+ class="flex items-center justify-center gap-2 bg-gradient-to-r from-blue-500 to-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-blue-600 hover:to-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
72
+ :disabled="task.status === 'in_progress'"
73
+ >
74
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path>
76
+ </svg>
77
+ Reschedule
78
+ </button>
79
+ <button
80
+ @click="showRunConfirmation(task)"
81
+ class="flex items-center justify-center gap-2 bg-gradient-to-r from-green-500 to-green-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-green-600 hover:to-green-700 disabled:opacity-50 disabled:cursor-not-allowed"
82
+ :disabled="task.status === 'in_progress'"
83
+ >
84
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
85
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 3l14 9-14 9V3z"></path>
86
+ </svg>
87
+ Run Now
88
+ </button>
89
+ <button
90
+ v-if="task.status === 'pending'"
91
+ @click="showCancelConfirmation(task)"
92
+ class="flex items-center justify-center gap-2 bg-gradient-to-r from-red-500 to-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:from-red-600 hover:to-red-700"
93
+ >
94
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
96
+ </svg>
97
+ Cancel Task
98
+ </button>
99
+ </div>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Reschedule Modal -->
104
+ <modal v-if="showRescheduleModal" containerClass="!max-w-md">
105
+ <template #body>
106
+ <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRescheduleModal = false" role="button" aria-label="Close modal">&times;</div>
107
+ <div class="p-6">
108
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Reschedule Task</h3>
109
+ <p class="text-sm text-gray-600 mb-2">Reschedule task <strong>{{ selectedTask?.id }}</strong>?</p>
110
+ <p class="text-sm text-gray-500 mb-4">This will reset the task's status and schedule it to run again.</p>
111
+ <label for="newScheduledTime" class="block text-sm font-medium text-gray-700 mb-2">New Scheduled Time</label>
112
+ <input
113
+ id="newScheduledTime"
114
+ v-model="newScheduledTime"
115
+ type="datetime-local"
116
+ class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500 mb-4"
117
+ />
118
+ <div class="flex gap-3">
119
+ <button @click="confirmRescheduleTask" class="flex-1 bg-blue-600 text-white px-4 py-2 rounded-md hover:bg-blue-700 font-medium">Reschedule</button>
120
+ <button @click="showRescheduleModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Cancel</button>
121
+ </div>
122
+ </div>
123
+ </template>
124
+ </modal>
125
+
126
+ <!-- Run Modal -->
127
+ <modal v-if="showRunModal" containerClass="!max-w-md">
128
+ <template #body>
129
+ <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showRunModal = false" role="button" aria-label="Close modal">&times;</div>
130
+ <div class="p-6">
131
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Run Task Now</h3>
132
+ <p class="text-sm text-gray-600 mb-2">Run task <strong>{{ selectedTask?.id }}</strong> immediately?</p>
133
+ <p class="text-sm text-gray-500 mb-4">This will execute the task right away, bypassing its scheduled time.</p>
134
+ <div class="flex gap-3">
135
+ <button @click="confirmRunTask" class="flex-1 bg-green-600 text-white px-4 py-2 rounded-md hover:bg-green-700 font-medium">Run Now</button>
136
+ <button @click="showRunModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Cancel</button>
137
+ </div>
138
+ </div>
139
+ </template>
140
+ </modal>
141
+
142
+ <!-- Cancel Task Modal -->
143
+ <modal v-if="showCancelModal" containerClass="!max-w-md">
144
+ <template #body>
145
+ <div class="absolute font-mono right-1 top-1 cursor-pointer text-xl" @click="showCancelModal = false" role="button" aria-label="Close modal">&times;</div>
146
+ <div class="p-6">
147
+ <h3 class="text-lg font-medium text-gray-900 mb-4">Cancel Task</h3>
148
+ <p class="text-sm text-gray-600 mb-2">Cancel task <strong>{{ selectedTask?.id }}</strong>?</p>
149
+ <p class="text-sm text-gray-500 mb-4">This will permanently cancel the task and it cannot be undone.</p>
150
+ <div class="flex gap-3">
151
+ <button @click="confirmCancelTask" class="flex-1 bg-red-600 text-white px-4 py-2 rounded-md hover:bg-red-700 font-medium">Cancel Task</button>
152
+ <button @click="showCancelModal = false" class="flex-1 bg-gray-300 text-gray-700 px-4 py-2 rounded-md hover:bg-gray-400 font-medium">Keep Task</button>
153
+ </div>
154
+ </div>
155
+ </template>
156
+ </modal>
157
+ </div>
@@ -0,0 +1,116 @@
1
+ 'use strict';
2
+
3
+ // Page: one task by id. Dedicated single-task detail UI (not the list).
4
+ const template = require('./task-single.html');
5
+ const api = require('../api');
6
+
7
+ module.exports = app => app.component('task-single', {
8
+ template,
9
+ data: () => ({
10
+ status: 'init',
11
+ task: null,
12
+ errorMessage: '',
13
+ showRescheduleModal: false,
14
+ showRunModal: false,
15
+ showCancelModal: false,
16
+ selectedTask: null,
17
+ newScheduledTime: ''
18
+ }),
19
+ computed: {
20
+ taskId() {
21
+ return this.$route.params.id || '';
22
+ },
23
+ taskByNamePath() {
24
+ const name = this.task?.name ?? this.$route.params.name ?? '';
25
+ const path = `/tasks/${encodeURIComponent(name || '')}`;
26
+ const query = this.$route.query?.status ? { status: this.$route.query.status } : {};
27
+ return { path, query };
28
+ },
29
+ },
30
+ watch: {
31
+ '$route.params': {
32
+ deep: true,
33
+ handler() {
34
+ this.loadTask();
35
+ }
36
+ }
37
+ },
38
+ methods: {
39
+ getStatusColor(status) {
40
+ if (status === 'succeeded') return 'bg-green-100 text-green-800';
41
+ if (status === 'pending') return 'bg-yellow-100 text-yellow-800';
42
+ if (status === 'cancelled') return 'bg-gray-100 text-gray-800';
43
+ if (status === 'failed') return 'bg-red-100 text-red-800';
44
+ if (status === 'in_progress') return 'bg-blue-100 text-blue-800';
45
+ return 'bg-slate-100 text-slate-800';
46
+ },
47
+ formatDate(dateString) {
48
+ if (!dateString) return 'N/A';
49
+ return new Date(dateString).toLocaleString();
50
+ },
51
+ async loadTask() {
52
+ if (!this.taskId) return;
53
+ this.status = 'init';
54
+ this.task = null;
55
+ this.errorMessage = '';
56
+ try {
57
+ const { doc } = await api.Model.getDocument({ model: 'Task', documentId: this.taskId });
58
+ this.task = doc;
59
+ this.status = 'loaded';
60
+ } catch (err) {
61
+ const status = err?.response?.status;
62
+ const notFound = status === 404 || err?.response?.data?.name === 'DocumentNotFoundError';
63
+ this.status = notFound ? 'notfound' : 'error';
64
+ this.errorMessage = notFound ? '' : (err?.response?.data?.message || err.message || 'Failed to load task');
65
+ }
66
+ },
67
+ showRescheduleConfirmation(task) {
68
+ this.selectedTask = task;
69
+ const defaultTime = new Date();
70
+ defaultTime.setHours(defaultTime.getHours() + 1);
71
+ this.newScheduledTime = defaultTime.toISOString().slice(0, 16);
72
+ this.showRescheduleModal = true;
73
+ },
74
+ showRunConfirmation(task) {
75
+ this.selectedTask = task;
76
+ this.showRunModal = true;
77
+ },
78
+ showCancelConfirmation(task) {
79
+ this.selectedTask = task;
80
+ this.showCancelModal = true;
81
+ },
82
+ async confirmRescheduleTask() {
83
+ if (!this.newScheduledTime) return;
84
+ await api.Task.rescheduleTask({ taskId: this.selectedTask.id, scheduledAt: this.newScheduledTime });
85
+ this.$toast.success({ title: 'Task Rescheduled', text: `Task ${this.selectedTask.id} has been rescheduled`, });
86
+ this.showRescheduleModal = false;
87
+ this.selectedTask = null;
88
+ this.newScheduledTime = '';
89
+ await this.loadTask();
90
+ },
91
+ async confirmRunTask() {
92
+ await api.Task.runTask({ taskId: this.selectedTask.id });
93
+ this.$toast.success({ title: 'Task Started', text: `Task ${this.selectedTask.id} is now running`, type: 'success' });
94
+ this.showRunModal = false;
95
+ this.selectedTask = null;
96
+ await this.loadTask();
97
+ },
98
+ goBack() {
99
+ if (window.history.length > 1) {
100
+ window.history.back();
101
+ } else {
102
+ this.$router.push(this.taskByNamePath);
103
+ }
104
+ },
105
+ async confirmCancelTask() {
106
+ await api.Task.cancelTask({ taskId: this.selectedTask.id });
107
+ this.$toast.success({ title: 'Task Cancelled', text: `Task ${this.selectedTask.id} has been cancelled` });
108
+ this.showCancelModal = false;
109
+ this.selectedTask = null;
110
+ this.goBack();
111
+ }
112
+ },
113
+ mounted() {
114
+ this.loadTask();
115
+ }
116
+ });
@@ -1,11 +1,11 @@
1
1
  <div class="p-4 space-y-6">
2
2
  <div class="flex items-center justify-between">
3
3
  <div>
4
- <button @click="$emit('back')" class="text-gray-500 hover:text-gray-700 mb-2">
4
+ <button @click="goBack" class="text-gray-500 hover:text-gray-700 mb-2">
5
5
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
6
6
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"></path>
7
7
  </svg>
8
- Back to Task Groups
8
+ {{ backLabel }}
9
9
  </button>
10
10
  <h1 class="text-2xl font-bold text-gray-700">{{ taskGroup.name }}</h1>
11
11
  <p class="text-gray-500">Total: {{ taskGroup.totalCount }} tasks</p>
@@ -14,64 +14,115 @@
14
14
  </div>
15
15
 
16
16
  <!-- Status Summary -->
17
- <div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
18
- <button
19
- @click="filterByStatus('pending')"
20
- class="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer"
21
- :class="{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }"
22
- >
23
- <div class="text-xs text-yellow-600 font-medium">Pending</div>
24
- <div class="text-lg font-bold text-yellow-700">{{ taskGroup.statusCounts.pending || 0 }}</div>
25
- </button>
26
- <button
27
- @click="filterByStatus('succeeded')"
28
- class="bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer"
29
- :class="{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }"
30
- >
31
- <div class="text-xs text-green-600 font-medium">Succeeded</div>
32
- <div class="text-lg font-bold text-green-700">{{ taskGroup.statusCounts.succeeded || 0 }}</div>
33
- </button>
34
- <button
35
- @click="filterByStatus('failed')"
36
- class="bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer"
37
- :class="{ 'ring-2 ring-red-400': currentFilter === 'failed' }"
38
- >
39
- <div class="text-xs text-red-600 font-medium">Failed</div>
40
- <div class="text-lg font-bold text-red-700">{{ taskGroup.statusCounts.failed || 0 }}</div>
41
- </button>
42
- <button
43
- @click="filterByStatus('cancelled')"
44
- class="bg-gray-50 border border-gray-200 rounded-md p-3 text-center hover:bg-gray-100 transition-colors cursor-pointer"
45
- :class="{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }"
46
- >
47
- <div class="text-xs text-gray-600 font-medium">Cancelled</div>
48
- <div class="text-lg font-bold text-gray-700">{{ taskGroup.statusCounts.cancelled || 0 }}</div>
49
- </button>
50
- </div>
51
-
52
- <!-- Task List -->
53
- <div class="bg-white rounded-lg shadow">
54
- <div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
55
- <h2 class="text-lg font-semibold text-gray-700">
56
- Individual Tasks
57
- <span v-if="currentFilter" class="text-sm font-normal text-gray-500 ml-2">
58
- (Filtered by {{ currentFilter }})
59
- </span>
60
- </h2>
17
+ <div class="space-y-3">
18
+ <div class="flex items-center justify-between">
19
+ <span class="text-sm font-medium text-gray-700">Status</span>
20
+ <div class="flex rounded-md shadow-sm" role="group">
21
+ <button
22
+ type="button"
23
+ @click="statusView = 'summary'"
24
+ class="px-3 py-1.5 text-sm font-medium rounded-l-md border transition-colors"
25
+ :class="statusView === 'summary' ? 'bg-ultramarine-600 text-white border-ultramarine-600' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'"
26
+ >
27
+ Summary
28
+ </button>
29
+ <button
30
+ type="button"
31
+ @click="statusView = 'chart'"
32
+ class="px-3 py-1.5 text-sm font-medium rounded-r-md border border-l-0 transition-colors"
33
+ :class="statusView === 'chart' ? 'bg-ultramarine-600 text-white border-ultramarine-600' : 'bg-white text-gray-700 border-gray-300 hover:bg-gray-50'"
34
+ >
35
+ Chart
36
+ </button>
37
+ </div>
38
+ </div>
39
+ <!-- Summary view -->
40
+ <div v-show="statusView === 'summary'" class="grid grid-cols-2 sm:grid-cols-4 gap-4">
41
+ <button
42
+ @click="filterByStatus('pending')"
43
+ class="bg-yellow-50 border border-yellow-200 rounded-md p-3 text-center hover:bg-yellow-100 transition-colors cursor-pointer"
44
+ :class="{ 'ring-2 ring-yellow-400': currentFilter === 'pending' }"
45
+ >
46
+ <div class="text-xs text-yellow-600 font-medium">Pending</div>
47
+ <div class="text-lg font-bold text-yellow-700">{{ taskGroup.statusCounts.pending || 0 }}</div>
48
+ </button>
49
+ <button
50
+ @click="filterByStatus('succeeded')"
51
+ class="bg-green-50 border border-green-200 rounded-md p-3 text-center hover:bg-green-100 transition-colors cursor-pointer"
52
+ :class="{ 'ring-2 ring-green-400': currentFilter === 'succeeded' }"
53
+ >
54
+ <div class="text-xs text-green-600 font-medium">Succeeded</div>
55
+ <div class="text-lg font-bold text-green-700">{{ taskGroup.statusCounts.succeeded || 0 }}</div>
56
+ </button>
61
57
  <button
62
- v-if="currentFilter"
63
- @click="clearFilter"
64
- class="text-sm text-ultramarine-600 hover:text-ultramarine-700 font-medium"
58
+ @click="filterByStatus('failed')"
59
+ class="bg-red-50 border border-red-200 rounded-md p-3 text-center hover:bg-red-100 transition-colors cursor-pointer"
60
+ :class="{ 'ring-2 ring-red-400': currentFilter === 'failed' }"
65
61
  >
66
- Show All
62
+ <div class="text-xs text-red-600 font-medium">Failed</div>
63
+ <div class="text-lg font-bold text-red-700">{{ taskGroup.statusCounts.failed || 0 }}</div>
64
+ </button>
65
+ <button
66
+ @click="filterByStatus('cancelled')"
67
+ class="bg-gray-50 border border-gray-200 rounded-md p-3 text-center hover:bg-gray-100 transition-colors cursor-pointer"
68
+ :class="{ 'ring-2 ring-gray-400': currentFilter === 'cancelled' }"
69
+ >
70
+ <div class="text-xs text-gray-600 font-medium">Cancelled</div>
71
+ <div class="text-lg font-bold text-gray-700">{{ taskGroup.statusCounts.cancelled || 0 }}</div>
67
72
  </button>
68
73
  </div>
74
+ <!-- Chart view -->
75
+ <div v-show="statusView === 'chart'" class="flex flex-col items-center justify-center bg-white border border-gray-200 rounded-lg p-4 gap-3" style="min-height: 280px;">
76
+ <div v-if="taskGroup.totalCount > 0" class="w-[240px] h-[240px] shrink-0">
77
+ <canvas ref="statusPieChart" width="240" height="240" class="block"></canvas>
78
+ </div>
79
+ <p v-else class="text-gray-500 text-sm py-8">No tasks to display</p>
80
+ <!-- Selection labels: show which segment is selected (click to filter) -->
81
+ <div v-if="taskGroup.totalCount > 0" class="flex flex-wrap justify-center gap-2">
82
+ <button
83
+ v-for="status in statusOrderForDisplay"
84
+ :key="status"
85
+ type="button"
86
+ class="text-xs px-2 py-1 rounded-full font-medium transition-all cursor-pointer"
87
+ :class="currentFilter === status ? getStatusPillClass(status) : 'bg-gray-100 text-gray-500 hover:bg-gray-200'"
88
+ @click="filterByStatus(status)"
89
+ >
90
+ {{ statusLabel(status) }}
91
+ </button>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Task List -->
97
+ <div class="bg-white rounded-lg shadow">
98
+ <div class="px-6 py-6 border-b border-gray-200 flex items-center justify-between bg-gray-50">
99
+ <h2 class="text-xl font-bold text-gray-900">
100
+ Individual Tasks
101
+ <span v-if="currentFilter" class="ml-3 text-base font-semibold text-ultramarine-700">
102
+ (Filtered by {{ currentFilter }})
103
+ </span>
104
+ </h2>
105
+ <button
106
+ v-if="currentFilter"
107
+ @click="clearFilter"
108
+ class="text-sm font-semibold text-ultramarine-600 hover:text-ultramarine-700"
109
+ >
110
+ Show All
111
+ </button>
112
+ </div>
69
113
  <div class="divide-y divide-gray-200">
70
114
  <div v-for="task in sortedTasks" :key="task.id" class="p-6">
71
115
  <div class="flex items-start justify-between">
72
116
  <div class="flex-1">
73
117
  <div class="flex items-center gap-3 mb-2">
74
118
  <span class="text-sm font-medium text-gray-900">Task ID: {{ task.id }}</span>
119
+ <router-link
120
+ v-if="backTo"
121
+ :to="taskDetailRoute(task)"
122
+ class="text-sm text-ultramarine-600 hover:text-ultramarine-700 font-medium"
123
+ >
124
+ View details
125
+ </router-link>
75
126
  <span
76
127
  class="text-xs px-2 py-1 rounded-full font-medium"
77
128
  :class="getStatusColor(task.status)"