1
1
import { usePrivy } from "@privy-io/react-auth" ;
2
- import { Calendar , Clock , Plus } from "lucide-react" ;
2
+ import { Calendar , Clock , Plus , RefreshCw , Trash2 } from "lucide-react" ;
3
3
import { useRouter } from "next/router" ;
4
4
import { useCallback , useEffect , useMemo , useState } from "react" ;
5
5
import { VAPIClient } from "../lib/api-client" ;
6
- import { ActionResponse , getActions } from "../lib/oto-api" ;
6
+ import { ActionResponse , getActions , updateAction } from "../lib/oto-api" ;
7
7
import { LoadingSpinner } from "./LoadingSpinner" ;
8
8
import { useToast } from "./Toast" ;
9
9
import { Button } from "./ui/button" ;
@@ -186,21 +186,38 @@ export function TaskList({ className }: TaskListProps) {
186
186
const task = tasks . find ( ( t ) => t . id === taskId ) ;
187
187
if ( ! task ) return ;
188
188
189
- const updatedTask = { ...task , completed : ! task . completed } ;
190
-
191
- // TODO: Implement API call to update task status
192
- // For now, only update local state since we don't have an update endpoint yet
193
- // In the future, you would call something like:
194
- // await updateActionStatus(taskId, updatedTask.completed ? "completed" : "created", userId, apiKey, apiEndpoint);
195
-
196
- setTasks ( ( prevTasks ) => prevTasks . map ( ( t ) => ( t . id === taskId ? updatedTask : t ) ) ) ;
189
+ if ( ! apiEndpoint || ! apiKey || ! userId ) {
190
+ showToast ( {
191
+ type : "error" ,
192
+ title : "Cannot update task - API not configured" ,
193
+ } ) ;
194
+ return ;
195
+ }
197
196
198
- showToast ( {
199
- type : updatedTask . completed ? "success" : "success" ,
200
- title : updatedTask . completed
201
- ? "Task marked as completed (local only)"
202
- : "Task marked as incomplete (local only)" ,
203
- } ) ;
197
+ const newStatus = task . completed ? "created" : "completed" ;
198
+
199
+ console . log ( "🔄 Updating task status..." , { taskId, newStatus } ) ;
200
+
201
+ // Call the API to update the task status
202
+ const result = await updateAction ( taskId , newStatus , userId , apiKey , apiEndpoint ) ;
203
+
204
+ if ( result . success ) {
205
+ // Update local state only if API call was successful
206
+ const updatedTask = { ...task , completed : ! task . completed } ;
207
+ setTasks ( ( prevTasks ) => prevTasks . map ( ( t ) => ( t . id === taskId ? updatedTask : t ) ) ) ;
208
+
209
+ showToast ( {
210
+ type : "success" ,
211
+ title : updatedTask . completed
212
+ ? "Task marked as completed"
213
+ : "Task marked as incomplete" ,
214
+ } ) ;
215
+ } else {
216
+ showToast ( {
217
+ type : "error" ,
218
+ title : "Failed to update task status" ,
219
+ } ) ;
220
+ }
204
221
} catch ( err ) {
205
222
console . error ( "Failed to update task:" , err ) ;
206
223
showToast ( {
@@ -230,6 +247,50 @@ export function TaskList({ className }: TaskListProps) {
230
247
}
231
248
} ;
232
249
250
+ /**
251
+ * Handles task deletion by marking it as deleted
252
+ */
253
+ const handleDeleteTask = async ( taskId : string ) => {
254
+ try {
255
+ const task = tasks . find ( ( t ) => t . id === taskId ) ;
256
+ if ( ! task ) return ;
257
+
258
+ if ( ! apiEndpoint || ! apiKey || ! userId ) {
259
+ showToast ( {
260
+ type : "error" ,
261
+ title : "Cannot delete task - API not configured" ,
262
+ } ) ;
263
+ return ;
264
+ }
265
+
266
+ console . log ( "🗑️ Deleting task..." , { taskId } ) ;
267
+
268
+ // Call the API to mark the task as deleted
269
+ const result = await updateAction ( taskId , "deleted" , userId , apiKey , apiEndpoint ) ;
270
+
271
+ if ( result . success ) {
272
+ // Remove the task from local state
273
+ setTasks ( ( prevTasks ) => prevTasks . filter ( ( t ) => t . id !== taskId ) ) ;
274
+
275
+ showToast ( {
276
+ type : "success" ,
277
+ title : "Task deleted successfully" ,
278
+ } ) ;
279
+ } else {
280
+ showToast ( {
281
+ type : "error" ,
282
+ title : "Failed to delete task" ,
283
+ } ) ;
284
+ }
285
+ } catch ( err ) {
286
+ console . error ( "Failed to delete task:" , err ) ;
287
+ showToast ( {
288
+ type : "error" ,
289
+ title : "Failed to delete task" ,
290
+ } ) ;
291
+ }
292
+ } ;
293
+
233
294
const getPriorityColor = ( priority ?: Task [ "priority" ] ) => {
234
295
switch ( priority ) {
235
296
case "high" :
@@ -277,7 +338,19 @@ export function TaskList({ className }: TaskListProps) {
277
338
< div className = { `p-6 ${ className || "" } ` } >
278
339
< div className = "max-w-4xl mx-auto" >
279
340
< div className = "mb-6" >
280
- < h1 className = "text-3xl font-bold text-gray-900 mb-2" > Task List</ h1 >
341
+ < div className = "flex items-center justify-between mb-2" >
342
+ < h1 className = "text-3xl font-bold text-gray-900" > Task List</ h1 >
343
+ < Button
344
+ onClick = { loadTasks }
345
+ variant = "outline"
346
+ size = "sm"
347
+ className = "flex items-center gap-2"
348
+ disabled = { loading }
349
+ >
350
+ < RefreshCw size = { 16 } className = { loading ? "animate-spin" : "" } />
351
+ Refresh
352
+ </ Button >
353
+ </ div >
281
354
< p className = "text-gray-600" > View and manage tasks extracted from voice conversations</ p >
282
355
</ div >
283
356
@@ -299,92 +372,124 @@ export function TaskList({ className }: TaskListProps) {
299
372
</ Button >
300
373
</ div >
301
374
) : (
302
- < div className = "space-y-4" >
303
- { tasks . map ( ( task ) => (
304
- < div
305
- key = { task . id }
306
- className = { `bg-white rounded-lg shadow-sm border transition-all duration-200 ${
307
- task . completed ? "opacity-75" : "hover:shadow-md"
308
- } `}
309
- >
310
- < div className = "p-6" >
311
- < div className = "flex items-start justify-between mb-3" >
312
- < div className = "flex items-start gap-3 flex-1" >
313
- < input
314
- type = "checkbox"
315
- checked = { task . completed }
316
- onChange = { ( ) => handleToggleComplete ( task . id ) }
317
- className = "mt-1 h-4 w-4 text-violet-600 rounded border-gray-300 focus:ring-violet-500"
318
- />
319
- < div className = "flex-1" >
320
- < h3
321
- className = { `text-lg font-medium mb-1 ${
322
- task . completed ? "line-through text-gray-500" : "text-gray-900"
323
- } `}
324
- >
325
- { task . title }
326
- </ h3 >
327
- < p
328
- className = { `text-sm mb-3 ${
329
- task . completed ? "text-gray-400" : "text-gray-600"
330
- } `}
331
- >
332
- { task . description }
333
- </ p >
375
+ < div >
376
+ { /* Task Statistics */ }
377
+ < div className = "grid grid-cols-1 md:grid-cols-3 gap-4 mb-6" >
378
+ < div className = "bg-white p-4 rounded-lg shadow-sm border" >
379
+ < div className = "text-2xl font-bold text-gray-900" > { tasks . length } </ div >
380
+ < div className = "text-sm text-gray-600" > Total Tasks</ div >
381
+ </ div >
382
+ < div className = "bg-white p-4 rounded-lg shadow-sm border" >
383
+ < div className = "text-2xl font-bold text-green-600" >
384
+ { tasks . filter ( t => t . completed ) . length }
385
+ </ div >
386
+ < div className = "text-sm text-gray-600" > Completed</ div >
387
+ </ div >
388
+ < div className = "bg-white p-4 rounded-lg shadow-sm border" >
389
+ < div className = "text-2xl font-bold text-orange-600" >
390
+ { tasks . filter ( t => ! t . completed ) . length }
391
+ </ div >
392
+ < div className = "text-sm text-gray-600" > Pending</ div >
393
+ </ div >
394
+ </ div >
395
+
396
+ { /* Task List */ }
397
+ < div className = "space-y-4" >
398
+ { tasks . map ( ( task ) => (
399
+ < div
400
+ key = { task . id }
401
+ className = { `bg-white rounded-lg shadow-sm border transition-all duration-200 ${
402
+ task . completed ? "opacity-75" : "hover:shadow-md"
403
+ } `}
404
+ >
405
+ < div className = "p-6" >
406
+ < div className = "flex items-start justify-between mb-3" >
407
+ < div className = "flex items-start gap-3 flex-1" >
408
+ < input
409
+ type = "checkbox"
410
+ checked = { task . completed }
411
+ onChange = { ( ) => handleToggleComplete ( task . id ) }
412
+ className = "mt-1 h-4 w-4 text-violet-600 rounded border-gray-300 focus:ring-violet-500"
413
+ />
414
+ < div className = "flex-1" >
415
+ < h3
416
+ className = { `text-lg font-medium mb-1 ${
417
+ task . completed ? "line-through text-gray-500" : "text-gray-900"
418
+ } `}
419
+ >
420
+ { task . title }
421
+ </ h3 >
422
+ < p
423
+ className = { `text-sm mb-3 ${
424
+ task . completed ? "text-gray-400" : "text-gray-600"
425
+ } `}
426
+ >
427
+ { task . description }
428
+ </ p >
429
+ </ div >
334
430
</ div >
431
+ < span
432
+ className = { `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${ getPriorityColor ( task . priority ) } ` }
433
+ >
434
+ { task . priority === "high"
435
+ ? "High"
436
+ : task . priority === "medium"
437
+ ? "Med"
438
+ : task . priority === "low"
439
+ ? "Low"
440
+ : "-" }
441
+ </ span >
335
442
</ div >
336
- < span
337
- className = { `inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${ getPriorityColor ( task . priority ) } ` }
338
- >
339
- { task . priority === "high"
340
- ? "High"
341
- : task . priority === "medium"
342
- ? "Med"
343
- : task . priority === "low"
344
- ? "Low"
345
- : "-" }
346
- </ span >
347
- </ div >
348
443
349
- < div className = "flex items-center justify-between text-sm text-gray-500" >
350
- < div className = "flex items-center gap-4" >
351
- < div className = "flex items-center gap-1" >
352
- < Clock size = { 14 } />
353
- < span > Created: { formatDate ( task . createdAt ) } </ span >
354
- </ div >
355
- { task . dueDate && (
444
+ < div className = "flex items-center justify-between text-sm text-gray-500" >
445
+ < div className = "flex items-center gap-4" >
356
446
< div className = "flex items-center gap-1" >
357
- < Calendar size = { 14 } />
358
- < span > Due: { formatDate ( task . dueDate ) } </ span >
447
+ < Clock size = { 14 } />
448
+ < span > Created: { formatDate ( task . createdAt ) } </ span >
449
+ </ div >
450
+ { task . dueDate && (
451
+ < div className = "flex items-center gap-1" >
452
+ < Calendar size = { 14 } />
453
+ < span > Due: { formatDate ( task . dueDate ) } </ span >
454
+ </ div >
455
+ ) }
456
+ < span className = "px-2 py-1 bg-gray-100 rounded text-xs" > { task . type } </ span >
457
+ </ div >
458
+
459
+ { ! task . completed && (
460
+ < div className = "flex gap-2" >
461
+ < Button
462
+ size = "sm"
463
+ variant = "outline"
464
+ onClick = { ( ) => handleAddToCalendar ( task , "google" ) }
465
+ className = "text-xs"
466
+ >
467
+ Google Calendar
468
+ </ Button >
469
+ < Button
470
+ size = "sm"
471
+ variant = "outline"
472
+ onClick = { ( ) => handleAddToCalendar ( task , "ios" ) }
473
+ className = "text-xs"
474
+ >
475
+ iOS Calendar
476
+ </ Button >
477
+ < Button
478
+ size = "sm"
479
+ variant = "outline"
480
+ onClick = { ( ) => handleDeleteTask ( task . id ) }
481
+ className = "text-xs text-red-600 hover:text-red-700 hover:border-red-300"
482
+ >
483
+ < Trash2 size = { 12 } className = "mr-1" />
484
+ Delete
485
+ </ Button >
359
486
</ div >
360
487
) }
361
- < span className = "px-2 py-1 bg-gray-100 rounded text-xs" > { task . type } </ span >
362
488
</ div >
363
-
364
- { ! task . completed && (
365
- < div className = "flex gap-2" >
366
- < Button
367
- size = "sm"
368
- variant = "outline"
369
- onClick = { ( ) => handleAddToCalendar ( task , "google" ) }
370
- className = "text-xs"
371
- >
372
- Google Calendar
373
- </ Button >
374
- < Button
375
- size = "sm"
376
- variant = "outline"
377
- onClick = { ( ) => handleAddToCalendar ( task , "ios" ) }
378
- className = "text-xs"
379
- >
380
- iOS Calendar
381
- </ Button >
382
- </ div >
383
- ) }
384
489
</ div >
385
490
</ div >
386
- </ div >
387
- ) ) }
491
+ ) ) }
492
+ </ div >
388
493
</ div >
389
494
) }
390
495
</ div >
0 commit comments