import { action, makeObservable, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { FieldView } from './FieldView'; import { DateField } from '../../../fields/DateField'; import { Doc } from '../../../fields/Doc'; import './TaskBox.scss'; import { GoogleAuthenticationManager } from '../../apis/GoogleAuthenticationManager'; /** * Props (reference to document) for Task Box */ interface TaskBoxProps { Document: Doc; } /** * TaskBox class for adding task information + completing tasks */ @observer export class TaskBox extends React.Component { // contains the last synced task information lastSyncedTask: { title: string; text: string; due?: string; completed: boolean; } = { title: '', text: '', due: '', completed: false, }; state = { needsSync: false, }; /** * Method to reuturn the * @param fieldStr * @returns */ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(TaskBox, fieldStr); } /** * Method to update the task description * @param e - event of changing the description box input */ @action updateText = (e: React.ChangeEvent) => { this.props.Document.text = e.target.value; }; /** * Method to update the task title * @param e - event of changing the title box input */ @action updateTitle = (e: React.ChangeEvent) => { this.props.Document.title = e.target.value; }; /** * Method to update the all day status * @param e - event of changing the all day checkbox */ @action updateAllDay = (e: React.ChangeEvent) => { this.props.Document.$task_allDay = e.target.checked; if (e.target.checked) { delete this.props.Document.$task_startTime; delete this.props.Document.$task_endTime; } this.setTaskDateRange(); }; /** * Method to update the task start time * @param e - event of changing the start time input */ @action updateStart = (e: React.ChangeEvent) => { const newStart = new Date(e.target.value); this.props.Document.$task_startTime = new DateField(newStart); const endDate = this.props.Document.$task_endTime instanceof DateField ? this.props.Document.$task_endTime.date : undefined; if (endDate && newStart > endDate) { // Alert user alert('Start time cannot be after end time. End time has been adjusted.'); // Fix end time const adjustedEnd = new Date(newStart.getTime() + 60 * 60 * 1000); this.props.Document.$task_endTime = new DateField(adjustedEnd); } this.setTaskDateRange(); }; /** * Method to update the task end time * @param e - event of changing the end time input */ @action updateEnd = (e: React.ChangeEvent) => { const newEnd = new Date(e.target.value); this.props.Document.$task_endTime = new DateField(newEnd); const startDate = this.props.Document.$task_startTime instanceof DateField ? this.props.Document.$task_startTime.date : undefined; if (startDate && newEnd < startDate) { // Alert user alert('End time cannot be before start time. Start time has been adjusted.'); // Fix start time const adjustedStart = new Date(newEnd.getTime() - 60 * 60 * 1000); this.props.Document.$task_startTime = new DateField(adjustedStart); } this.setTaskDateRange(); }; /** * Method to update the task date range */ @action setTaskDateRange() { const doc = this.props.Document; if (doc.$task_allDay) { const range = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange.split('|') : []; const dateStr = range[0] ?? new Date().toISOString().split('T')[0]; // default to today doc.$task_dateRange = `${dateStr}|${dateStr}`; doc.$task_allDay = true; } else { const startField = doc.$task_startTime; const endField = doc.$task_endTime; const startDate = startField instanceof DateField ? startField.date : null; const endDate = endField instanceof DateField ? endField.date : null; if (startDate && endDate && !isNaN(startDate.getTime()) && !isNaN(endDate.getTime())) { doc.$task_dateRange = `${startDate.toISOString()}|${endDate.toISOString()}`; doc.$task_allDay = false; } } } /** * Method to set task's completion status * @param e - event of changing the "completed" input checkbox */ @action toggleComplete = (e: React.ChangeEvent) => { this.props.Document.$task_completed = e.target.checked; }; /** * Constructor for the task box * @param props - props containing the document reference */ constructor(props: TaskBoxProps) { super(props); makeObservable(this); } _googleTaskCreateDisposer?: IReactionDisposer; _heightDisposer?: IReactionDisposer; _widthDisposer?: IReactionDisposer; componentDidMount() { this.setTaskDateRange(); const doc = this.props.Document; // adding task on creation to google (async () => { if (!doc.$googleTaskId && doc.title) { try { const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); if (!token) return; const body: any = { title: doc.title || 'Untitled Task', notes: doc.text || '', status: doc.$task_completed ? 'completed' : 'needsAction', completed: doc.$task_completed ? new Date().toISOString() : undefined, }; if (doc.$task_allDay && typeof doc.$task_dateRange === 'string') { const datePart = doc.$task_dateRange.split('|')[0]; if (datePart && !isNaN(new Date(datePart).getTime())) { const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z'; body.due = new Date(baseDate).toISOString(); } } else if (doc.$task_endTime instanceof DateField) { body.due = doc.$task_endTime.date.toISOString(); } const res = await fetch('/googleTasks/create', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify(body), }); const result = await res.json(); if (result?.id) { doc.$googleTaskId = result.id; console.log('✅ Google Task created on mount:', result); } else { console.warn('❌ Google Task creation failed:', result); } } catch (err) { console.error('❌ Error creating Google Task:', err); } } })(); this._heightDisposer = reaction( () => Number(doc._height), height => { const minHeight = Number(doc.height_min ?? 0); if (!isNaN(height) && height < minHeight) { doc._height = minHeight; } } ); this._widthDisposer = reaction( () => Number(doc._width), width => { const minWidth = Number(doc.width_min ?? 0); if (!isNaN(width) && width < minWidth) { doc._width = minWidth; } } ); this._googleTaskCreateDisposer = reaction( () => { const { title, text, $task_completed, $task_dateRange, $task_startTime, $task_endTime, $task_allDay } = doc; const completed = !!$task_completed; let due: string | undefined; if ($task_allDay && typeof $task_dateRange === 'string') { const datePart = $task_dateRange.split('|')[0]; if (datePart && !isNaN(new Date(datePart).getTime())) { due = new Date(datePart + 'T00:00:00Z').toISOString(); } } else if ($task_endTime && $task_endTime instanceof DateField && $task_endTime.date) { due = $task_endTime.date.toISOString(); } else if ($task_startTime && $task_startTime instanceof DateField && $task_startTime.date) { due = $task_startTime.date.toISOString(); } return { title, text, completed, due }; }, current => { const hasChanged = current.title !== this.lastSyncedTask.title || current.text !== this.lastSyncedTask.text || current.due !== this.lastSyncedTask.due || current.completed !== this.lastSyncedTask.completed; this.setState({ needsSync: hasChanged }); }, { fireImmediately: true } ); } componentWillUnmount() { const doc = this.props.Document; this._googleTaskCreateDisposer?.(); this._heightDisposer?.(); this._widthDisposer?.(); // task deletion if (doc.$googleTaskId) { (async () => { try { const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); if (!token) return; await fetch(`/googleTasks/${doc.$googleTaskId}`, { method: 'DELETE', credentials: 'include', headers: { Authorization: `Bearer ${token}`, }, }); console.log(`✅ Deleted Google Task ${doc.$googleTaskId}`); } catch (err) { console.warn('❌ Failed to delete Google Task:', err); } })(); } } /** * Method to render the task box * @returns - HTML with taskbox components */ render() { function toLocalDateTimeString(date: Date): string { const pad = (n: number) => n.toString().padStart(2, '0'); return date.getFullYear() + '-' + pad(date.getMonth() + 1) + '-' + pad(date.getDate()) + 'T' + pad(date.getHours()) + ':' + pad(date.getMinutes()); } const doc = this.props.Document; const taskDesc = typeof doc.text === 'string' ? doc.text : ''; const taskTitle = typeof doc.title === 'string' ? doc.title : ''; const allDay = !!doc.$task_allDay; const isCompleted = !!this.props.Document.$task_completed; const startTime = doc.$task_startTime instanceof DateField && doc.$task_startTime.date instanceof Date ? toLocalDateTimeString(doc.$task_startTime.date) : ''; const endTime = doc.$task_endTime instanceof DateField && doc.$task_endTime.date instanceof Date ? toLocalDateTimeString(doc.$task_endTime.date) : ''; const handleGoogleTaskSync = async () => { console.log('GT button clicked'); try { const token = await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); if (!token) { const listener = () => { window.removeEventListener('focusin', listener); if (confirm('✅ Authorization complete. Try syncing the task again?')) { // you could refactor the click handler here handleGoogleTaskSync(); } window.removeEventListener('focusin', listener); }; setTimeout(() => window.addEventListener('focusin', listener), 100); return; } let due: string | undefined; if (allDay) { const rawRange = typeof doc.$task_dateRange === 'string' ? doc.$task_dateRange : ''; const datePart = rawRange.split('|')[0]; if (datePart && !isNaN(new Date(datePart).getTime())) { // Set time to midnight UTC to represent the start of the all-day event const baseDate = datePart.includes('T') ? datePart : datePart + 'T00:00:00Z'; due = new Date(baseDate).toISOString(); } else { due = undefined; } } else if (doc.$task_endTime instanceof DateField && doc.$task_endTime.date) { due = doc.$task_endTime.date.toISOString(); } else if (doc.$task_startTime instanceof DateField && doc.$task_startTime.date) { due = doc.$task_startTime.date.toISOString(); } else { due = undefined; } const isUpdate = !!doc.$googleTaskId; const endpoint = isUpdate ? `/googleTasks/${doc.$googleTaskId}` : '/googleTasks/create'; const method = isUpdate ? 'PATCH' : 'POST'; const response = await fetch(endpoint, { method, credentials: 'include', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify({ title: taskTitle || 'Untitled Task', notes: taskDesc, due, status: doc.$task_completed ? 'completed' : 'needsAction', completed: doc.$task_completed ? new Date().toISOString() : undefined, }), }); const result = await response.json(); console.log('Google Task result:', result); if (result?.id) { alert('✅ Task synced with Google Tasks!'); if (result?.id) { this.lastSyncedTask = { title: taskTitle, text: taskDesc, due, completed: isCompleted, }; this.setState({ needsSync: false }); } } else { alert(`❌ Failed: ${result?.error?.message || 'Unknown error'}`); } } catch (err) { console.error('Fetch error:', err); alert('❌ Task syncing failed.'); } }; return (