1
-/**
2
- * Task Scheduler Component for Settings Modal
3
- * Manages scheduled and ad-hoc tasks through a dedicated settings tab
4
- */
5
-
6
-import { formatDateTime, getUserTimezone } from './time-utils.js';
7
-import { store as chatsStore } from "/components/sidebar/chats/chats-store.js"
8
-import { store as notificationsStore } from "/components/notifications/notification-store.js"
9
-import { store as projectsStore } from "/components/projects/projects-store.js"
10
-
11
-// Ensure the showToast function is available
12
-// if (typeof window.showToast !== 'function') {
13
-// window.showToast = function(message, type = 'info') {
14
-// console.log(`[Toast ${type}]: ${message}`);
15
-// // Create toast element if not already present
16
-// let toastContainer = document.getElementById('toast-container');
17
-// if (!toastContainer) {
18
-// toastContainer = document.createElement('div');
19
-// toastContainer.id = 'toast-container';
20
-// toastContainer.style.position = 'fixed';
21
-// toastContainer.style.bottom = '20px';
22
-// toastContainer.style.right = '20px';
23
-// toastContainer.style.zIndex = '9999';
24
-// document.body.appendChild(toastContainer);
25
-// }
26
-
27
-// // Create the toast
28
-// const toast = document.createElement('div');
29
-// toast.className = `toast toast-${type}`;
30
-// toast.style.padding = '10px 15px';
31
-// toast.style.margin = '5px 0';
32
-// toast.style.backgroundColor = type === 'error' ? '#f44336' :
33
-// type === 'success' ? '#4CAF50' :
34
-// type === 'warning' ? '#ff9800' : '#2196F3';
35
-// toast.style.color = 'white';
36
-// toast.style.borderRadius = '4px';
37
-// toast.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
38
-// toast.style.width = 'auto';
39
-// toast.style.maxWidth = '300px';
40
-// toast.style.wordWrap = 'break-word';
41
-
42
-// toast.innerHTML = message;
43
-
44
-// // Add to container
45
-// toastContainer.appendChild(toast);
46
-
47
-// // Auto remove after 3 seconds
48
-// setTimeout(() => {
49
-// if (toast.parentNode) {
50
-// toast.style.opacity = '0';
51
-// toast.style.transition = 'opacity 0.5s ease';
52
-// setTimeout(() => {
53
-// if (toast.parentNode) {
54
-// toast.parentNode.removeChild(toast);
55
-// }
56
-// }, 500);
57
-// }
58
-// }, 3000);
59
-// };
60
-// }
61
-
62
-// Add this near the top of the scheduler.js file, outside of any function
63
-const showToast = function(message, type = 'info') {
64
- // Use new frontend notification system
65
- switch (type.toLowerCase()) {
66
- case 'error':
67
- return notificationsStore.frontendError(message, "Scheduler", 5);
68
- case 'success':
69
- return notificationsStore.frontendInfo(message, "Scheduler", 3);
70
- case 'warning':
71
- return notificationsStore.frontendWarning(message, "Scheduler", 4);
72
- case 'info':
73
- default:
74
- return notificationsStore.frontendInfo(message, "Scheduler", 3);
75
- }
76
-};
77
-
78
-// Define the full component implementation
79
-const fullComponentImplementation = function() {
80
- return {
81
- tasks: [],
82
- isLoading: true,
83
- selectedTask: null,
84
- expandedTaskId: null,
85
- sortField: 'name',
86
- sortDirection: 'asc',
87
- filterType: 'all', // all, scheduled, adhoc, planned
88
- filterState: 'all', // all, idle, running, disabled, error
89
- pollingInterval: null,
90
- pollingActive: false, // Track if polling is currently active
91
- editingTask: {
92
- name: '',
93
- type: 'scheduled',
94
- state: 'idle',
95
- schedule: {
96
- minute: '*',
97
- hour: '*',
98
- day: '*',
99
- month: '*',
100
- weekday: '*',
101
- timezone: getUserTimezone()
102
- },
103
- token: '',
104
- plan: {
105
- todo: [],
106
- in_progress: null,
107
- done: []
108
- },
109
- system_prompt: '',
110
- prompt: '',
111
- attachments: [],
112
- project: null,
113
- dedicated_context: true,
114
- },
115
- projectOptions: [],
116
- selectedProjectSlug: '',
117
- isCreating: false,
118
- isEditing: false,
119
- showLoadingState: false,
120
- viewMode: 'list', // Controls whether to show list or detail view
121
- selectedTaskForDetail: null, // Task object for detail view
122
- attachmentsText: '',
123
- filteredTasks: [],
124
- hasNoTasks: true, // Add explicit reactive property
125
-
126
- // Initialize the component
127
- init() {
128
- // Initialize component data
129
- this.tasks = [];
130
- this.isLoading = true;
131
- this.hasNoTasks = true; // Add explicit reactive property
132
- this.filterType = 'all';
133
- this.filterState = 'all';
134
- this.sortField = 'name';
135
- this.sortDirection = 'asc';
136
- this.pollingInterval = null;
137
- this.pollingActive = false;
138
-
139
- // Start polling for tasks
140
- this.startPolling();
141
-
142
- // Refresh initial data
143
- this.fetchTasks();
144
-
145
- // Set up event handler for tab selection to ensure view is refreshed when tab becomes visible
146
- document.addEventListener('click', (event) => {
147
- // Check if a tab was clicked
148
- const clickedTab = event.target.closest('.settings-tab');
149
- if (clickedTab && clickedTab.getAttribute('data-tab') === 'scheduler') {
150
- setTimeout(() => {
151
- this.fetchTasks();
152
- }, 100);
153
- }
154
- });
155
-
156
- // Watch for changes to the tasks array to update UI
157
- this.$watch('tasks', (newTasks) => {
158
- this.updateTasksUI();
159
- });
160
-
161
- this.$watch('filterType', () => {
162
- this.updateTasksUI();
163
- });
164
-
165
- this.$watch('filterState', () => {
166
- this.updateTasksUI();
167
- });
168
-
169
- // Set up default configuration
170
- this.viewMode = localStorage.getItem('scheduler_view_mode') || 'list';
171
- this.selectedTask = null;
172
- this.expandedTaskId = null;
173
- this.editingTask = {
174
- name: '',
175
- type: 'scheduled',
176
- state: 'idle',
177
- schedule: {
178
- minute: '*',
179
- hour: '*',
180
- day: '*',
181
- month: '*',
182
- weekday: '*',
183
- timezone: getUserTimezone()
184
- },
185
- token: this.generateRandomToken ? this.generateRandomToken() : '',
186
- plan: {
187
- todo: [],
188
- in_progress: null,
189
- done: []
190
- },
191
- system_prompt: '',
192
- prompt: '',
193
- attachments: [],
194
- project: null,
195
- dedicated_context: true,
196
- };
197
- this.refreshProjectOptions();
198
-
199
- // Initialize Flatpickr for date/time pickers after Alpine is fully initialized
200
- this.$nextTick(() => {
201
- // Wait until DOM is updated
202
- setTimeout(() => {
203
- if (this.isCreating) {
204
- this.initFlatpickr('create');
205
- } else if (this.isEditing) {
206
- this.initFlatpickr('edit');
207
- }
208
- }, 100);
209
- });
210
-
211
- // Cleanup on component destruction
212
- this.$cleanup = () => {
213
- console.log('Cleaning up schedulerSettings component');
214
- this.stopPolling();
215
-
216
- // Clean up any Flatpickr instances
217
- const createInput = document.getElementById('newPlannedTime-create');
218
- if (createInput && createInput._flatpickr) {
219
- createInput._flatpickr.destroy();
220
- }
221
-
222
- const editInput = document.getElementById('newPlannedTime-edit');
223
- if (editInput && editInput._flatpickr) {
224
- editInput._flatpickr.destroy();
225
- }
226
- };
227
- },
228
-
229
- // Start polling for task updates
230
- startPolling() {
231
- // Don't start if already polling
232
- if (this.pollingInterval) {
233
- console.log('Polling already active, not starting again');
234
- return;
235
- }
236
-
237
- console.log('Starting task polling');
238
- this.pollingActive = true;
239
-
240
- // Fetch immediately, then set up interval for every 2 seconds
241
- this.fetchTasks();
242
- this.pollingInterval = setInterval(() => {
243
- if (this.pollingActive) {
244
- this.fetchTasks();
245
- }
246
- }, 2000); // Poll every 2 seconds as requested
247
- },
248
-
249
- // Stop polling when tab is inactive
250
- stopPolling() {
251
- console.log('Stopping task polling');
252
- this.pollingActive = false;
253
-
254
- if (this.pollingInterval) {
255
- clearInterval(this.pollingInterval);
256
- this.pollingInterval = null;
257
- }
258
- },
259
-
260
- // Fetch tasks from API
261
- async fetchTasks() {
262
- // Don't fetch if polling is inactive (prevents race conditions)
263
- if (!this.pollingActive && this.pollingInterval) {
264
- return;
265
- }
266
-
267
- // Don't fetch while creating/editing a task
268
- if (this.isCreating || this.isEditing) {
269
- return;
270
- }
271
-
272
- this.isLoading = true;
273
- try {
274
- const response = await fetchApi('/scheduler_tasks_list', {
275
- method: 'POST',
276
- headers: {
277
- 'Content-Type': 'application/json'
278
- },
279
- body: JSON.stringify({
280
- timezone: getUserTimezone()
281
- })
282
- });
283
-
284
- if (!response.ok) {
285
- throw new Error('Failed to fetch tasks');
286
- }
287
-
288
- const data = await response.json();
289
-
290
- // Check if data.tasks exists and is an array
291
- if (!data || !data.tasks) {
292
- console.error('Invalid response: data.tasks is missing', data);
293
- this.tasks = [];
294
- } else if (!Array.isArray(data.tasks)) {
295
- console.error('Invalid response: data.tasks is not an array', data.tasks);
296
- this.tasks = [];
297
- } else {
298
- // Verify each task has necessary properties
299
- const validTasks = data.tasks.filter(task => {
300
- if (!task || typeof task !== 'object') {
301
- console.error('Invalid task (not an object):', task);
302
- return false;
303
- }
304
- if (!task.uuid) {
305
- console.error('Task missing uuid:', task);
306
- return false;
307
- }
308
- if (!task.name) {
309
- console.error('Task missing name:', task);
310
- return false;
311
- }
312
- if (!task.type) {
313
- console.error('Task missing type:', task);
314
- return false;
315
- }
316
- return true;
317
- });
318
-
319
- if (validTasks.length !== data.tasks.length) {
320
- console.warn(`Filtered out ${data.tasks.length - validTasks.length} invalid tasks`);
321
- }
322
-
323
- this.tasks = validTasks;
324
-
325
- // Update UI using the shared function
326
- this.updateTasksUI();
327
- }
328
- } catch (error) {
329
- console.error('Error fetching tasks:', error);
330
- // Only show toast for errors on manual refresh, not during polling
331
- if (!this.pollingInterval) {
332
- showToast('Failed to fetch tasks: ' + error.message, 'error');
333
- }
334
- // Reset tasks to empty array on error
335
- this.tasks = [];
336
- } finally {
337
- this.isLoading = false;
338
- }
339
- },
340
-
341
- // Change sort field/direction
342
- changeSort(field) {
343
- if (this.sortField === field) {
344
- // Toggle direction if already sorting by this field
345
- this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
346
- } else {
347
- // Set new sort field and default to ascending
348
- this.sortField = field;
349
- this.sortDirection = 'asc';
350
- }
351
- },
352
-
353
- // Toggle expanded task row
354
- toggleTaskExpand(taskId) {
355
- if (this.expandedTaskId === taskId) {
356
- this.expandedTaskId = null;
357
- } else {
358
- this.expandedTaskId = taskId;
359
- }
360
- },
361
-
362
- // Show task detail view
363
- showTaskDetail(taskId) {
364
- const task = this.tasks.find(t => t.uuid === taskId);
365
- if (!task) {
366
- showToast('Task not found', 'error');
367
- return;
368
- }
369
-
370
- // Create a copy of the task to avoid modifying the original
371
- this.selectedTaskForDetail = JSON.parse(JSON.stringify(task));
372
-
373
- // Ensure attachments is always an array
374
- if (!this.selectedTaskForDetail.attachments) {
375
- this.selectedTaskForDetail.attachments = [];
376
- }
377
-
378
- this.viewMode = 'detail';
379
- },
380
-
381
- // Close detail view and return to list
382
- closeTaskDetail() {
383
- this.selectedTaskForDetail = null;
384
- this.viewMode = 'list';
385
- },
386
-
387
- // Format date for display
388
- formatDate(dateString) {
389
- if (!dateString) return 'Never';
390
- return formatDateTime(dateString, 'full');
391
- },
392
-
393
- // Format plan for display
394
- formatPlan(task) {
395
- if (!task || !task.plan) return 'No plan';
396
-
397
- const todoCount = Array.isArray(task.plan.todo) ? task.plan.todo.length : 0;
398
- const inProgress = task.plan.in_progress ? 'Yes' : 'No';
399
- const doneCount = Array.isArray(task.plan.done) ? task.plan.done.length : 0;
400
-
401
- let nextRun = '';
402
- if (Array.isArray(task.plan.todo) && task.plan.todo.length > 0) {
403
- try {
404
- const nextTime = new Date(task.plan.todo[0]);
405
-
406
- // Verify it's a valid date before formatting
407
- if (!isNaN(nextTime.getTime())) {
408
- nextRun = formatDateTime(nextTime, 'short');
409
- } else {
410
- nextRun = 'Invalid date';
411
- console.warn(`Invalid date format in plan.todo[0]: ${task.plan.todo[0]}`);
412
- }
413
- } catch (error) {
414
- console.error(`Error formatting next run time: ${error.message}`);
415
- nextRun = 'Error';
416
- }
417
- } else {
418
- nextRun = 'None';
419
- }
420
-
421
- return `Next: ${nextRun}\nTodo: ${todoCount}\nIn Progress: ${inProgress}\nDone: ${doneCount}`;
422
- },
423
-
424
- // Format schedule for display
425
- formatSchedule(task) {
426
- if (!task.schedule) return 'None';
427
-
428
- let schedule = '';
429
- if (typeof task.schedule === 'string') {
430
- schedule = task.schedule;
431
- } else if (typeof task.schedule === 'object') {
432
- // Display only the cron parts, not the timezone
433
- schedule = `${task.schedule.minute || '*'} ${task.schedule.hour || '*'} ${task.schedule.day || '*'} ${task.schedule.month || '*'} ${task.schedule.weekday || '*'}`;
434
- }
435
-
436
- return schedule;
437
- },
438
-
439
- // Get CSS class for state badge
440
- getStateBadgeClass(state) {
441
- switch (state) {
442
- case 'idle': return 'scheduler-status-idle';
443
- case 'running': return 'scheduler-status-running';
444
- case 'disabled': return 'scheduler-status-disabled';
445
- case 'error': return 'scheduler-status-error';
446
- default: return '';
447
- }
448
- },
449
-
450
- deriveActiveProject() {
451
- const selected = chatsStore?.selectedContext || null;
452
- if (!selected || !selected.project) {
453
- return null;
454
- }
455
-
456
- const project = selected.project;
457
- return {
458
- name: project.name || null,
459
- title: project.title || project.name || null,
460
- color: project.color || '',
461
- };
462
- },
463
-
464
- formatProjectName(project) {
465
- if (!project) {
466
- return 'No Project';
467
- }
468
- const title = project.title || project.name;
469
- return title || 'No Project';
470
- },
471
-
472
- formatProjectLabel(project) {
473
- return `Project: ${this.formatProjectName(project)}`;
474
- },
475
-
476
- async refreshProjectOptions() {
477
- try {
478
- if (!Array.isArray(projectsStore.projectList) || !projectsStore.projectList.length) {
479
- if (typeof projectsStore.loadProjectsList === 'function') {
480
- await projectsStore.loadProjectsList();
481
- }
482
- }
483
- } catch (error) {
484
- console.warn('schedulerSettings: failed to load project list', error);
485
- }
486
-
487
- const list = Array.isArray(projectsStore.projectList) ? projectsStore.projectList : [];
488
- this.projectOptions = list.map((proj) => ({
489
- name: proj.name,
490
- title: proj.title || proj.name,
491
- color: proj.color || '',
492
- }));
493
- },
494
-
495
- onProjectSelect(slug) {
496
- this.selectedProjectSlug = slug || '';
497
- if (!slug) {
498
- this.editingTask.project = null;
499
- return;
500
- }
501
-
502
- const option = this.projectOptions.find((item) => item.name === slug);
503
- if (option) {
504
- this.editingTask.project = { ...option };
505
- } else {
506
- this.editingTask.project = {
507
- name: slug,
508
- title: slug,
509
- color: '',
510
- };
511
- }
512
- },
513
-
514
- extractTaskProject(task) {
515
- if (!task) {
516
- return null;
517
- }
518
-
519
- const slug = task.project_name || null;
520
- const project = task.project || {};
521
- const title = project.name || slug;
522
- const color = task.project_color || project.color || '';
523
-
524
- if (!slug && !title) {
525
- return null;
526
- }
527
-
528
- return {
529
- name: slug,
530
- title: title || slug,
531
- color: color,
532
- };
533
- },
534
-
535
- formatTaskProject(task) {
536
- return this.formatProjectName(this.extractTaskProject(task));
537
- },
538
-
539
- // Create a new task
540
- async startCreateTask() {
541
- this.isCreating = true;
542
- this.isEditing = false;
543
- document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'creating');
544
- await this.refreshProjectOptions();
545
- const activeProject = this.deriveActiveProject();
546
- let initialProject = activeProject ? { ...activeProject } : null;
547
- if (!initialProject && this.projectOptions.length > 0) {
548
- initialProject = { ...this.projectOptions[0] };
549
- }
550
-
551
- this.editingTask = {
552
- name: '',
553
- type: 'scheduled', // Default to scheduled
554
- state: 'idle', // Initialize with idle state
555
- schedule: {
556
- minute: '*',
557
- hour: '*',
558
- day: '*',
559
- month: '*',
560
- weekday: '*',
561
- timezone: getUserTimezone()
562
- },
563
- token: this.generateRandomToken(), // Generate token even for scheduled tasks to prevent undefined errors
564
- plan: { // Initialize plan for all task types to prevent undefined errors
565
- todo: [],
566
- in_progress: null,
567
- done: []
568
- },
569
- system_prompt: '',
570
- prompt: '',
571
- attachments: [], // Always initialize as an empty array
572
- project: initialProject,
573
- dedicated_context: true,
574
- };
575
- this.selectedProjectSlug = initialProject && initialProject.name ? initialProject.name : '';
576
-
577
- // Set up Flatpickr after the component is visible
578
- this.$nextTick(() => {
579
- this.initFlatpickr('create');
580
- });
581
- },
582
-
583
- // Edit an existing task
584
- async startEditTask(taskId) {
585
- const task = this.tasks.find(t => t.uuid === taskId);
586
- if (!task) {
587
- showToast('Task not found', 'error');
588
- return;
589
- }
590
-
591
- this.isCreating = false;
592
- this.isEditing = true;
593
- document.querySelector('[x-data="schedulerSettings"]')?.setAttribute('data-editing-state', 'editing');
594
-
595
- // Create a deep copy to avoid modifying the original
596
- this.editingTask = JSON.parse(JSON.stringify(task));
597
- const projectSlug = task.project_name || null;
598
- const projectDisplay = (task.project && task.project.name) || projectSlug;
599
- const projectColor = task.project_color || (task.project ? task.project.color : '') || '';
600
- this.editingTask.project = projectSlug || projectDisplay ? {
601
- name: projectSlug,
602
- title: projectDisplay,
603
- color: projectColor,
604
- } : null;
605
- this.editingTask.dedicated_context = !!task.dedicated_context;
606
- this.selectedProjectSlug = this.editingTask.project && this.editingTask.project.name ? this.editingTask.project.name : '';
607
-
608
- // Debug log
609
- console.log('Task data for editing:', task);
610
- console.log('Attachments from task:', task.attachments);
611
-
612
- // Ensure state is set with a default if missing
613
- if (!this.editingTask.state) this.editingTask.state = 'idle';
614
-
615
- // Always initialize schedule to prevent UI errors
616
- // All task types need this structure for the form to work properly
617
- if (!this.editingTask.schedule || typeof this.editingTask.schedule === 'string') {
618
- let scheduleObj = {
619
- minute: '*',
620
- hour: '*',
621
- day: '*',
622
- month: '*',
623
- weekday: '*',
624
- timezone: getUserTimezone()
625
- };
626
-
627
- // If it's a string, parse it
628
- if (typeof this.editingTask.schedule === 'string') {
629
- const parts = this.editingTask.schedule.split(' ');
630
- if (parts.length >= 5) {
631
- scheduleObj.minute = parts[0] || '*';
632
- scheduleObj.hour = parts[1] || '*';
633
- scheduleObj.day = parts[2] || '*';
634
- scheduleObj.month = parts[3] || '*';
635
- scheduleObj.weekday = parts[4] || '*';
636
- }
637
- }
638
-
639
- this.editingTask.schedule = scheduleObj;
640
- } else {
641
- // Ensure timezone exists in the schedule
642
- if (!this.editingTask.schedule.timezone) {
643
- this.editingTask.schedule.timezone = getUserTimezone();
644
- }
645
- }
646
-
647
- // Ensure attachments is always an array
648
- if (!this.editingTask.attachments) {
649
- this.editingTask.attachments = [];
650
- } else if (typeof this.editingTask.attachments === 'string') {
651
- // Handle case where attachments might be stored as a string
652
- this.editingTask.attachments = this.editingTask.attachments
653
- .split('\n')
654
- .map(line => line.trim())
655
- .filter(line => line.length > 0);
656
- } else if (!Array.isArray(this.editingTask.attachments)) {
657
- // If not an array or string, set to empty array
658
- this.editingTask.attachments = [];
659
- }
660
-
661
- // Ensure appropriate properties are initialized based on task type
662
- if (this.editingTask.type === 'scheduled') {
663
- // Initialize token for scheduled tasks to prevent undefined errors if UI accesses it
664
- if (!this.editingTask.token) {
665
- this.editingTask.token = '';
666
- }
667
-
668
- // Initialize plan stub for scheduled tasks to prevent undefined errors
669
- if (!this.editingTask.plan) {
670
- this.editingTask.plan = {
671
- todo: [],
672
- in_progress: null,
673
- done: []
674
- };
675
- }
676
- } else if (this.editingTask.type === 'adhoc') {
677
- // Initialize token if it doesn't exist
678
- if (!this.editingTask.token) {
679
- this.editingTask.token = this.generateRandomToken();
680
- console.log('Generated new token for adhoc task:', this.editingTask.token);
681
- }
682
-
683
- console.log('Setting token for adhoc task:', this.editingTask.token);
684
-
685
- // Initialize plan stub for adhoc tasks to prevent undefined errors
686
- if (!this.editingTask.plan) {
687
- this.editingTask.plan = {
688
- todo: [],
689
- in_progress: null,
690
- done: []
691
- };
692
- }
693
- } else if (this.editingTask.type === 'planned') {
694
- // Initialize plan if it doesn't exist
695
- if (!this.editingTask.plan) {
696
- this.editingTask.plan = {
697
- todo: [],
698
- in_progress: null,
699
- done: []
700
- };
701
- }
702
-
703
- // Ensure todo is an array
704
- if (!Array.isArray(this.editingTask.plan.todo)) {
705
- this.editingTask.plan.todo = [];
706
- }
707
-
708
- // Initialize token to prevent undefined errors
709
- if (!this.editingTask.token) {
710
- this.editingTask.token = '';
711
- }
712
- }
713
-
714
- // Set up Flatpickr after the component is visible and task data is loaded
715
- this.$nextTick(() => {
716
- this.initFlatpickr('edit');
717
- });
718
- },
719
-
720
- // Cancel editing
721
- cancelEdit() {
722
- // Clean up Flatpickr instances
723
- const destroyFlatpickr = (inputId) => {
724
- const input = document.getElementById(inputId);
725
- if (input && input._flatpickr) {
726
- console.log(`Destroying Flatpickr instance for ${inputId}`);
727
- input._flatpickr.destroy();
728
-
729
- // Also remove any wrapper elements that might have been created
730
- const wrapper = input.closest('.scheduler-flatpickr-wrapper');
731
- if (wrapper && wrapper.parentNode) {
732
- // Move the input back to its original position
733
- wrapper.parentNode.insertBefore(input, wrapper);
734
- // Remove the wrapper
735
- wrapper.parentNode.removeChild(wrapper);
736
- }
737
-
738
- // Remove any added classes
739
- input.classList.remove('scheduler-flatpickr-input');
740
- }
741
- };
742
-
743
- if (this.isCreating) {
744
- destroyFlatpickr('newPlannedTime-create');
745
- } else if (this.isEditing) {
746
- destroyFlatpickr('newPlannedTime-edit');
747
- }
748
-
749
- // Reset to initial state but keep default values to prevent errors
750
- this.editingTask = {
751
- name: '',
752
- type: 'scheduled',
753
- state: 'idle', // Initialize with idle state
754
- schedule: {
755
- minute: '*',
756
- hour: '*',
757
- day: '*',
758
- month: '*',
759
- weekday: '*',
760
- timezone: getUserTimezone()
761
- },
762
- token: '',
763
- plan: { // Initialize plan for planned tasks
764
- todo: [],
765
- in_progress: null,
766
- done: []
767
- },
768
- system_prompt: '',
769
- prompt: '',
770
- attachments: [], // Always initialize as an empty array
771
- project: null,
772
- dedicated_context: true,
773
- };
774
- this.selectedProjectSlug = '';
775
- this.isCreating = false;
776
- this.isEditing = false;
777
- document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
778
- },
779
-
780
- // Save task (create new or update existing)
781
- async saveTask() {
782
- // Validate task data
783
- if (!this.editingTask.name.trim() || !this.editingTask.prompt.trim()) {
784
- // showToast('Task name and prompt are required', 'error');
785
- alert('Task name and prompt are required');
786
- return;
787
- }
788
-
789
- try {
790
- let apiEndpoint, taskData;
791
-
792
- // Prepare task data
793
- taskData = {
794
- name: this.editingTask.name,
795
- system_prompt: this.editingTask.system_prompt || '',
796
- prompt: this.editingTask.prompt || '',
797
- state: this.editingTask.state || 'idle', // Include state in task data
798
- timezone: getUserTimezone()
799
- };
800
-
801
- if (this.isCreating && this.editingTask.project) {
802
- if (this.editingTask.project.name) {
803
- taskData.project_name = this.editingTask.project.name;
804
- }
805
- if (this.editingTask.project.color) {
806
- taskData.project_color = this.editingTask.project.color;
807
- }
808
- }
809
-
810
- // Process attachments - now always stored as array
811
- taskData.attachments = Array.isArray(this.editingTask.attachments)
812
- ? this.editingTask.attachments
813
- .map(line => typeof line === 'string' ? line.trim() : line)
814
- .filter(line => line && line.trim().length > 0)
815
- : [];
816
-
817
- // Handle task type specific data
818
- if (this.editingTask.type === 'scheduled') {
819
- // Ensure schedule is properly formatted as an object
820
- if (typeof this.editingTask.schedule === 'string') {
821
- // Parse string schedule into object
822
- const parts = this.editingTask.schedule.split(' ');
823
- taskData.schedule = {
824
- minute: parts[0] || '*',
825
- hour: parts[1] || '*',
826
- day: parts[2] || '*',
827
- month: parts[3] || '*',
828
- weekday: parts[4] || '*',
829
- timezone: getUserTimezone() // Add timezone to schedule object
830
- };
831
- } else {
832
- // Use object schedule directly but ensure timezone is included
833
- taskData.schedule = {
834
- ...this.editingTask.schedule,
835
- timezone: this.editingTask.schedule.timezone || getUserTimezone()
836
- };
837
- }
838
- // Don't send token or plan for scheduled tasks
839
- delete taskData.token;
840
- delete taskData.plan;
841
- } else if (this.editingTask.type === 'adhoc') {
842
- // Ad-hoc task with token
843
- // Ensure token is a non-empty string, generate a new one if needed
844
- if (!this.editingTask.token) {
845
- this.editingTask.token = this.generateRandomToken();
846
- console.log('Generated new token for adhoc task:', this.editingTask.token);
847
- }
848
-
849
- console.log('Setting token in taskData:', this.editingTask.token);
850
- taskData.token = this.editingTask.token;
851
-
852
- // Don't send schedule or plan for adhoc tasks
853
- delete taskData.schedule;
854
- delete taskData.plan;
855
- } else if (this.editingTask.type === 'planned') {
856
- // Planned task with plan
857
- // Make sure plan exists and has required properties
858
- if (!this.editingTask.plan) {
859
- this.editingTask.plan = {
860
- todo: [],
861
- in_progress: null,
862
- done: []
863
- };
864
- }
865
-
866
- // Ensure todo and done are arrays
867
- if (!Array.isArray(this.editingTask.plan.todo)) {
868
- this.editingTask.plan.todo = [];
869
- }
870
-
871
- if (!Array.isArray(this.editingTask.plan.done)) {
872
- this.editingTask.plan.done = [];
873
- }
874
-
875
- // Validate each date in the todo list to ensure it's a valid ISO string
876
- const validatedTodo = [];
877
- for (const dateStr of this.editingTask.plan.todo) {
878
- try {
879
- const date = new Date(dateStr);
880
- if (!isNaN(date.getTime())) {
881
- validatedTodo.push(date.toISOString());
882
- } else {
883
- console.warn(`Skipping invalid date in todo list: ${dateStr}`);
884
- }
885
- } catch (error) {
886
- console.warn(`Error processing date: ${error.message}`);
887
- }
888
- }
889
-
890
- // Replace with validated list
891
- this.editingTask.plan.todo = validatedTodo;
892
-
893
- // Sort the todo items by date (earliest first)
894
- this.editingTask.plan.todo.sort();
895
-
896
- // Set the plan in taskData
897
- taskData.plan = {
898
- todo: this.editingTask.plan.todo,
899
- in_progress: this.editingTask.plan.in_progress,
900
- done: this.editingTask.plan.done || []
901
- };
902
-
903
- // Log the plan data for debugging
904
- console.log('Planned task plan data:', JSON.stringify(taskData.plan, null, 2));
905
-
906
- // Don't send schedule or token for planned tasks
907
- delete taskData.schedule;
908
- delete taskData.token;
909
- }
910
-
911
- // Determine if creating or updating
912
- if (this.isCreating) {
913
- apiEndpoint = '/scheduler_task_create';
914
- } else {
915
- apiEndpoint = '/scheduler_task_update';
916
- taskData.task_id = this.editingTask.uuid;
917
- }
918
-
919
- // Debug: Log the final task data being sent
920
- console.log('Final task data being sent to API:', JSON.stringify(taskData, null, 2));
921
-
922
- // Make API request
923
- const response = await fetchApi(apiEndpoint, {
924
- method: 'POST',
925
- headers: {
926
- 'Content-Type': 'application/json'
927
- },
928
- body: JSON.stringify(taskData)
929
- });
930
-
931
- if (!response.ok) {
932
- const errorData = await response.json();
933
- throw new Error(errorData.error || 'Failed to save task');
934
- }
935
-
936
- // Parse response data to get the created/updated task
937
- const responseData = await response.json();
938
-
939
- // Show success message
940
- showToast(this.isCreating ? 'Task created successfully' : 'Task updated successfully', 'success');
941
-
942
- // Immediately update the UI if the response includes the task
943
- if (responseData && responseData.task) {
944
- console.log('Task received in response:', responseData.task);
945
-
946
- // Update the tasks array
947
- if (this.isCreating) {
948
- // For new tasks, add to the array
949
- this.tasks = [...this.tasks, responseData.task];
950
- } else {
951
- // For updated tasks, replace the existing one
952
- this.tasks = this.tasks.map(t =>
953
- t.uuid === responseData.task.uuid ? responseData.task : t
954
- );
955
- }
956
-
957
- // Update UI using the shared function
958
- this.updateTasksUI();
959
- } else {
960
- // Fallback to fetching tasks if no task in response
961
- await this.fetchTasks();
962
- }
963
-
964
- // Clean up Flatpickr instances
965
- const destroyFlatpickr = (inputId) => {
966
- const input = document.getElementById(inputId);
967
- if (input && input._flatpickr) {
968
- input._flatpickr.destroy();
969
- }
970
- };
971
-
972
- if (this.isCreating) {
973
- destroyFlatpickr('newPlannedTime-create');
974
- } else if (this.isEditing) {
975
- destroyFlatpickr('newPlannedTime-edit');
976
- }
977
-
978
- // Reset task data and form state
979
- this.editingTask = {
980
- name: '',
981
- type: 'scheduled',
982
- state: 'idle',
983
- schedule: {
984
- minute: '*',
985
- hour: '*',
986
- day: '*',
987
- month: '*',
988
- weekday: '*',
989
- timezone: getUserTimezone()
990
- },
991
- token: '',
992
- plan: {
993
- todo: [],
994
- in_progress: null,
995
- done: []
996
- },
997
- system_prompt: '',
998
- prompt: '',
999
- attachments: [],
1000
- project: null,
1001
- dedicated_context: true,
1002
- };
1003
- this.isCreating = false;
1004
- this.isEditing = false;
1005
- document.querySelector('[x-data="schedulerSettings"]')?.removeAttribute('data-editing-state');
1006
- } catch (error) {
1007
- console.error('Error saving task:', error);
1008
- showToast('Failed to save task: ' + error.message, 'error');
1009
- }
1010
- },
1011
-
1012
- // Run a task
1013
- async runTask(taskId) {
1014
- try {
1015
- const response = await fetchApi('/scheduler_task_run', {
1016
- method: 'POST',
1017
- headers: {
1018
- 'Content-Type': 'application/json'
1019
- },
1020
- body: JSON.stringify({
1021
- task_id: taskId,
1022
- timezone: getUserTimezone()
1023
- })
1024
- });
1025
-
1026
- const data = await response.json();
1027
-
1028
- if (!response.ok) {
1029
- throw new Error(data?.error || 'Failed to run task');
1030
- }
1031
-
1032
- const toastMessage = data.warning || data.message || 'Task started successfully';
1033
- const toastType = data.warning ? 'warning' : 'success';
1034
- showToast(toastMessage, toastType);
1035
-
1036
- // Refresh task list
1037
- this.fetchTasks();
1038
- } catch (error) {
1039
- console.error('Error running task:', error);
1040
- showToast('Failed to run task: ' + error.message, 'error');
1041
- }
1042
- },
1043
-
1044
- // Reset a task's state
1045
- async resetTaskState(taskId) {
1046
- try {
1047
- const task = this.tasks.find(t => t.uuid === taskId);
1048
- if (!task) {
1049
- showToast('Task not found', 'error');
1050
- return;
1051
- }
1052
-
1053
- // Check if task is already in idle state
1054
- if (task.state === 'idle') {
1055
- showToast('Task is already in idle state', 'info');
1056
- return;
1057
- }
1058
-
1059
- this.showLoadingState = true;
1060
-
1061
- // Call API to update the task state
1062
- const response = await fetchApi('/scheduler_task_update', {
1063
- method: 'POST',
1064
- headers: {
1065
- 'Content-Type': 'application/json'
1066
- },
1067
- body: JSON.stringify({
1068
- task_id: taskId,
1069
- state: 'idle', // Always reset to idle state
1070
- timezone: getUserTimezone()
1071
- })
1072
- });
1073
-
1074
- if (!response.ok) {
1075
- const errorData = await response.json();
1076
- throw new Error(errorData.error || 'Failed to reset task state');
1077
- }
1078
-
1079
- showToast('Task state reset to idle', 'success');
1080
-
1081
- // Refresh task list
1082
- await this.fetchTasks();
1083
- this.showLoadingState = false;
1084
- } catch (error) {
1085
- console.error('Error resetting task state:', error);
1086
- showToast('Failed to reset task state: ' + error.message, 'error');
1087
- this.showLoadingState = false;
1088
- }
1089
- },
1090
-
1091
- // Delete a task
1092
- async deleteTask(taskId) {
1093
- // Confirm deletion
1094
- if (!confirm('Are you sure you want to delete this task? This action cannot be undone.')) {
1095
- return;
1096
- }
1097
-
1098
- try {
1099
-
1100
- // if we delete selected context, switch to another first
1101
- await chatsStore.switchFromContext(taskId);
1102
-
1103
- const response = await fetchApi('/scheduler_task_delete', {
1104
- method: 'POST',
1105
- headers: {
1106
- 'Content-Type': 'application/json'
1107
- },
1108
- body: JSON.stringify({
1109
- task_id: taskId,
1110
- timezone: getUserTimezone()
1111
- })
1112
- });
1113
-
1114
- if (!response.ok) {
1115
- const errorData = await response.json();
1116
- throw new Error(errorData.error || 'Failed to delete task');
1117
- }
1118
-
1119
- showToast('Task deleted successfully', 'success');
1120
-
1121
- // If we were viewing the detail of the deleted task, close the detail view
1122
- if (this.selectedTaskForDetail && this.selectedTaskForDetail.uuid === taskId) {
1123
- this.closeTaskDetail();
1124
- }
1125
-
1126
- // Immediately update UI without waiting for polling
1127
- this.tasks = this.tasks.filter(t => t.uuid !== taskId);
1128
-
1129
- // Update UI using the shared function
1130
- this.updateTasksUI();
1131
- } catch (error) {
1132
- console.error('Error deleting task:', error);
1133
- showToast('Failed to delete task: ' + error.message, 'error');
1134
- }
1135
- },
1136
-
1137
- // Initialize datetime input with default value (30 minutes from now)
1138
- initDateTimeInput(event) {
1139
- if (!event.target.value) {
1140
- const now = new Date();
1141
- now.setMinutes(now.getMinutes() + 30);
1142
-
1143
- // Format as YYYY-MM-DDThh:mm
1144
- const year = now.getFullYear();
1145
- const month = String(now.getMonth() + 1).padStart(2, '0');
1146
- const day = String(now.getDate()).padStart(2, '0');
1147
- const hours = String(now.getHours()).padStart(2, '0');
1148
- const minutes = String(now.getMinutes()).padStart(2, '0');
1149
-
1150
- event.target.value = `${year}-${month}-${day}T${hours}:${minutes}`;
1151
-
1152
- // If using Flatpickr, update it as well
1153
- if (event.target._flatpickr) {
1154
- event.target._flatpickr.setDate(event.target.value);
1155
- }
1156
- }
1157
- },
1158
-
1159
- // Generate a random token for ad-hoc tasks
1160
- generateRandomToken() {
1161
- const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
1162
- let token = '';
1163
- for (let i = 0; i < 16; i++) {
1164
- token += characters.charAt(Math.floor(Math.random() * characters.length));
1165
- }
1166
- return token;
1167
- },
1168
-
1169
- // Getter for filtered tasks
1170
- get filteredTasks() {
1171
- // Make sure we have tasks to filter
1172
- if (!Array.isArray(this.tasks)) {
1173
- console.warn('Tasks is not an array:', this.tasks);
1174
- return [];
1175
- }
1176
-
1177
- let filtered = [...this.tasks];
1178
-
1179
- // Apply type filter with case-insensitive comparison
1180
- if (this.filterType && this.filterType !== 'all') {
1181
- filtered = filtered.filter(task => {
1182
- if (!task.type) return false;
1183
- return String(task.type).toLowerCase() === this.filterType.toLowerCase();
1184
- });
1185
- }
1186
-
1187
- // Apply state filter with case-insensitive comparison
1188
- if (this.filterState && this.filterState !== 'all') {
1189
- filtered = filtered.filter(task => {
1190
- if (!task.state) return false;
1191
- return String(task.state).toLowerCase() === this.filterState.toLowerCase();
1192
- });
1193
- }
1194
-
1195
- // Sort the filtered tasks
1196
- return this.sortTasks(filtered);
1197
- },
1198
-
1199
- // Sort the tasks based on sort field and direction
1200
- sortTasks(tasks) {
1201
- if (!Array.isArray(tasks) || tasks.length === 0) {
1202
- return tasks;
1203
- }
1204
-
1205
- return [...tasks].sort((a, b) => {
1206
- if (!this.sortField) return 0;
1207
-
1208
- const fieldA = a[this.sortField];
1209
- const fieldB = b[this.sortField];
1210
-
1211
- // Handle cases where fields might be undefined
1212
- if (fieldA === undefined && fieldB === undefined) return 0;
1213
- if (fieldA === undefined) return 1;
1214
- if (fieldB === undefined) return -1;
1215
-
1216
- // For dates, convert to timestamps
1217
- if (this.sortField === 'createdAt' || this.sortField === 'updatedAt') {
1218
- const dateA = new Date(fieldA).getTime();
1219
- const dateB = new Date(fieldB).getTime();
1220
- return this.sortDirection === 'asc' ? dateA - dateB : dateB - dateA;
1221
- }
1222
-
1223
- // For string comparisons
1224
- if (typeof fieldA === 'string' && typeof fieldB === 'string') {
1225
- return this.sortDirection === 'asc'
1226
- ? fieldA.localeCompare(fieldB)
1227
- : fieldB.localeCompare(fieldA);
1228
- }
1229
-
1230
- // For numerical comparisons
1231
- return this.sortDirection === 'asc' ? fieldA - fieldB : fieldB - fieldA;
1232
- });
1233
- },
1234
-
1235
- // Computed property for attachments text representation
1236
- get attachmentsText() {
1237
- // Ensure we always have an array to work with
1238
- const attachments = Array.isArray(this.editingTask.attachments)
1239
- ? this.editingTask.attachments
1240
- : [];
1241
-
1242
- // Join array items with newlines
1243
- return attachments.join('\n');
1244
- },
1245
-
1246
- // Setter for attachments text - preserves empty lines during editing
1247
- set attachmentsText(value) {
1248
- if (typeof value === 'string') {
1249
- // Just split by newlines without filtering to preserve editing experience
1250
- this.editingTask.attachments = value.split('\n');
1251
- } else {
1252
- // Fallback to empty array if not a string
1253
- this.editingTask.attachments = [];
1254
- }
1255
- },
1256
-
1257
- // Debug method to test filtering logic
1258
- testFiltering() {
1259
- console.group('SchedulerSettings Debug: Filter Test');
1260
- console.log('Current Filter Settings:');
1261
- console.log('- Filter Type:', this.filterType);
1262
- console.log('- Filter State:', this.filterState);
1263
- console.log('- Sort Field:', this.sortField);
1264
- console.log('- Sort Direction:', this.sortDirection);
1265
-
1266
- // Check if tasks is an array
1267
- if (!Array.isArray(this.tasks)) {
1268
- console.error('ERROR: this.tasks is not an array!', this.tasks);
1269
- console.groupEnd();
1270
- return;
1271
- }
1272
-
1273
- console.log(`Raw Tasks (${this.tasks.length}):`, this.tasks);
1274
-
1275
- // Test filtering by type
1276
- console.group('Filter by Type Test');
1277
- ['all', 'adhoc', 'scheduled', 'recurring'].forEach(type => {
1278
- const filtered = this.tasks.filter(task =>
1279
- type === 'all' ||
1280
- (task.type && String(task.type).toLowerCase() === type)
1281
- );
1282
- console.log(`Type "${type}": ${filtered.length} tasks`, filtered);
1283
- });
1284
- console.groupEnd();
1285
-
1286
- // Test filtering by state
1287
- console.group('Filter by State Test');
1288
- ['all', 'idle', 'running', 'completed', 'failed'].forEach(state => {
1289
- const filtered = this.tasks.filter(task =>
1290
- state === 'all' ||
1291
- (task.state && String(task.state).toLowerCase() === state)
1292
- );
1293
- console.log(`State "${state}": ${filtered.length} tasks`, filtered);
1294
- });
1295
- console.groupEnd();
1296
-
1297
- // Show current filtered tasks
1298
- console.log('Current Filtered Tasks:', this.filteredTasks);
1299
-
1300
- console.groupEnd();
1301
- },
1302
-
1303
- // New comprehensive debug method
1304
- debugTasks() {
1305
- console.group('SchedulerSettings Comprehensive Debug');
1306
-
1307
- // Component state
1308
- console.log('Component State:');
1309
- console.log({
1310
- filterType: this.filterType,
1311
- filterState: this.filterState,
1312
- sortField: this.sortField,
1313
- sortDirection: this.sortDirection,
1314
- isLoading: this.isLoading,
1315
- isEditing: this.isEditing,
1316
- isCreating: this.isCreating,
1317
- viewMode: this.viewMode
1318
- });
1319
-
1320
- // Tasks validation
1321
- if (!this.tasks) {
1322
- console.error('ERROR: this.tasks is undefined or null!');
1323
- console.groupEnd();
1324
- return;
1325
- }
1326
-
1327
- if (!Array.isArray(this.tasks)) {
1328
- console.error('ERROR: this.tasks is not an array!', typeof this.tasks, this.tasks);
1329
- console.groupEnd();
1330
- return;
1331
- }
1332
-
1333
- // Raw tasks
1334
- console.group('Raw Tasks');
1335
- console.log(`Count: ${this.tasks.length}`);
1336
- if (this.tasks.length > 0) {
1337
- console.table(this.tasks.map(t => ({
1338
- uuid: t.uuid,
1339
- name: t.name,
1340
- type: t.type,
1341
- state: t.state
1342
- })));
1343
-
1344
- // Inspect first task in detail
1345
- console.log('First Task Structure:', JSON.stringify(this.tasks[0], null, 2));
1346
- } else {
1347
- console.log('No tasks available');
1348
- }
1349
- console.groupEnd();
1350
-
1351
- // Filtered tasks
1352
- console.group('Filtered Tasks');
1353
- const filteredTasks = this.filteredTasks;
1354
- console.log(`Count: ${filteredTasks.length}`);
1355
- if (filteredTasks.length > 0) {
1356
- console.table(filteredTasks.map(t => ({
1357
- uuid: t.uuid,
1358
- name: t.name,
1359
- type: t.type,
1360
- state: t.state
1361
- })));
1362
- } else {
1363
- console.log('No filtered tasks');
1364
- }
1365
- console.groupEnd();
1366
-
1367
- // Check for potential issues
1368
- console.group('Potential Issues');
1369
-
1370
- // Check for case mismatches
1371
- if (this.tasks.length > 0 && filteredTasks.length === 0) {
1372
- console.warn('Filter seems to exclude all tasks. Checking why:');
1373
-
1374
- // Check type values
1375
- const uniqueTypes = [...new Set(this.tasks.map(t => t.type))];
1376
- console.log('Unique task types in data:', uniqueTypes);
1377
-
1378
- // Check state values
1379
- const uniqueStates = [...new Set(this.tasks.map(t => t.state))];
1380
- console.log('Unique task states in data:', uniqueStates);
1381
-
1382
- // Check for exact mismatches
1383
- if (this.filterType !== 'all') {
1384
- const typeMatch = this.tasks.some(t =>
1385
- t.type && String(t.type).toLowerCase() === this.filterType.toLowerCase()
1386
- );
1387
- console.log(`Type "${this.filterType}" matches found:`, typeMatch);
1388
- }
1389
-
1390
- if (this.filterState !== 'all') {
1391
- const stateMatch = this.tasks.some(t =>
1392
- t.state && String(t.state).toLowerCase() === this.filterState.toLowerCase()
1393
- );
1394
- console.log(`State "${this.filterState}" matches found:`, stateMatch);
1395
- }
1396
- }
1397
-
1398
- // Check for undefined or null values
1399
- const hasUndefinedType = this.tasks.some(t => t.type === undefined || t.type === null);
1400
- const hasUndefinedState = this.tasks.some(t => t.state === undefined || t.state === null);
1401
-
1402
- if (hasUndefinedType) {
1403
- console.warn('Some tasks have undefined or null type values!');
1404
- }
1405
-
1406
- if (hasUndefinedState) {
1407
- console.warn('Some tasks have undefined or null state values!');
1408
- }
1409
-
1410
- console.groupEnd();
1411
-
1412
- console.groupEnd();
1413
- },
1414
-
1415
- // Initialize Flatpickr datetime pickers for both create and edit forms
1416
- /**
1417
- * Initialize Flatpickr date/time pickers for scheduler forms
1418
- *
1419
- * @param {string} mode - Which pickers to initialize: 'all', 'create', or 'edit'
1420
- * @returns {void}
1421
- */
1422
- initFlatpickr(mode = 'all') {
1423
- const initPicker = (inputId, refName, wrapperClass, options = {}) => {
1424
- // Try to get input using Alpine.js x-ref first (more reliable)
1425
- let input = this.$refs[refName];
1426
-
1427
- // Fall back to getElementById if x-ref is not available
1428
- if (!input) {
1429
- input = document.getElementById(inputId);
1430
- console.log(`Using getElementById fallback for ${inputId}`);
1431
- }
1432
-
1433
- if (!input) {
1434
- console.warn(`Input element ${inputId} not found by ID or ref`);
1435
- return null;
1436
- }
1437
-
1438
- // Create a wrapper around the input
1439
- const wrapper = document.createElement('div');
1440
- wrapper.className = wrapperClass || 'scheduler-flatpickr-wrapper';
1441
- wrapper.style.overflow = 'visible'; // Ensure dropdown can escape container
1442
-
1443
- // Replace the input with our wrapped version
1444
- input.parentNode.insertBefore(wrapper, input);
1445
- wrapper.appendChild(input);
1446
- input.classList.add('scheduler-flatpickr-input');
1447
-
1448
- // Default options
1449
- const defaultOptions = {
1450
- dateFormat: "Y-m-d H:i",
1451
- enableTime: true,
1452
- time_24hr: true,
1453
- static: false, // Not static so it will float
1454
- appendTo: document.body, // Append to body to avoid overflow issues
1455
- theme: "scheduler-theme",
1456
- allowInput: true,
1457
- positionElement: wrapper, // Position relative to wrapper
1458
- onOpen: function(selectedDates, dateStr, instance) {
1459
- // Ensure calendar is properly positioned and visible
1460
- instance.calendarContainer.style.zIndex = '9999';
1461
- instance.calendarContainer.style.position = 'absolute';
1462
- instance.calendarContainer.style.visibility = 'visible';
1463
- instance.calendarContainer.style.opacity = '1';
1464
-
1465
- // Add class to calendar container for our custom styling
1466
- instance.calendarContainer.classList.add('scheduler-theme');
1467
- },
1468
- // Set default date to 30 minutes from now if no date selected
1469
- onReady: function(selectedDates, dateStr, instance) {
1470
- if (!dateStr) {
1471
- const now = new Date();
1472
- now.setMinutes(now.getMinutes() + 30);
1473
- instance.setDate(now, true);
1474
- }
1475
- }
1476
- };
1477
-
1478
- // Merge options
1479
- const mergedOptions = {...defaultOptions, ...options};
1480
-
1481
- // Initialize flatpickr
1482
- const fp = flatpickr(input, mergedOptions);
1483
-
1484
- // Add a clear button
1485
- const clearButton = document.createElement('button');
1486
- clearButton.className = 'scheduler-flatpickr-clear';
1487
- clearButton.innerHTML = '×';
1488
- clearButton.type = 'button';
1489
- clearButton.addEventListener('click', (e) => {
1490
- e.preventDefault();
1491
- e.stopPropagation();
1492
- if (fp) {
1493
- fp.clear();
1494
- }
1495
- });
1496
- wrapper.appendChild(clearButton);
1497
-
1498
- return fp;
1499
- };
1500
-
1501
- // Clear any existing Flatpickr instances to prevent duplication
1502
- if (mode === 'all' || mode === 'create') {
1503
- const createInput = document.getElementById('newPlannedTime-create');
1504
- if (createInput && createInput._flatpickr) {
1505
- createInput._flatpickr.destroy();
1506
- }
1507
- }
1508
-
1509
- if (mode === 'all' || mode === 'edit') {
1510
- const editInput = document.getElementById('newPlannedTime-edit');
1511
- if (editInput && editInput._flatpickr) {
1512
- editInput._flatpickr.destroy();
1513
- }
1514
- }
1515
-
1516
- // Initialize new instances
1517
- if (mode === 'all' || mode === 'create') {
1518
- initPicker('newPlannedTime-create', 'plannedTimeCreate', 'scheduler-flatpickr-wrapper', {
1519
- minuteIncrement: 5,
1520
- defaultHour: new Date().getHours(),
1521
- defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
1522
- });
1523
- }
1524
-
1525
- if (mode === 'all' || mode === 'edit') {
1526
- initPicker('newPlannedTime-edit', 'plannedTimeEdit', 'scheduler-flatpickr-wrapper', {
1527
- minuteIncrement: 5,
1528
- defaultHour: new Date().getHours(),
1529
- defaultMinute: Math.ceil(new Date().getMinutes() / 5) * 5
1530
- });
1531
- }
1532
- },
1533
-
1534
- // Update tasks UI
1535
- updateTasksUI() {
1536
- // First update filteredTasks if that method exists
1537
- if (typeof this.updateFilteredTasks === 'function') {
1538
- this.updateFilteredTasks();
1539
- }
1540
-
1541
- // Wait for UI to update
1542
- this.$nextTick(() => {
1543
- // Get empty state and task list elements
1544
- const emptyElement = document.querySelector('.scheduler-empty');
1545
- const tableElement = document.querySelector('.scheduler-task-list');
1546
-
1547
- // Calculate visibility state based on filtered tasks
1548
- const hasFilteredTasks = Array.isArray(this.filteredTasks) && this.filteredTasks.length > 0;
1549
-
1550
- // Update visibility directly
1551
- if (emptyElement) {
1552
- emptyElement.style.display = !hasFilteredTasks ? '' : 'none';
1553
- }
1554
-
1555
- if (tableElement) {
1556
- tableElement.style.display = hasFilteredTasks ? '' : 'none';
1557
- }
1558
- });
1559
- }
1560
- };
1561
-};
1562
-
1563
-
1564
-// Only define the component if it doesn't already exist or extend the existing one
1565
-if (!window.schedulerSettings) {
1566
- console.log('Defining schedulerSettings component from scratch');
1567
- window.schedulerSettings = fullComponentImplementation;
1568
-} else {
1569
- console.log('Extending existing schedulerSettings component');
1570
- // Store the original function
1571
- const originalSchedulerSettings = window.schedulerSettings;
1572
-
1573
- // Replace with enhanced version that merges the pre-initialized stub with the full implementation
1574
- window.schedulerSettings = function() {
1575
- // Get the base pre-initialized component
1576
- const baseComponent = originalSchedulerSettings();
1577
-
1578
- // Create a backup of the original init function
1579
- const originalInit = baseComponent.init || function() {};
1580
-
1581
- // Create our enhanced init function that adds the missing functionality
1582
- baseComponent.init = function() {
1583
- // Call the original init if it exists
1584
- originalInit.call(this);
1585
-
1586
- console.log('Enhanced init running: adding missing methods to component');
1587
-
1588
- // Get the full implementation
1589
- const fullImpl = fullComponentImplementation();
1590
-
1591
- // Register all implementation methods (except init) directly
1592
- Object.keys(fullImpl).forEach((key) => {
1593
- if (key === 'init') {
1594
- return;
1595
- }
1596
- if (typeof fullImpl[key] === 'function') {
1597
- console.log(`Registering method: ${key}`);
1598
- this[key] = fullImpl[key];
1599
- }
1600
- });
1601
-
1602
- if (typeof this.refreshProjectOptions === 'function') {
1603
- this.refreshProjectOptions();
1604
- }
1605
-
1606
- // hack to expose deleteTask
1607
- window.deleteTaskGlobal = this.deleteTask.bind(this);
1608
-
1609
- // Make sure we have a filteredTasks array initialized
1610
- this.filteredTasks = [];
1611
-
1612
- // Initialize essential properties if missing
1613
- if (!Array.isArray(this.tasks)) {
1614
- this.tasks = [];
1615
- }
1616
-
1617
- if (!Array.isArray(this.projectOptions)) {
1618
- this.projectOptions = [];
1619
- }
1620
-
1621
- if (typeof this.selectedProjectSlug !== 'string') {
1622
- this.selectedProjectSlug = '';
1623
- }
1624
-
1625
- // Make sure attachmentsText getter/setter are defined
1626
- if (!Object.getOwnPropertyDescriptor(this, 'attachmentsText')?.get) {
1627
- Object.defineProperty(this, 'attachmentsText', {
1628
- get: function() {
1629
- // Ensure we always have an array to work with
1630
- const attachments = Array.isArray(this.editingTask?.attachments)
1631
- ? this.editingTask.attachments
1632
- : [];
1633
-
1634
- // Join array items with newlines
1635
- return attachments.join('\n');
1636
- },
1637
- set: function(value) {
1638
- if (!this.editingTask) {
1639
- this.editingTask = {
1640
- attachments: [],
1641
- project: null,
1642
- dedicated_context: true,
1643
- };
1644
- }
1645
-
1646
- if (typeof value === 'string') {
1647
- // Just split by newlines without filtering to preserve editing experience
1648
- this.editingTask.attachments = value.split('\n');
1649
- } else {
1650
- // Fallback to empty array if not a string
1651
- this.editingTask.attachments = [];
1652
- }
1653
- }
1654
- });
1655
- }
1656
-
1657
- // Add methods for updating filteredTasks directly
1658
- if (typeof this.updateFilteredTasks !== 'function') {
1659
- this.updateFilteredTasks = function() {
1660
- // Make sure we have tasks to filter
1661
- if (!Array.isArray(this.tasks)) {
1662
- this.filteredTasks = [];
1663
- return;
1664
- }
1665
-
1666
- let filtered = [...this.tasks];
1667
-
1668
- // Apply type filter with case-insensitive comparison
1669
- if (this.filterType && this.filterType !== 'all') {
1670
- filtered = filtered.filter(task => {
1671
- if (!task.type) return false;
1672
- return String(task.type).toLowerCase() === this.filterType.toLowerCase();
1673
- });
1674
- }
1675
-
1676
- // Apply state filter with case-insensitive comparison
1677
- if (this.filterState && this.filterState !== 'all') {
1678
- filtered = filtered.filter(task => {
1679
- if (!task.state) return false;
1680
- return String(task.state).toLowerCase() === this.filterState.toLowerCase();
1681
- });
1682
- }
1683
-
1684
- // Sort the filtered tasks
1685
- if (typeof this.sortTasks === 'function') {
1686
- filtered = this.sortTasks(filtered);
1687
- }
1688
-
1689
- // Directly update the filteredTasks property
1690
- this.filteredTasks = filtered;
1691
- };
1692
- }
1693
-
1694
- // Set up watchers to update filtered tasks when dependencies change
1695
- this.$nextTick(() => {
1696
- // Update filtered tasks when raw tasks change
1697
- this.$watch('tasks', () => {
1698
- this.updateFilteredTasks();
1699
- });
1700
-
1701
- // Update filtered tasks when filter type changes
1702
- this.$watch('filterType', () => {
1703
- this.updateFilteredTasks();
1704
- });
1705
-
1706
- // Update filtered tasks when filter state changes
1707
- this.$watch('filterState', () => {
1708
- this.updateFilteredTasks();
1709
- });
1710
-
1711
- // Update filtered tasks when sort field or direction changes
1712
- this.$watch('sortField', () => {
1713
- this.updateFilteredTasks();
1714
- });
1715
-
1716
- this.$watch('sortDirection', () => {
1717
- this.updateFilteredTasks();
1718
- });
1719
-
1720
- // Initial update
1721
- this.updateFilteredTasks();
1722
-
1723
- // Set up watcher for task type changes to initialize Flatpickr for planned tasks
1724
- this.$watch('editingTask.type', (newType) => {
1725
- if (newType === 'planned') {
1726
- this.$nextTick(() => {
1727
- // Reinitialize Flatpickr when switching to planned task type
1728
- if (this.isCreating) {
1729
- this.initFlatpickr('create');
1730
- } else if (this.isEditing) {
1731
- this.initFlatpickr('edit');
1732
- }
1733
- });
1734
- }
1735
- });
1736
-
1737
- // Initialize Flatpickr
1738
- this.$nextTick(() => {
1739
- if (typeof this.initFlatpickr === 'function') {
1740
- this.initFlatpickr();
1741
- } else {
1742
- console.error('initFlatpickr is not available');
1743
- }
1744
- });
1745
- });
1746
-
1747
- // Try fetching tasks after a short delay
1748
- setTimeout(() => {
1749
- if (typeof this.fetchTasks === 'function') {
1750
- this.fetchTasks();
1751
- } else {
1752
- console.error('fetchTasks still not available after enhancement');
1753
- }
1754
- }, 100);
1755
-
1756
- console.log('Enhanced init complete');
1757
- };
1758
-
1759
- return baseComponent;
1760
- };
1761
-}
1762
-
1763
-// Force Alpine.js to register the component immediately
1764
-if (window.Alpine) {
1765
- // Alpine is already loaded, register now
1766
- console.log('Alpine already loaded, registering schedulerSettings component now');
1767
- window.Alpine.data('schedulerSettings', window.schedulerSettings);
1768
-} else {
1769
- // Wait for Alpine to load
1770
- document.addEventListener('alpine:init', () => {
1771
- console.log('Alpine:init - immediately registering schedulerSettings component');
1772
- Alpine.data('schedulerSettings', window.schedulerSettings);
1773
- });
1774
-}
1775
-
1776
-// Add a document ready event handler to ensure the scheduler tab can be clicked on first load
1777
-document.addEventListener('DOMContentLoaded', function() {
1778
- console.log('DOMContentLoaded - setting up scheduler tab click handler');
1779
- // Setup scheduler tab click handling
1780
- const setupSchedulerTab = () => {
1781
- const settingsModal = document.getElementById('settingsModal');
1782
- if (!settingsModal) {
1783
- setTimeout(setupSchedulerTab, 100);
1784
- return;
1785
- }
1786
-
1787
- // Create a global event listener for clicks on the scheduler tab
1788
- document.addEventListener('click', function(e) {
1789
- // Find if the click was on the scheduler tab or its children
1790
- const schedulerTab = e.target.closest('.settings-tab[title="Task Scheduler"]');
1791
- if (!schedulerTab) return;
1792
-
1793
- e.preventDefault();
1794
- e.stopPropagation();
1795
-
1796
- // Get the settings modal data
1797
- try {
1798
- const modalData = Alpine.$data(settingsModal);
1799
- if (modalData.activeTab !== 'scheduler') {
1800
- // Directly call the modal's switchTab method
1801
- modalData.switchTab('scheduler');
1802
- }
1803
-
1804
- // Force start polling and fetch tasks immediately when tab is selected
1805
- setTimeout(() => {
1806
- // Get the scheduler component data
1807
- const schedulerElement = document.querySelector('[x-data="schedulerSettings"]');
1808
- if (schedulerElement) {
1809
- const schedulerData = Alpine.$data(schedulerElement);
1810
-
1811
- // Force fetch tasks and start polling
1812
- if (typeof schedulerData.fetchTasks === 'function') {
1813
- schedulerData.fetchTasks();
1814
- } else {
1815
- console.error('fetchTasks is not a function on scheduler component');
1816
- }
1817
-
1818
- if (typeof schedulerData.startPolling === 'function') {
1819
- schedulerData.startPolling();
1820
- } else {
1821
- console.error('startPolling is not a function on scheduler component');
1822
- }
1823
- } else {
1824
- console.error('Could not find scheduler component element');
1825
- }
1826
- }, 100);
1827
- } catch (err) {
1828
- console.error('Error handling scheduler tab click:', err);
1829
- }
1830
- }, true); // Use capture phase to intercept before Alpine.js handlers
1831
- };
1832
-
1833
- // Initialize the tab handling
1834
- setupSchedulerTab();
1835
-});