|
1 | 1 | import React, { useState, useEffect } from 'react'; |
2 | | -import { X, Calendar, Flag, Folder, Check } from 'lucide-react'; |
| 2 | +import { X, Calendar, Flag, Folder, Check, Sunrise, CalendarPlus, Sofa, Briefcase, Tent, CalendarFold } from 'lucide-react'; |
3 | 3 | import { TickTickTask, TickTickProject } from '@/lib/ticktick'; |
4 | 4 | import { parseISO } from 'date-fns'; |
5 | 5 | import { TagCombobox } from './TagCombobox'; |
@@ -111,6 +111,68 @@ export function TaskDetailModal({ isOpen, onClose, task, onUpdate, onDelete, pro |
111 | 111 | } |
112 | 112 | }; |
113 | 113 |
|
| 114 | + // Quick reschedule helpers |
| 115 | + const toDateStr = (d: Date): string => { |
| 116 | + const year = d.getFullYear(); |
| 117 | + const month = String(d.getMonth() + 1).padStart(2, '0'); |
| 118 | + const day = String(d.getDate()).padStart(2, '0'); |
| 119 | + return `${year}-${month}-${day}T12:00:00+0000`; |
| 120 | + }; |
| 121 | + |
| 122 | + const offsetDate = (days: number): string => { |
| 123 | + const d = new Date(); |
| 124 | + d.setDate(d.getDate() + days); |
| 125 | + return toDateStr(d); |
| 126 | + }; |
| 127 | + |
| 128 | + const getThisSaturday = (): string => { |
| 129 | + const dow = new Date().getDay(); // 0=Sun,6=Sat |
| 130 | + const offset = dow === 6 ? 7 : dow === 0 ? 6 : 6 - dow; |
| 131 | + return offsetDate(offset); |
| 132 | + }; |
| 133 | + |
| 134 | + const getNextMonday = (): string => { |
| 135 | + const dow = new Date().getDay(); |
| 136 | + const offset = dow === 1 ? 7 : (8 - dow) % 7; |
| 137 | + return offsetDate(offset); |
| 138 | + }; |
| 139 | + |
| 140 | + const getNextSaturday = (): string => { |
| 141 | + const dow = new Date().getDay(); |
| 142 | + const offset = (dow === 6 ? 7 : dow === 0 ? 6 : 6 - dow) + 7; |
| 143 | + return offsetDate(offset); |
| 144 | + }; |
| 145 | + |
| 146 | + const getNextMonthFirst = (): string => { |
| 147 | + const today = new Date(); |
| 148 | + return toDateStr(new Date(today.getFullYear(), today.getMonth() + 1, 1)); |
| 149 | + }; |
| 150 | + |
| 151 | + const handleQuickReschedule = (newDateStr: string) => { |
| 152 | + const updates: Partial<TickTickTask> = {}; |
| 153 | + if (title !== task.title) updates.title = title; |
| 154 | + if (content !== (task.content || '')) updates.content = content; |
| 155 | + if (priority !== (task.priority || 0)) updates.priority = priority; |
| 156 | + if (projectId !== task.projectId) updates.projectId = projectId; |
| 157 | + const originalTags = task.tags || []; |
| 158 | + if (JSON.stringify(tags) !== JSON.stringify(originalTags)) updates.tags = tags; |
| 159 | + updates.startDate = newDateStr; |
| 160 | + updates.dueDate = newDateStr; |
| 161 | + onUpdate(task.id, updates); |
| 162 | + onClose(); |
| 163 | + }; |
| 164 | + |
| 165 | + const RESCHEDULE_OPTIONS = [ |
| 166 | + { label: 'Tomorrow', icon: Sunrise, date: () => offsetDate(1) }, |
| 167 | + { label: 'In 3 days', icon: CalendarPlus, date: () => offsetDate(3) }, |
| 168 | + { label: 'This Sat', icon: Sofa, date: getThisSaturday }, |
| 169 | + { label: 'Next Mon', icon: Briefcase, date: getNextMonday }, |
| 170 | + { label: 'Next Sat', icon: Tent, date: getNextSaturday }, |
| 171 | + { label: 'In 15 days', icon: CalendarPlus, date: () => offsetDate(15) }, |
| 172 | + { label: 'Next month', icon: CalendarFold, date: getNextMonthFirst }, |
| 173 | + { label: 'In 30 days', icon: CalendarPlus, date: () => offsetDate(30) }, |
| 174 | + ]; |
| 175 | + |
114 | 176 | // Tag management |
115 | 177 | const addTag = (tag: string) => { |
116 | 178 | const trimmed = tag.trim().toLowerCase(); |
@@ -147,6 +209,7 @@ export function TaskDetailModal({ isOpen, onClose, task, onUpdate, onDelete, pro |
147 | 209 | onClick={() => { |
148 | 210 | const newStatus = task.status === 2 ? 0 : 2; |
149 | 211 | onUpdate(task.id, { status: newStatus }); |
| 212 | + if (newStatus === 2) onClose(); |
150 | 213 | }} |
151 | 214 | className={`w-5 h-5 rounded border flex items-center justify-center transition-colors ${task.status === 2 |
152 | 215 | ? 'bg-green-500 border-green-500 text-white' |
@@ -216,6 +279,23 @@ export function TaskDetailModal({ isOpen, onClose, task, onUpdate, onDelete, pro |
216 | 279 | </div> |
217 | 280 | </div> |
218 | 281 |
|
| 282 | + {/* Quick Reschedule */} |
| 283 | + <div className="mb-4"> |
| 284 | + <label className="block text-xs font-bold text-gray-400 dark:text-gray-500 uppercase tracking-widest mb-2">Reschedule</label> |
| 285 | + <div className="flex flex-wrap gap-2"> |
| 286 | + {RESCHEDULE_OPTIONS.map(({ label, icon: Icon, date }) => ( |
| 287 | + <button |
| 288 | + key={label} |
| 289 | + onClick={() => handleQuickReschedule(date())} |
| 290 | + className="flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium rounded-md bg-gray-50 dark:bg-gray-800 text-gray-600 dark:text-gray-300 hover:bg-blue-50 dark:hover:bg-blue-900/30 hover:text-blue-600 dark:hover:text-blue-400 transition-colors" |
| 291 | + > |
| 292 | + <Icon size={12} /> |
| 293 | + {label} |
| 294 | + </button> |
| 295 | + ))} |
| 296 | + </div> |
| 297 | + </div> |
| 298 | + |
219 | 299 | {/* Tags Section */} |
220 | 300 | <div className="mb-6"> |
221 | 301 | <label className="block text-xs font-bold text-gray-400 uppercase tracking-widest mb-2">Tags</label> |
|
0 commit comments