diff --git a/backend/__pycache__/main.cpython-312.pyc b/backend/__pycache__/main.cpython-312.pyc index 4903f4e..8743bd6 100644 Binary files a/backend/__pycache__/main.cpython-312.pyc and b/backend/__pycache__/main.cpython-312.pyc differ diff --git a/backend/alembic/__pycache__/env.cpython-312.pyc b/backend/alembic/__pycache__/env.cpython-312.pyc index 23a325f..6c6a35e 100644 Binary files a/backend/alembic/__pycache__/env.cpython-312.pyc and b/backend/alembic/__pycache__/env.cpython-312.pyc differ diff --git a/backend/alembic/env.py b/backend/alembic/env.py index ae86d51..4770a9c 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -19,6 +19,7 @@ from core.database import Base # Import your Base from modules.auth.models import User # Example: Import User model from modules.calendar.models import CalendarEvent # Example: Import CalendarEvent model from modules.nlp.models import ChatMessage # Import the new ChatMessage model +from modules.todo.models import Todo # Import the new Todo model # Add imports for any other models you have # ---------------------------- diff --git a/backend/alembic/versions/9a82960db482_add_todo_table.py b/backend/alembic/versions/9a82960db482_add_todo_table.py new file mode 100644 index 0000000..fea09ee --- /dev/null +++ b/backend/alembic/versions/9a82960db482_add_todo_table.py @@ -0,0 +1,32 @@ +"""Add todo table + +Revision ID: 9a82960db482 +Revises: 69069d6184b3 +Create Date: 2025-04-21 20:33:27.028529 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9a82960db482' +down_revision: Union[str, None] = '69069d6184b3' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/backend/alembic/versions/__pycache__/9a82960db482_add_todo_table.cpython-312.pyc b/backend/alembic/versions/__pycache__/9a82960db482_add_todo_table.cpython-312.pyc new file mode 100644 index 0000000..50b8b35 Binary files /dev/null and b/backend/alembic/versions/__pycache__/9a82960db482_add_todo_table.cpython-312.pyc differ diff --git a/backend/main.py b/backend/main.py index 651ccd1..29719b3 100644 --- a/backend/main.py +++ b/backend/main.py @@ -11,6 +11,7 @@ import logging # import all models to ensure they are registered before create_all from modules.calendar.models import CalendarEvent from modules.auth.models import User +from modules.todo.models import Todo # Import the new Todo model logging.getLogger('passlib').setLevel(logging.ERROR) # fix bc package logging is broken diff --git a/backend/modules/__init__.py b/backend/modules/__init__.py index 154386d..73bfd78 100644 --- a/backend/modules/__init__.py +++ b/backend/modules/__init__.py @@ -4,6 +4,7 @@ from .auth.api import router as auth_router from .user.api import router as user_router from .calendar.api import router as calendar_router from .nlp.api import router as nlp_router +from .todo.api import router as todo_router router = APIRouter(prefix="/api") router.include_router(admin_router) @@ -11,3 +12,4 @@ router.include_router(auth_router) router.include_router(user_router) router.include_router(calendar_router) router.include_router(nlp_router) +router.include_router(todo_router) diff --git a/backend/modules/__pycache__/__init__.cpython-312.pyc b/backend/modules/__pycache__/__init__.cpython-312.pyc index 8e2100d..f3f44a1 100644 Binary files a/backend/modules/__pycache__/__init__.cpython-312.pyc and b/backend/modules/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/modules/nlp/__pycache__/api.cpython-312.pyc b/backend/modules/nlp/__pycache__/api.cpython-312.pyc index 1d6c0c4..054df8c 100644 Binary files a/backend/modules/nlp/__pycache__/api.cpython-312.pyc and b/backend/modules/nlp/__pycache__/api.cpython-312.pyc differ diff --git a/backend/modules/nlp/__pycache__/service.cpython-312.pyc b/backend/modules/nlp/__pycache__/service.cpython-312.pyc index c49445c..bd3f9cb 100644 Binary files a/backend/modules/nlp/__pycache__/service.cpython-312.pyc and b/backend/modules/nlp/__pycache__/service.cpython-312.pyc differ diff --git a/backend/modules/nlp/api.py b/backend/modules/nlp/api.py index de262b0..8f0587c 100644 --- a/backend/modules/nlp/api.py +++ b/backend/modules/nlp/api.py @@ -15,6 +15,10 @@ from modules.nlp.models import ChatMessage # Import ChatMessage model from modules.calendar.service import create_calendar_event, get_calendar_events, update_calendar_event, delete_calendar_event from modules.calendar.models import CalendarEvent from modules.calendar.schemas import CalendarEventCreate, CalendarEventUpdate +# Import TODO services, schemas, and model +from modules.todo import service as todo_service +from modules.todo.models import Todo +from modules.todo.schemas import TodoCreate, TodoUpdate router = APIRouter(prefix="/nlp", tags=["nlp"]) @@ -31,6 +35,18 @@ def format_calendar_events(events: List[CalendarEvent]) -> List[str]: formatted.append(f"- {title} ({start_str}{' - ' + end_str if end_str else ''})") return formatted +# Helper to format TODO items (expects list of Todo models) +def format_todos(todos: List[Todo]) -> List[str]: + if not todos: + return ["Your TODO list is empty."] + formatted = ["Here is your TODO list:"] + for todo in todos: + status = "[X]" if todo.complete else "[ ]" + date_str = f" (Due: {todo.date.strftime('%Y-%m-%d')})" if todo.date else "" + remind_str = " (Reminder)" if todo.remind else "" + formatted.append(f"- {status} {todo.task}{date_str}{remind_str} (ID: {todo.id})") + return formatted + # Update the response model for the endpoint @router.post("/process-command", response_model=ProcessCommandResponse) def process_command(request_data: ProcessCommandRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db)): @@ -129,6 +145,60 @@ def process_command(request_data: ProcessCommandRequest, current_user: User = De # --------------------------------- return ProcessCommandResponse(responses=responses) + # --- Add TODO Cases --- + case "get_todos": + todos: List[Todo] = todo_service.get_todos(db, user=current_user, **params) + formatted_responses = format_todos(todos) + responses.extend(formatted_responses) + # --- Save Additional AI Responses --- + for resp in formatted_responses: + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=resp) + # ---------------------------------- + return ProcessCommandResponse(responses=responses) + + case "add_todo": + todo_data = TodoCreate(**params) + created_todo = todo_service.create_todo(db, todo=todo_data, user=current_user) + add_response = f"Added TODO: '{created_todo.task}' (ID: {created_todo.id})." + responses.append(add_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=add_response) + # --------------------------------- + return ProcessCommandResponse(responses=responses) + + case "update_todo": + todo_id = params.pop('todo_id', None) + if todo_id is None: + error_msg = "TODO ID is required for update." + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=error_msg) + raise HTTPException(status_code=400, detail=error_msg) + todo_data = TodoUpdate(**params) + updated_todo = todo_service.update_todo(db, todo_id=todo_id, todo_update=todo_data, user=current_user) + update_response = f"Updated TODO ID {updated_todo.id}: '{updated_todo.task}'." + if 'complete' in params: + status = "complete" if params['complete'] else "incomplete" + update_response += f" Marked as {status}." + responses.append(update_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=update_response) + # --------------------------------- + return ProcessCommandResponse(responses=responses) + + case "delete_todo": + todo_id = params.get('todo_id') + if todo_id is None: + error_msg = "TODO ID is required for delete." + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=error_msg) + raise HTTPException(status_code=400, detail=error_msg) + deleted_todo = todo_service.delete_todo(db, todo_id=todo_id, user=current_user) + delete_response = f"Deleted TODO ID {deleted_todo.id}: '{deleted_todo.task}'." + responses.append(delete_response) + # --- Save Additional AI Response --- + save_chat_message(db, user_id=current_user.id, sender=MessageSender.AI, text=delete_response) + # --------------------------------- + return ProcessCommandResponse(responses=responses) + # --- End TODO Cases --- + case _: print(f"Warning: Unhandled intent '{intent}' reached api.py match statement.") # The initial response_text was already saved diff --git a/backend/modules/nlp/service.py b/backend/modules/nlp/service.py index d2722be..f1e2046 100644 --- a/backend/modules/nlp/service.py +++ b/backend/modules/nlp/service.py @@ -9,10 +9,9 @@ from typing import List # Import List # Import the new model and Enum from .models import ChatMessage, MessageSender -# from core.config import settings +from core.config import settings -# client = genai.Client(api_key=settings.GOOGLE_API_KEY) -client = genai.Client(api_key="AIzaSyBrte_mETZJce8qE6cRTSz_fHOjdjlShBk") +client = genai.Client(api_key=settings.GOOGLE_API_KEY) ### Base prompt for MAIA, used for inital user requests SYSTEM_PROMPT = """ @@ -24,10 +23,14 @@ Available functions/intents: 3. add_calendar_event(title: str, description: str, start: datetime, end: Optional[datetime], location: str): Add a new event. 4. update_calendar_event(event_id: int, title: Optional[str], description: Optional[str], start: Optional[datetime], end: Optional[datetime], location: Optional[str]): Update an existing event. Requires event_id. 5. delete_calendar_event(event_id: int): Delete an event. Requires event_id. -6. clarification_needed(request: str): Use this if the user's request is ambiguous or lacks necessary information (like event_id for update/delete). The original user request should be passed in the 'request' parameter. +6. get_todos(): Retrieve the user's TODO list. +7. add_todo(task: str, date: Optional[datetime], remind: Optional[bool]): Add a new task to the user's TODO list. +8. update_todo(todo_id: int, task: Optional[str], date: Optional[datetime], remind: Optional[bool], complete: Optional[bool]): Update an existing TODO item. Requires todo_id. +9. delete_todo(todo_id: int): Delete a TODO item. Requires todo_id. +10. clarification_needed(request: str): Use this if the user's request is ambiguous or lacks necessary information (like event_id or todo_id for update/delete). The original user request should be passed in the 'request' parameter. **IMPORTANT:** Respond ONLY with JSON containing BOTH "intent" and "params", AND a "response_text" field. -- "response_text" should be a friendly, user-facing message confirming the action taken, providing the answer, or asking for clarification. +- "response_text" should be a friendly, user-facing message confirming the action taken, providing the answer, asking for clarification OR can be empty if the query does not require a response to the user. Examples: @@ -38,7 +41,7 @@ MAIA: "params": { "title": "Meeting", "description": "Project X", - "start": "2025-04-19 15:00:00.000000+00:00", + "start": "2025-04-22 15:00:00.000000+00:00", "end": null, "location": null }, @@ -65,6 +68,47 @@ MAIA: "response_text": "Okay, I can help with that. Could you please provide the ID or more specific details about the 'team sync' event you want me to delete?" } +User: Add 'Buy groceries' to my todo list +MAIA: +{ + "intent": "add_todo", + "params": { + "task": "Buy groceries", + "date": null, + "remind": false + }, + "response_text": "I've added 'Buy groceries' to your TODO list." +} + +User: Show me my todos +MAIA: +{ + "intent": "get_todos", + "params": {}, + "response_text": "Okay, fetching your TODO list now." +} + +User: Mark task 15 as complete +MAIA: +{ + "intent": "update_todo", + "params": { + "todo_id": 15, + "complete": true + }, + "response_text": "Got it, I've marked task 15 as complete." +} + +User: Delete task 2 +MAIA: +{ + "intent": "delete_todo", + "params": { + "todo_id": 2 + }, + "response_text": "Okay, I've deleted task 2 from your list." +} + The datetime right now is """+str(datetime.now(timezone.utc))+""". """ diff --git a/backend/modules/todo/__init__.py b/backend/modules/todo/__init__.py new file mode 100644 index 0000000..d6fc651 --- /dev/null +++ b/backend/modules/todo/__init__.py @@ -0,0 +1,2 @@ +# backend/modules/todo/__init__.py +# This file makes the 'todo' directory a Python package. diff --git a/backend/modules/todo/__pycache__/__init__.cpython-312.pyc b/backend/modules/todo/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..1a4f3d3 Binary files /dev/null and b/backend/modules/todo/__pycache__/__init__.cpython-312.pyc differ diff --git a/backend/modules/todo/__pycache__/api.cpython-312.pyc b/backend/modules/todo/__pycache__/api.cpython-312.pyc new file mode 100644 index 0000000..0cc2bdc Binary files /dev/null and b/backend/modules/todo/__pycache__/api.cpython-312.pyc differ diff --git a/backend/modules/todo/__pycache__/models.cpython-312.pyc b/backend/modules/todo/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..32eb8e9 Binary files /dev/null and b/backend/modules/todo/__pycache__/models.cpython-312.pyc differ diff --git a/backend/modules/todo/__pycache__/schemas.cpython-312.pyc b/backend/modules/todo/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000..53de287 Binary files /dev/null and b/backend/modules/todo/__pycache__/schemas.cpython-312.pyc differ diff --git a/backend/modules/todo/__pycache__/service.cpython-312.pyc b/backend/modules/todo/__pycache__/service.cpython-312.pyc new file mode 100644 index 0000000..b774be9 Binary files /dev/null and b/backend/modules/todo/__pycache__/service.cpython-312.pyc differ diff --git a/backend/modules/todo/api.py b/backend/modules/todo/api.py new file mode 100644 index 0000000..ae57ceb --- /dev/null +++ b/backend/modules/todo/api.py @@ -0,0 +1,62 @@ +# backend/modules/todo/api.py +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List + +from . import service, schemas +from core.database import get_db +from modules.auth.dependencies import get_current_user # Corrected import +from modules.auth.models import User # Assuming User model is in auth.models + +router = APIRouter( + prefix="/todos", + tags=["todos"], + dependencies=[Depends(get_current_user)], # Corrected dependency + responses={404: {"description": "Not found"}}, +) + +@router.post("/", response_model=schemas.Todo, status_code=status.HTTP_201_CREATED) +def create_todo_endpoint( + todo: schemas.TodoCreate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) # Corrected dependency +): + return service.create_todo(db=db, todo=todo, user=current_user) + +@router.get("/", response_model=List[schemas.Todo]) +def read_todos_endpoint( + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) # Corrected dependency +): + todos = service.get_todos(db=db, user=current_user, skip=skip, limit=limit) + return todos + +@router.get("/{todo_id}", response_model=schemas.Todo) +def read_todo_endpoint( + todo_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) # Corrected dependency +): + db_todo = service.get_todo(db=db, todo_id=todo_id, user=current_user) + if db_todo is None: + raise HTTPException(status_code=404, detail="Todo not found") + return db_todo + +@router.put("/{todo_id}", response_model=schemas.Todo) +def update_todo_endpoint( + todo_id: int, + todo_update: schemas.TodoUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) # Corrected dependency +): + return service.update_todo(db=db, todo_id=todo_id, todo_update=todo_update, user=current_user) + +@router.delete("/{todo_id}", response_model=schemas.Todo) +def delete_todo_endpoint( + todo_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(get_current_user) # Corrected dependency +): + return service.delete_todo(db=db, todo_id=todo_id, user=current_user) diff --git a/backend/modules/todo/models.py b/backend/modules/todo/models.py new file mode 100644 index 0000000..2b09fdd --- /dev/null +++ b/backend/modules/todo/models.py @@ -0,0 +1,17 @@ +# backend/modules/todo/models.py +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey +from sqlalchemy.orm import relationship +from core.database import Base +import datetime + +class Todo(Base): + __tablename__ = "todos" + + id = Column(Integer, primary_key=True, index=True) + task = Column(String, index=True, nullable=False) + date = Column(DateTime, nullable=True) + remind = Column(Boolean, default=False) + complete = Column(Boolean, default=False) + owner_id = Column(Integer, ForeignKey("users.id")) + + owner = relationship("User") # Add relationship if needed, assuming User model exists in auth.models diff --git a/backend/modules/todo/schemas.py b/backend/modules/todo/schemas.py new file mode 100644 index 0000000..a857393 --- /dev/null +++ b/backend/modules/todo/schemas.py @@ -0,0 +1,26 @@ +# backend/modules/todo/schemas.py +from pydantic import BaseModel +from typing import Optional +import datetime + +class TodoBase(BaseModel): + task: str + date: Optional[datetime.datetime] = None + remind: bool = False + complete: bool = False + +class TodoCreate(TodoBase): + pass + +class TodoUpdate(BaseModel): + task: Optional[str] = None + date: Optional[datetime.datetime] = None + remind: Optional[bool] = None + complete: Optional[bool] = None + +class Todo(TodoBase): + id: int + owner_id: int + + class Config: + from_attributes = True diff --git a/backend/modules/todo/service.py b/backend/modules/todo/service.py new file mode 100644 index 0000000..bff56f6 --- /dev/null +++ b/backend/modules/todo/service.py @@ -0,0 +1,36 @@ +# backend/modules/todo/service.py +from sqlalchemy.orm import Session +from . import models, schemas +from modules.auth.models import User # Assuming User model is in auth.models +from fastapi import HTTPException, status + +def create_todo(db: Session, todo: schemas.TodoCreate, user: User): + db_todo = models.Todo(**todo.dict(), owner_id=user.id) + db.add(db_todo) + db.commit() + db.refresh(db_todo) + return db_todo + +def get_todos(db: Session, user: User, skip: int = 0, limit: int = 100): + return db.query(models.Todo).filter(models.Todo.owner_id == user.id).offset(skip).limit(limit).all() + +def get_todo(db: Session, todo_id: int, user: User): + db_todo = db.query(models.Todo).filter(models.Todo.id == todo_id, models.Todo.owner_id == user.id).first() + if db_todo is None: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Todo not found") + return db_todo + +def update_todo(db: Session, todo_id: int, todo_update: schemas.TodoUpdate, user: User): + db_todo = get_todo(db=db, todo_id=todo_id, user=user) # Reuse get_todo to check ownership and existence + update_data = todo_update.dict(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_todo, key, value) + db.commit() + db.refresh(db_todo) + return db_todo + +def delete_todo(db: Session, todo_id: int, user: User): + db_todo = get_todo(db=db, todo_id=todo_id, user=user) # Reuse get_todo to check ownership and existence + db.delete(db_todo) + db.commit() + return db_todo diff --git a/interfaces/nativeapp/src/api/client.ts b/interfaces/nativeapp/src/api/client.ts index 5999b98..616f07c 100644 --- a/interfaces/nativeapp/src/api/client.ts +++ b/interfaces/nativeapp/src/api/client.ts @@ -108,11 +108,9 @@ apiClient.interceptors.response.use( } console.log('[API Client] Attempting token refresh...'); - // Send refresh token in the body, remove withCredentials const refreshResponse = await apiClient.post('/auth/refresh', { refresh_token: storedRefreshToken }, // Send token in body { - // No withCredentials needed headers: { 'Content-Type': 'application/json' }, } ); diff --git a/interfaces/nativeapp/src/api/todo.ts b/interfaces/nativeapp/src/api/todo.ts new file mode 100644 index 0000000..0ba6520 --- /dev/null +++ b/interfaces/nativeapp/src/api/todo.ts @@ -0,0 +1,58 @@ +// interfaces/nativeapp/src/api/todo.ts +import apiClient from './client'; +import { Todo, TodoCreate, TodoUpdate } from '../types/todo'; + +export const getTodos = async (skip: number = 0, limit: number = 100): Promise => { + try { + const response = await apiClient.get('/todos/', { params: { skip, limit } }); + console.log("[TODO] Got todos:", response.data); + return response.data; + } catch (error) { + console.error("Error fetching todos", error); + throw error; + } +}; + +export const getTodoById = async (todo_id: number): Promise => { + try { + const response = await apiClient.get(`/todos/${todo_id}`); + console.log("[TODO] Got todo:", response.data); + return response.data; + } catch (error) { + console.error(`Error fetching todo ${todo_id}`, error); + throw error; + } +}; + +export const createTodo = async (todo: TodoCreate): Promise => { + try { + const response = await apiClient.post('/todos/', todo); + console.log("[TODO] Created todo:", response.data); + return response.data; + } catch (error) { + console.error("Error creating todo", error); + throw error; + } +}; + +export const updateTodo = async (todo_id: number, todo: TodoUpdate): Promise => { + try { + const response = await apiClient.put(`/todos/${todo_id}`, todo); + console.log("[TODO] Updated todo:", response.data); + return response.data; + } catch (error) { + console.error(`Error updating todo ${todo_id}`, error); + throw error; + } +}; + +export const deleteTodo = async (todo_id: number): Promise => { // Backend returns the deleted item + try { + const response = await apiClient.delete(`/todos/${todo_id}`); + console.log("[TODO] Deleted todo:", response.data); + return response.data; // Return the data which is the deleted todo object + } catch (error) { + console.error(`Error deleting todo ${todo_id}`, error); + throw error; + } +}; diff --git a/interfaces/nativeapp/src/screens/DashboardScreen.tsx b/interfaces/nativeapp/src/screens/DashboardScreen.tsx index e271202..806beaf 100644 --- a/interfaces/nativeapp/src/screens/DashboardScreen.tsx +++ b/interfaces/nativeapp/src/screens/DashboardScreen.tsx @@ -1,20 +1,220 @@ // src/screens/DashboardScreen.tsx -import React, { useState, useEffect } from 'react'; // Added useEffect -import { View, StyleSheet, ScrollView } from 'react-native'; // Added ScrollView -import { Text, TextInput, Button, useTheme, Card, List, Divider } from 'react-native-paper'; // Added Card, List, Divider +import React, { useState, useEffect, useCallback } from 'react'; // Added useCallback +import { View, StyleSheet, ScrollView, TouchableOpacity } from 'react-native'; // Added TouchableOpacity +import { Text, TextInput, Button, useTheme, Card, List, Divider, Checkbox, IconButton, ActivityIndicator } from 'react-native-paper'; // Added Checkbox, IconButton, ActivityIndicator import { useNavigation } from '@react-navigation/native'; import { StackNavigationProp } from '@react-navigation/stack'; import { format, addDays, startOfDay, isSameDay, parseISO, endOfDay } from 'date-fns'; // Added date-fns imports import { getCalendarEvents } from '../api/calendar'; import { CalendarEvent } from '../types/calendar'; +import { Todo, TodoCreate, TodoUpdate } from '../types/todo'; // Import TODO types +import { getTodos, createTodo, updateTodo, deleteTodo } from '../api/todo'; // Import TODO API functions -// Placeholder for the TODO component -const TodoComponent = () => ( - - TODO Component Placeholder - -); +// --- TODO Component Implementation --- +const TodoComponent = () => { + const theme = useTheme(); + const [todos, setTodos] = useState([]); + const [newTask, setNewTask] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const fetchTodos = useCallback(async () => { + setLoading(true); + setError(null); + try { + const fetchedTodos = await getTodos(); + // Add explicit types for sort parameters + setTodos(fetchedTodos.sort((a: Todo, b: Todo) => a.id - b.id)); + } catch (err) { + console.error("Failed to fetch todos:", err); + setError("Failed to load TODOs."); + } finally { + setLoading(false); + } + }, []); + + useEffect(() => { + fetchTodos(); + }, [fetchTodos]); + + const handleAddTask = async () => { + const trimmedTask = newTask.trim(); + if (!trimmedTask) return; + + const newTodoData: TodoCreate = { task: trimmedTask }; + try { + const createdTodo = await createTodo(newTodoData); + setTodos(prevTodos => [...prevTodos, createdTodo]); + setNewTask(''); // Clear input + } catch (err) { + console.error("Failed to add todo:", err); + setError("Failed to add TODO."); // Show error feedback + } + }; + + const handleToggleComplete = async (todo: Todo) => { + const updatedTodoData: TodoUpdate = { complete: !todo.complete }; + try { + const updatedTodo = await updateTodo(todo.id, updatedTodoData); + setTodos(prevTodos => + prevTodos.map(t => (t.id === todo.id ? updatedTodo : t)) + ); + } catch (err) { + console.error("Failed to update todo:", err); + setError("Failed to update TODO status."); + } + }; + + const handleDeleteTask = async (id: number) => { + try { + await deleteTodo(id); + setTodos(prevTodos => prevTodos.filter(t => t.id !== id)); + } catch (err) { + console.error("Failed to delete todo:", err); + setError("Failed to delete TODO."); + } + }; + + const styles = StyleSheet.create({ + card: { + marginVertical: 8, + backgroundColor: theme.colors.surface, // Use surface color for card background + }, + cardTitle: { + fontSize: 18, + fontWeight: 'bold', + paddingLeft: 16, + paddingTop: 12, + paddingBottom: 8, + color: theme.colors.primary, + }, + inputContainer: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 16, + paddingBottom: 12, + }, + textInput: { + flex: 1, + marginRight: 8, + backgroundColor: theme.colors.background, // Match background + }, + listItem: { + paddingVertical: 0, // Reduced padding + paddingLeft: 8, // Adjust left padding + backgroundColor: theme.colors.surface, // Ensure item background matches card + minHeight: 48, + }, + listCheckboxContainer: { + justifyContent: 'center', + height: '100%', + marginRight: 8, + }, + listItemContent: { + marginLeft: -8, // Counteract default List.Item padding if needed + }, + taskText: { + fontSize: 15, + color: theme.colors.onSurface, + }, + completedTaskText: { + textDecorationLine: 'line-through', + color: theme.colors.onSurfaceDisabled, + }, + deleteButton: { + marginRight: -8, // Align delete button better + }, + loadingContainer: { + padding: 20, + alignItems: 'center', + justifyContent: 'center', + }, + errorText: { + paddingHorizontal: 16, + paddingBottom: 12, + color: theme.colors.error, + }, + divider: { + marginHorizontal: 16, + } + }); + + return ( + + + TODO List + + {/* Add New Task Input */} + + + + + + {/* Loading Indicator */} + {loading && ( + + + + )} + + {/* Error Message */} + {error && !loading && {error}} + + {/* TODO List */} + {!loading && !error && todos.length === 0 && ( + No tasks yet. Add one above! + )} + {!loading && !error && todos.map((todo, index) => ( + + + {todo.task} + + } + titleNumberOfLines={2} // Allow wrapping + left={props => ( + + handleToggleComplete(todo)} + color={theme.colors.primary} + /> + + )} + right={props => ( + handleDeleteTask(todo.id)} + iconColor={theme.colors.error} + style={styles.deleteButton} + size={20} + /> + )} + contentStyle={styles.listItemContent} + /> + {index < todos.length - 1 && } + + ))} + + + ); +}; +// --- End TODO Component --- // --- Calendar Preview Component Implementation --- const CalendarPreview = () => { diff --git a/interfaces/nativeapp/src/types/todo.ts b/interfaces/nativeapp/src/types/todo.ts new file mode 100644 index 0000000..745b679 --- /dev/null +++ b/interfaces/nativeapp/src/types/todo.ts @@ -0,0 +1,23 @@ +// interfaces/nativeapp/src/types/todo.ts +export interface Todo { + id: number; + task: string; + date?: string | null; // Assuming date comes as ISO string or null + remind: boolean; + complete: boolean; + owner_id: number; +} + +export interface TodoCreate { + task: string; + date?: string | null; + remind?: boolean; + complete?: boolean; // Usually not set on creation, defaults to false +} + +export interface TodoUpdate { + task?: string; + date?: string | null; + remind?: boolean; + complete?: boolean; +}