diff --git a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart index c7e4495e..6449d389 100644 --- a/lib/app/modules/home/views/add_task_bottom_sheet_new.dart +++ b/lib/app/modules/home/views/add_task_bottom_sheet_new.dart @@ -321,7 +321,13 @@ class AddTaskBottomSheet extends StatelessWidget { due: getDueDate(homeController.selectedDates).toString(), end: '', modified: 'r', - tags: homeController.tags); + tags: homeController.tags, + start: '', + wait: '', + rtype: '', + recur: '', + depends: [], + annotations: []); await homeController.taskdb.insertTask(task); homeController.namecontroller.text = ''; homeController.due.value = null; diff --git a/lib/app/modules/home/views/home_page_app_bar.dart b/lib/app/modules/home/views/home_page_app_bar.dart index 52fade53..f4b22218 100644 --- a/lib/app/modules/home/views/home_page_app_bar.dart +++ b/lib/app/modules/home/views/home_page_app_bar.dart @@ -157,6 +157,7 @@ class HomePageAppBar extends StatelessWidget implements PreferredSizeWidget { .homePageFetchingTasks, false); } catch (e) { + debugPrint('Error refreshing tasks: $e'); ScaffoldMessenger.of(context) .hideCurrentSnackBar(); _showResultSnackBar( diff --git a/lib/app/modules/home/views/show_details.dart b/lib/app/modules/home/views/show_details.dart deleted file mode 100644 index e1a5bc44..00000000 --- a/lib/app/modules/home/views/show_details.dart +++ /dev/null @@ -1,527 +0,0 @@ -// ignore_for_file: deprecated_member_use, use_build_context_synchronously -import 'package:flutter/material.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:intl/intl.dart'; -import 'package:taskwarrior/app/utils/app_settings/app_settings.dart'; -import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart'; -import 'package:taskwarrior/app/utils/constants/utilites.dart'; -import 'package:taskwarrior/app/utils/themes/theme_extension.dart'; -import 'package:taskwarrior/app/utils/language/sentence_manager.dart'; -import 'package:taskwarrior/app/v3/db/task_database.dart'; -import 'package:taskwarrior/app/v3/models/annotation.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; // Ensure this path is correct for your new model -// import 'package:taskwarrior/app/v3/net/modify.dart'; - -class TaskDetails extends StatefulWidget { - final TaskForC task; - const TaskDetails({super.key, required this.task}); - - @override - State createState() => _TaskDetailsState(); -} - -class _TaskDetailsState extends State { - late String description; - late String project; - late String status; - late String priority; - late String due; - late String start; - late String wait; - late List tags; - late List depends; - late String rtype; - late String recur; - late List annotations; - - late TaskDatabase taskDatabase; - - bool hasChanges = false; - - @override - void initState() { - super.initState(); - description = widget.task.description; - project = widget.task.project ?? '-'; - status = widget.task.status; - priority = widget.task.priority ?? '-'; - due = widget.task.due ?? '-'; - start = widget.task.start ?? '-'; - wait = widget.task.wait ?? '-'; - tags = widget.task.tags ?? []; - depends = widget.task.depends ?? []; - rtype = widget.task.rtype ?? '-'; - recur = widget.task.recur ?? '-'; - annotations = widget.task.annotations ?? []; - - due = _buildDate(due); // Format the date for display - start = _buildDate(start); - wait = _buildDate(wait); - - setState(() { - taskDatabase = TaskDatabase(); - taskDatabase.open(); - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - // This part seems redundant if taskDatabase.open() is already in initState - // and ideally, the database connection should be managed more robustly - // (e.g., singleton, provider, or passed down). - // However, keeping it as per original logic, but be aware of potential multiple openings. - taskDatabase = TaskDatabase(); - taskDatabase.open(); - } - - @override - Widget build(BuildContext context) { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return WillPopScope( - onWillPop: () async { - if (hasChanges) { - final action = await _showUnsavedChangesDialog(context); - if (action == UnsavedChangesAction.cancel) { - return Future.value(false); - } else if (action == UnsavedChangesAction.save) { - await _saveTask(); - } - } - return Future.value(true); - }, - child: Scaffold( - backgroundColor: tColors.primaryBackgroundColor, - appBar: AppBar( - foregroundColor: TaskWarriorColors.lightGrey, - backgroundColor: TaskWarriorColors.kprimaryBackgroundColor, - title: Text( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.task}: ${widget.task.description}', - style: GoogleFonts.poppins(color: TaskWarriorColors.white), - ), - ), - body: Padding( - padding: const EdgeInsets.all(16.0), - child: ListView( - children: [ - _buildEditableDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageDescription}:', - description, (value) { - setState(() { - description = value; - hasChanges = true; - }); - }), - _buildEditableDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.project}:', - project, (value) { - setState(() { - project = value; - hasChanges = true; - }); - }), - _buildSelectableDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageStatus}:', - status, - ['pending', 'completed'], (value) { - setState(() { - status = value; - hasChanges = true; - }); - }), - _buildSelectableDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPagePriority}:', - priority, - ['H', 'M', 'L', '-'], (value) { - setState(() { - priority = value; - hasChanges = true; - }); - }), - _buildDatePickerDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.homePageDue}:', - due, (value) { - setState(() { - due = value; - hasChanges = true; - }); - }), - _buildDatePickerDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageStart}:', - start, (value) { - setState(() { - start = value; - hasChanges = true; - }); - }), - _buildDatePickerDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageWait}:', - wait, (value) { - setState(() { - wait = value; - hasChanges = true; - }); - }), - _buildEditableDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageTags}:', - tags.join(', '), (value) { - setState(() { - tags = value.split(',').map((e) => e.trim()).toList(); - hasChanges = true; - }); - }), - _buildEditableDetail('Depends:', depends.join(', '), (value) { - setState(() { - depends = value.split(',').map((e) => e.trim()).toList(); - hasChanges = true; - }); - }), - _buildEditableDetail('Rtype:', rtype, (value) { - setState(() { - rtype = value; - hasChanges = true; - }); - }), - _buildEditableDetail('Recur:', recur, (value) { - setState(() { - recur = value; - hasChanges = true; - }); - }), - _buildDetail('UUID:', widget.task.uuid ?? '-'), - _buildDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageUrgency}:', - widget.task.urgency?.toStringAsFixed(2) ?? '-'), - _buildDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageEnd}:', - _buildDate(widget.task.end)), - _buildDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageEntry}:', - _buildDate(widget.task.entry)), - _buildDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageModified}:', - _buildDate(widget.task.modified)), - _buildDetail( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences}:', - annotations.isNotEmpty - ? annotations.map((e) => e.description).join('\n') - : '-'), - ], - ), - ), - floatingActionButton: hasChanges - ? FloatingActionButton( - onPressed: () async { - await _saveTask(); - }, - child: const Icon(Icons.save), - ) - : null, - ), - ); - } - - Widget _buildEditableDetail( - String label, String value, Function(String) onChanged) { - return InkWell( - onTap: () async { - final result = await _showEditDialog(context, label, value); - if (result != null) { - onChanged(result); - } - }, - child: _buildDetail(label, value), - ); - } - - Widget _buildSelectableDetail(String label, String value, - List options, Function(String) onChanged) { - return InkWell( - onTap: () async { - final result = await _showSelectDialog(context, label, value, options); - if (result != null) { - onChanged(result); - } - }, - child: _buildDetail(label, value), - ); - } - - Widget _buildDatePickerDetail( - String label, String value, Function(String) onChanged) { - return InkWell( - onTap: () async { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: value != '-' - ? DateTime.tryParse(value) ?? DateTime.now() - : DateTime.now(), - firstDate: DateTime(2000), - lastDate: DateTime(2101), - builder: (BuildContext context, Widget? child) { - return Theme( - data: Theme.of(context), - child: child!, - ); - }, - ); - if (pickedDate != null) { - final TimeOfDay? pickedTime = await showTimePicker( - context: context, - initialTime: TimeOfDay.fromDateTime(value != '-' - ? DateTime.tryParse(value) ?? DateTime.now() - : DateTime.now()), - ); - if (pickedTime != null) { - final DateTime fullDateTime = DateTime( - pickedDate.year, - pickedDate.month, - pickedDate.day, - pickedTime.hour, - pickedTime.minute); - onChanged(DateFormat('yyyy-MM-dd HH:mm:ss').format(fullDateTime)); - } else { - // If only date is picked, use current time - onChanged(DateFormat('yyyy-MM-dd HH:mm:ss').format(pickedDate)); - } - } - }, - child: _buildDetail(label, value), - ); - } - - Widget _buildDetail(String label, String value) { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return Container( - width: double.infinity, - decoration: BoxDecoration( - color: tColors.secondaryBackgroundColor, - borderRadius: BorderRadius.circular(8.0), - boxShadow: const [ - BoxShadow( - color: Colors.black12, - blurRadius: 4.0, - offset: Offset(0, 2), - ), - ], - ), - padding: const EdgeInsets.all(16.0), - margin: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 18, - color: tColors.primaryTextColor, - ), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - value, - style: TextStyle( - fontSize: 18, - color: tColors.primaryTextColor, - ), - ), - ), - ], - ), - ); - } - - Future _showEditDialog( - BuildContext context, String label, String initialValue) async { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - final TextEditingController controller = - TextEditingController(text: initialValue); - return await showDialog( - context: context, - builder: (context) { - return Utils.showAlertDialog( - title: Text( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.edit} $label', - style: TextStyle(color: tColors.primaryTextColor), - ), - content: TextField( - style: TextStyle(color: tColors.primaryTextColor), - controller: controller, - decoration: InputDecoration( - hintText: - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.enterNew} $label', - hintStyle: TextStyle(color: tColors.primaryTextColor), - ), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .cancel, - style: TextStyle(color: tColors.primaryTextColor), - ), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(controller.text); - }, - child: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .save, - style: TextStyle(color: tColors.primaryTextColor), - ), - ), - ], - ); - }, - ); - } - - Future _showSelectDialog(BuildContext context, String label, - String initialValue, List options) async { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return await showDialog( - context: context, - builder: (context) { - return Utils.showAlertDialog( - title: Text( - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.select} $label', - style: TextStyle(color: tColors.primaryTextColor), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: options.map((option) { - return RadioListTile( - title: Text( - option, - style: TextStyle(color: tColors.primaryTextColor), - ), - value: option, - groupValue: initialValue, - onChanged: (value) { - Navigator.of(context).pop(value); - }, - ); - }).toList(), - ), - ); - }, - ); - } - - String _buildDate(String? dateString) { - if (dateString == null || dateString.isEmpty || dateString == '-') { - return '-'; - } - - try { - DateTime parsedDate = DateTime.parse(dateString); - return DateFormat('yyyy-MM-dd HH:mm:ss').format(parsedDate); - } catch (e) { - debugPrint('Error parsing date: $dateString'); - return '-'; - } - } - - Future _showUnsavedChangesDialog( - BuildContext context) async { - TaskwarriorColorTheme tColors = - Theme.of(context).extension()!; - return showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return Utils.showAlertDialog( - title: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .unsavedChanges, - style: TextStyle(color: tColors.primaryTextColor), - ), - content: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .unsavedChangesWarning, - style: TextStyle(color: tColors.primaryTextColor), - ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(UnsavedChangesAction.cancel); - }, - child: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .cancel), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(UnsavedChangesAction.discard); - }, - child: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .dontSave), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(UnsavedChangesAction.save); - }, - child: Text( - SentenceManager(currentLanguage: AppSettings.selectedLanguage) - .sentences - .save), - ), - ], - ); - }, - ); - } - - Future _saveTask() async { - // Update the TaskForC object with the new values - // final updatedTask = TaskForC( - // id: widget.task.id, - // description: description, - // project: project == '-' ? null : project, - // status: status, - // uuid: widget.task.uuid, - // urgency: widget - // .task.urgency, // Urgency is typically calculated, not edited directly - // priority: priority == '-' ? null : priority, - // due: due == '-' ? null : due, - // start: start == '-' ? null : start, - // end: widget - // .task.end, // 'end' is usually set when completed, not edited directly - // entry: widget.task.entry, // 'entry' is static - // wait: wait == '-' ? null : wait, - // modified: DateFormat('yyyy-MM-dd HH:mm:ss') - // .format(DateTime.now()), // Update modified time - // tags: tags.isEmpty ? null : tags, - // depends: depends.isEmpty ? null : depends, - // rtype: rtype == '-' ? null : rtype, - // recur: recur == '-' ? null : recur, - // annotations: annotations.isEmpty ? null : annotations, - // ); - - setState(() { - hasChanges = false; - }); - // Assuming modifyTaskOnTaskwarrior takes the updated TaskForC object - // await modifyTaskOnTaskwarrior(updatedTask); - } -} - -enum UnsavedChangesAction { save, discard, cancel } diff --git a/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart index 7dd803b2..dfec84da 100644 --- a/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart +++ b/lib/app/modules/taskc_details/controllers/taskc_details_controller.dart @@ -32,6 +32,7 @@ class TaskcDetailsController extends GetxController { late RxString rtype; late RxString recur; late RxList annotations; + late RxList previousTags = [].obs; @override void onInit() { @@ -48,13 +49,16 @@ class TaskcDetailsController extends GetxController { status = task.status.obs; priority = (task.priority ?? '-').obs; due = formatDate(task.due).obs; - start = formatDate(task.start).obs; - wait = formatDate(task.wait).obs; - tags = (task.tags ?? []).obs; - depends = (task.depends ?? []).obs; - rtype = (task.rtype ?? '-').obs; - recur = (task.recur ?? '-').obs; - annotations = (task.annotations ?? []).obs; + start = "".obs; + wait = "".obs; + tags = initialTask.tags != null + ? initialTask.tags!.map((e) => e.toString()).toList().obs + : [].obs; + previousTags = tags.toList().obs; + depends = "".split(",").obs; + rtype = "".obs; + recur = "".obs; + annotations = [].obs; } String formatDate(String? dateString) { @@ -85,31 +89,38 @@ class TaskcDetailsController extends GetxController { } } + void processTagsLists() { + final itemsToMove = previousTags.toSet().difference(tags.toSet()); + tags.addAll(itemsToMove.map((item) => '-$item')); + previousTags.removeWhere((item) => itemsToMove.contains(item)); + } + Future saveTask() async { - final updatedTask = TaskForC( - id: initialTask.id, - description: description.value, - project: project.value == '-' ? null : project.value, - status: status.value, - uuid: initialTask.uuid, - urgency: initialTask.urgency, // Urgency is typically calculated - priority: priority.value == '-' ? null : priority.value, - due: due.value == '-' ? null : due.value, - start: start.value == '-' ? null : start.value, - end: initialTask.end, // 'end' is usually set when completed - entry: initialTask.entry, // 'entry' is static - wait: wait.value == '-' ? null : wait.value, - modified: DateFormat('yyyy-MM-dd HH:mm:ss') - .format(DateTime.now()), // Update modified time - tags: tags.isEmpty ? null : tags.toList(), - depends: depends.isEmpty ? null : depends.toList(), - rtype: rtype.value == '-' ? null : rtype.value, - recur: recur.value == '-' ? null : recur.value, - annotations: annotations.isEmpty ? null : annotations.toList(), + if (tags.length == 1 && tags[0] == "") { + tags.clear(); + } + await taskDatabase.saveEditedTaskInDB( + initialTask.uuid!, + description.string, + project.string, + status.string, + priority.string, + DateTime.parse(due.string).toIso8601String(), + tags.toList(), ); - await TaskDatabase().updateTask(updatedTask); hasChanges.value = false; - await modifyTaskOnTaskwarrior(updatedTask); + debugPrint('Task saved in local DB ${description.string}'); + processTagsLists(); + await modifyTaskOnTaskwarrior( + description.string, + project.string, + DateTime.parse(due.string).toIso8601String(), + priority.string, + status.string, + initialTask.uuid!, + initialTask.id.toString(), + tags.toList(), + ); } Future handleWillPop() async { diff --git a/lib/app/modules/taskc_details/views/taskc_details_view.dart b/lib/app/modules/taskc_details/views/taskc_details_view.dart index 7c65e854..75e0dad5 100644 --- a/lib/app/modules/taskc_details/views/taskc_details_view.dart +++ b/lib/app/modules/taskc_details/views/taskc_details_view.dart @@ -66,17 +66,15 @@ class TaskcDetailsView extends GetView { controller.due.value, () => controller.pickDateTime(controller.due), ), - _buildDatePickerDetail( + _buildDetail( context, '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageStart}:', controller.start.value, - () => controller.pickDateTime(controller.start), ), - _buildDatePickerDetail( + _buildDetail( context, '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageWait}:', controller.wait.value, - () => controller.pickDateTime(controller.wait), ), _buildEditableDetail( context, @@ -84,24 +82,15 @@ class TaskcDetailsView extends GetView { controller.tags.join(', '), (value) => controller.updateListField(controller.tags, value), ), - _buildEditableDetail( - context, - 'Depends:', - controller.depends.join(', '), - (value) => - controller.updateListField(controller.depends, value), - ), - _buildEditableDetail( + _buildDetail( context, 'Rtype:', controller.rtype.value, - (value) => controller.updateField(controller.rtype, value), ), - _buildEditableDetail( + _buildDetail( context, 'Recur:', controller.recur.value, - (value) => controller.updateField(controller.recur, value), ), _buildDetail( context, 'UUID:', controller.initialTask.uuid ?? '-'), @@ -125,15 +114,6 @@ class TaskcDetailsView extends GetView { '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageModified}:', controller.formatDate(controller.initialTask.modified), ), - _buildDetail( - context, - '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences}:', - controller.annotations.isNotEmpty - ? controller.annotations - .map((e) => e.description) - .join('\n') - : '-', - ), ], ), ), diff --git a/lib/app/v3/db/task_database.dart b/lib/app/v3/db/task_database.dart index b1da7997..6928da9a 100644 --- a/lib/app/v3/db/task_database.dart +++ b/lib/app/v3/db/task_database.dart @@ -1,8 +1,8 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'package:taskwarrior/app/v3/models/annotation.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; // Path to your updated TaskForC model +import 'package:taskwarrior/app/v3/models/task.dart'; class TaskDatabase { Database? _database; @@ -11,441 +11,190 @@ class TaskDatabase { var databasesPath = await getDatabasesPath(); String path = join(databasesPath, 'tasks.db'); - _database = await openDatabase( - path, - version: 2, - onCreate: (Database db, version) async { - // Create the main Tasks table - await db.execute(''' - CREATE TABLE Tasks ( - uuid TEXT PRIMARY KEY, - id INTEGER, - description TEXT, - project TEXT, - status TEXT, - urgency REAL, - priority TEXT, - due TEXT, - start TEXT, -- New column - end TEXT, - entry TEXT, - wait TEXT, -- New column - modified TEXT, - rtype TEXT, -- New column - recur TEXT -- New column - ) - '''); - - // Create TaskTags table - await db.execute(''' - CREATE TABLE TaskTags ( - task_uuid TEXT NOT NULL, - tag TEXT NOT NULL, - PRIMARY KEY (task_uuid, tag), - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE - ) - '''); - - // Create TaskDepends table - await db.execute(''' - CREATE TABLE TaskDepends ( - task_uuid TEXT NOT NULL, - depends_on_uuid TEXT NOT NULL, - PRIMARY KEY (task_uuid, depends_on_uuid), - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE, - FOREIGN KEY (depends_on_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE - ) - '''); - - // Create TaskAnnotations table - await db.execute(''' - CREATE TABLE TaskAnnotations ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - task_uuid TEXT NOT NULL, - entry TEXT NOT NULL, - description TEXT NOT NULL, - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE - ) - '''); - - debugPrint( - "All tables created: Tasks, TaskTags, TaskDepends, TaskAnnotations"); - }, - onUpgrade: (db, oldVersion, newVersion) async { - // This code runs when an existing database is opened with a higher version. - debugPrint("Database upgrade from version $oldVersion to $newVersion"); - - // Example: Migrating from version 1 to version 2 - if (oldVersion < 2) { - // 1. Add new columns to the 'Tasks' table - try { - await db.execute("ALTER TABLE Tasks ADD COLUMN start TEXT"); - } catch (e) { - debugPrint("Could not add 'start' column: $e"); - } - try { - await db.execute("ALTER TABLE Tasks ADD COLUMN wait TEXT"); - } catch (e) { - debugPrint("Could not add 'wait' column: $e"); - } - try { - await db.execute("ALTER TABLE Tasks ADD COLUMN rtype TEXT"); - } catch (e) { - debugPrint("Could not add 'rtype' column: $e"); - } - try { - await db.execute("ALTER TABLE Tasks ADD COLUMN recur TEXT"); - } catch (e) { - debugPrint("Could not add 'recur' column: $e"); - } - - // 2. Create the new relational tables - await db.execute(''' - CREATE TABLE TaskTags ( - task_uuid TEXT NOT NULL, - tag TEXT NOT NULL, - PRIMARY KEY (task_uuid, tag), - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE + _database = await openDatabase(path, version: 2, + onCreate: (Database db, version) async { + await db.execute(''' + CREATE TABLE Tasks ( + uuid TEXT PRIMARY KEY, + id INTEGER, + description TEXT, + project TEXT, + status TEXT, + urgency REAL, + priority TEXT, + due TEXT, + end TEXT, + entry TEXT, + modified TEXT, + start TEXT, + wait TEXT, + rtype TEXT, + recur TEXT ) '''); - await db.execute(''' - CREATE TABLE TaskDepends ( + await db.execute(''' + CREATE TABLE Tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, task_uuid TEXT NOT NULL, - depends_on_uuid TEXT NOT NULL, - PRIMARY KEY (task_uuid, depends_on_uuid), - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE, - FOREIGN KEY (depends_on_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE + task_id INTEGER NOT NULL, + FOREIGN KEY (task_uuid, task_id) + REFERENCES Tasks (uuid, id) + ON DELETE CASCADE + ON UPDATE CASCADE ) '''); - await db.execute(''' - CREATE TABLE TaskAnnotations ( + await db.execute(''' + CREATE TABLE Annotations ( id INTEGER PRIMARY KEY AUTOINCREMENT, - task_uuid TEXT NOT NULL, entry TEXT NOT NULL, description TEXT NOT NULL, - FOREIGN KEY (task_uuid) REFERENCES Tasks (uuid) ON DELETE CASCADE + task_uuid TEXT NOT NULL, + task_id INTEGER NOT NULL, + FOREIGN KEY (task_uuid, task_id) + REFERENCES Tasks (uuid, id) + ON DELETE CASCADE + ON UPDATE CASCADE ) '''); - debugPrint("New tables and columns added for upgrade to version 2."); - - // 3. Migrate data from the old 'tags' column in 'Tasks' to the new 'TaskTags' table - // This is crucial if your old schema stored tags as a space-separated string. - try { - final List> oldTasksWithTags = - await db.query('Tasks', - columns: ['uuid', 'tags'], // Select old tags column - where: 'tags IS NOT NULL AND tags != ""'); - - await db.transaction((txn) async { - for (var oldTask in oldTasksWithTags) { - final String taskUuid = oldTask['uuid']; - final String tagsString = oldTask['tags']; - final List tags = - tagsString.split(' ').where((s) => s.isNotEmpty).toList(); - - for (String tag in tags) { - await txn.insert( - 'TaskTags', - {'task_uuid': taskUuid, 'tag': tag}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - } - }); - debugPrint( - "Migrated tags data from old 'tags' column to 'TaskTags' table."); - } catch (e) { - debugPrint( - "Error migrating old tags data (possibly old 'tags' column didn't exist or was empty): $e"); - } - - // 4. (Optional but recommended) Drop the old 'tags' column from 'Tasks' table - // Only do this after you are absolutely sure the data migration is successful. - try { - await db.execute("ALTER TABLE Tasks DROP COLUMN tags"); - debugPrint("Dropped old 'tags' column from 'Tasks' table."); - } catch (e) { - debugPrint( - "Could not drop old 'tags' column (might not exist): $e"); - } - } - }, - ); + await db.execute(''' + CREATE TABLE Depends ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + d_uuid TEXT NOT NULL, + task_uuid TEXT NOT NULL, + task_id INTEGER NOT NULL, + FOREIGN KEY (task_uuid, task_id) + REFERENCES Tasks (uuid, id) + ON DELETE CASCADE + ON UPDATE CASCADE + ) + '''); + }); + debugPrint("Database opened at $path"); } Future ensureDatabaseIsOpen() async { - if (_database == null || !_database!.isOpen) { + if (_database == null) { await open(); } } - // Helper method to convert a database map to a TaskForC object - Future _taskFromDbMap(Map taskMap) async { - final String taskUuid = taskMap['uuid'] as String; - - // Fetch Tags - final List> tagMaps = await _database!.query( - 'TaskTags', - columns: ['tag'], - where: 'task_uuid = ?', - whereArgs: [taskUuid], - ); - final List tags = - tagMaps.map((map) => map['tag'] as String).toList(); - - // Fetch Depends - final List> dependsMaps = await _database!.query( - 'TaskDepends', - columns: ['depends_on_uuid'], - where: 'task_uuid = ?', - whereArgs: [taskUuid], - ); - final List depends = - dependsMaps.map((map) => map['depends_on_uuid'] as String).toList(); - - // Fetch Annotations - final List> annotationMaps = await _database!.query( - 'TaskAnnotations', - where: 'task_uuid = ?', - whereArgs: [taskUuid], - ); - final List annotations = - annotationMaps.map((map) => Annotation.fromJson(map)).toList(); - - return TaskForC( - id: taskMap['id'], - description: taskMap['description'], - project: taskMap['project'], - status: taskMap['status'], - uuid: taskMap['uuid'], - urgency: (taskMap['urgency'] as num?)?.toDouble(), - priority: taskMap['priority'], - due: taskMap['due'], - start: taskMap['start'], - end: taskMap['end'], - entry: taskMap['entry'], - wait: taskMap['wait'], - modified: taskMap['modified'], - rtype: taskMap['rtype'], - recur: taskMap['recur'], - tags: tags, - depends: depends, - annotations: annotations, - ); - } - - // Helper method to convert a TaskForC object to a map for the 'Tasks' table - Map _taskToDbMap(TaskForC task) { - return { - 'id': task.id, - 'description': task.description, - 'project': task.project, - 'status': task.status, - 'uuid': task.uuid, - 'urgency': task.urgency, - 'priority': task.priority, - 'due': task.due, - 'start': task.start, - 'end': task.end, - 'entry': task.entry, - 'wait': task.wait, - 'modified': task.modified, - 'rtype': task.rtype, - 'recur': task.recur, - }; - } - - // --- CRUD Operations for TaskForC --- - Future> fetchTasksFromDatabase() async { await ensureDatabaseIsOpen(); - final List> taskMaps = await _database!.query('Tasks'); - debugPrint("Database fetch: ${taskMaps.length} tasks found."); - - List tasks = []; - for (var taskMap in taskMaps) { - tasks.add(await _taskFromDbMap(taskMap)); + final List> maps = await _database!.query('Tasks'); + List a = await Future.wait( + maps.map((mapItem) => getObjectForTask(mapItem)).toList(), + ); + debugPrint("Database fetch ${maps.last}"); + for (int i = 0; i < maps.length; i++) { + debugPrint("Database fetch ${maps[i]}"); } - return tasks; + debugPrint('Tasks from db'); + debugPrint(a.toString()); + return a; } Future deleteAllTasksInDB() async { await ensureDatabaseIsOpen(); - - await _database!.transaction((txn) async { - await txn.delete('TaskAnnotations'); - await txn.delete('TaskDepends'); - await txn.delete('TaskTags'); - await txn.delete('Tasks'); - }); - - debugPrint('Deleted all tasks and related data'); + await _database!.delete( + 'Tasks', + ); } Future printDatabaseContents() async { await ensureDatabaseIsOpen(); - debugPrint('--- Contents of Tasks table ---'); - List> tasks = await _database!.query('Tasks'); - for (var map in tasks) { - debugPrint(map.toString()); - } - - debugPrint('--- Contents of TaskTags table ---'); - List> taskTags = await _database!.query('TaskTags'); - for (var map in taskTags) { - debugPrint(map.toString()); - } - - debugPrint('--- Contents of TaskDepends table ---'); - List> taskDepends = - await _database!.query('TaskDepends'); - for (var map in taskDepends) { - debugPrint(map.toString()); - } - - debugPrint('--- Contents of TaskAnnotations table ---'); - List> taskAnnotations = - await _database!.query('TaskAnnotations'); - for (var map in taskAnnotations) { - debugPrint(map.toString()); + List> maps = await _database!.query('Tasks'); + for (var map in maps) { + map.forEach((key, value) { + debugPrint('Key: $key, Value: $value, Type: ${value.runtimeType}'); + }); } } Future insertTask(TaskForC task) async { await ensureDatabaseIsOpen(); - debugPrint("Database Insert: Starting for task UUID: ${task.uuid}"); - - await _database!.transaction((txn) async { - // 1. Insert into Tasks table - await txn.insert( - 'Tasks', - _taskToDbMap(task), // Use helper to get map for main table - conflictAlgorithm: ConflictAlgorithm.replace, - ); - debugPrint("Inserted main task data for UUID: ${task.uuid}"); - - // 2. Insert into TaskTags table - if (task.tags != null && task.tags!.isNotEmpty) { - for (String tag in task.tags!) { - await txn.insert( - 'TaskTags', - {'task_uuid': task.uuid, 'tag': tag}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - debugPrint("Inserted tags for UUID: ${task.uuid}"); - } - - // 3. Insert into TaskDepends table - if (task.depends != null && task.depends!.isNotEmpty) { - for (String dependsOnUuid in task.depends!) { - await txn.insert( - 'TaskDepends', - {'task_uuid': task.uuid, 'depends_on_uuid': dependsOnUuid}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - debugPrint("Inserted dependencies for UUID: ${task.uuid}"); - } - - // 4. Insert into TaskAnnotations table - if (task.annotations != null && task.annotations!.isNotEmpty) { - for (Annotation annotation in task.annotations!) { - await txn.insert( - 'TaskAnnotations', - { - 'task_uuid': task.uuid, - 'entry': annotation.entry, - 'description': annotation.description, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - debugPrint("Inserted annotations for UUID: ${task.uuid}"); - } - }); - debugPrint("Database Insert Complete for task UUID: ${task.uuid}"); + List taskTags = task.tags ?? []; + List taskDepends = task.depends ?? []; + List> taskAnnotations = task.annotations != null + ? task.annotations! + .map((a) => {"entry": a.entry, "description": a.description}) + .toList() + : []; + debugPrint( + "Database insert ${task.description} for task tags are $taskTags"); + var map = task.toJson(); + map.remove("tags"); + map.remove("depends"); + map.remove("annotations"); + await _database!.insert( + 'Tasks', + map, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + if (taskTags.isNotEmpty) { + // Use the ID from the task object itself for consistency + await setTagsForTask(task.uuid ?? '', task.id, taskTags.toList()); + } + if (taskDepends.isNotEmpty) { + await setDependsForTask(task.uuid ?? '', task.id, taskDepends.toList()); + } + if (taskAnnotations.isNotEmpty) { + await setAnnotationsForTask( + task.uuid ?? '', task.id, taskAnnotations.toList()); + } } Future updateTask(TaskForC task) async { await ensureDatabaseIsOpen(); - debugPrint("Database Update: Starting for task UUID: ${task.uuid}"); - - await _database!.transaction((txn) async { - // 1. Update main Tasks table - await txn.update( - 'Tasks', - _taskToDbMap(task), // Use helper to get map for main table - where: 'uuid = ?', - whereArgs: [task.uuid], - ); - debugPrint("Updated main task data for UUID: ${task.uuid}"); - - // 2. Update TaskTags table: Delete existing, then insert new - await txn - .delete('TaskTags', where: 'task_uuid = ?', whereArgs: [task.uuid]); - if (task.tags != null && task.tags!.isNotEmpty) { - for (String tag in task.tags!) { - await txn.insert( - 'TaskTags', - {'task_uuid': task.uuid, 'tag': tag}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - } - debugPrint("Updated tags for UUID: ${task.uuid}"); - - // 3. Update TaskDepends table: Delete existing, then insert new - await txn.delete('TaskDepends', - where: 'task_uuid = ?', whereArgs: [task.uuid]); - if (task.depends != null && task.depends!.isNotEmpty) { - for (String dependsOnUuid in task.depends!) { - await txn.insert( - 'TaskDepends', - {'task_uuid': task.uuid, 'depends_on_uuid': dependsOnUuid}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - } - debugPrint("Updated dependencies for UUID: ${task.uuid}"); - - // 4. Update TaskAnnotations table: Delete existing, then insert new - await txn.delete('TaskAnnotations', - where: 'task_uuid = ?', whereArgs: [task.uuid]); - if (task.annotations != null && task.annotations!.isNotEmpty) { - for (Annotation annotation in task.annotations!) { - await txn.insert( - 'TaskAnnotations', - { - 'task_uuid': task.uuid, - 'entry': annotation.entry, - 'description': annotation.description, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - } - debugPrint("Updated annotations for UUID: ${task.uuid}"); - }); - debugPrint("Database Update Complete for task UUID: ${task.uuid}"); + debugPrint("Database update"); + List taskTags = task.tags?.map((e) => e.toString()).toList() ?? []; + debugPrint("Database update $taskTags"); + List taskDepends = + task.tags?.map((e) => e.toString()).toList() ?? []; + debugPrint("Database update $taskDepends"); + List> taskAnnotations = task.annotations != null + ? task.annotations! + .map((a) => {"entry": a.entry, "description": a.description}) + .toList() + : []; + var map = task.toJson(); + map.remove("tags"); + map.remove("depends"); + map.remove("annotations"); + await _database!.update( + 'Tasks', + map, + where: 'uuid = ?', + whereArgs: [task.uuid], + ); + if (taskTags.isNotEmpty) { + await setTagsForTask(task.uuid ?? '', task.id, taskTags.toList()); + } + if (taskDepends.isNotEmpty) { + await setDependsForTask(task.uuid ?? '', task.id, taskDepends.toList()); + } + if (taskAnnotations.isNotEmpty) { + await setAnnotationsForTask( + task.uuid ?? '', task.id, taskAnnotations.toList()); + } } Future getTaskByUuid(String uuid) async { await ensureDatabaseIsOpen(); - final List> taskMaps = await _database!.query( + List> maps = await _database!.query( 'Tasks', where: 'uuid = ?', whereArgs: [uuid], ); - if (taskMaps.isEmpty) { + if (maps.isNotEmpty) { + return await getObjectForTask(maps.first); + } else { return null; } - - return await _taskFromDbMap(taskMaps.first); } Future markTaskAsCompleted(String uuid) async { @@ -457,98 +206,52 @@ class TaskDatabase { where: 'uuid = ?', whereArgs: [uuid], ); - debugPrint('Task $uuid marked as completed'); + debugPrint('task${uuid}completed'); + debugPrint({DateTime.now().toIso8601String()}.toString()); } Future markTaskAsDeleted(String uuid) async { await ensureDatabaseIsOpen(); - // Due to FOREIGN KEY ... ON DELETE CASCADE, related tags, depends, and annotations - // will be automatically deleted when the main task is deleted from the Tasks table. - await _database!.delete( + await _database!.update( 'Tasks', + {'status': 'deleted'}, where: 'uuid = ?', whereArgs: [uuid], ); - debugPrint('Task $uuid deleted (and related data cascaded)'); + debugPrint('task${uuid}deleted'); } Future saveEditedTaskInDB( String uuid, String newDescription, - String? newProject, + String newProject, String newStatus, - String? newPriority, - String? newDue, - List? newTags, - List? newDepends, - List? newAnnotations, + String newPriority, + String newDue, + List newTags, ) async { await ensureDatabaseIsOpen(); - debugPrint('Saving edited task $uuid'); - - await _database!.transaction((txn) async { - await txn.update( - 'Tasks', - { - 'description': newDescription, - 'project': newProject, - 'status': newStatus, - 'priority': newPriority, - 'due': newDue, - 'modified': (DateTime.now()).toIso8601String(), - }, - where: 'uuid = ?', - whereArgs: [uuid], - ); - debugPrint('Main task data updated for $uuid'); - - // Update Tags - await txn.delete('TaskTags', where: 'task_uuid = ?', whereArgs: [uuid]); - if (newTags != null && newTags.isNotEmpty) { - for (String tag in newTags) { - await txn.insert( - 'TaskTags', - {'task_uuid': uuid, 'tag': tag}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - } - debugPrint('Tags updated for $uuid'); - - // Update Depends - await txn - .delete('TaskDepends', where: 'task_uuid = ?', whereArgs: [uuid]); - if (newDepends != null && newDepends.isNotEmpty) { - for (String dependsOnUuid in newDepends) { - await txn.insert( - 'TaskDepends', - {'task_uuid': uuid, 'depends_on_uuid': dependsOnUuid}, - conflictAlgorithm: ConflictAlgorithm.ignore, - ); - } - } - debugPrint('Dependencies updated for $uuid'); - - // Update Annotations - await txn - .delete('TaskAnnotations', where: 'task_uuid = ?', whereArgs: [uuid]); - if (newAnnotations != null && newAnnotations.isNotEmpty) { - for (Annotation annotation in newAnnotations) { - await txn.insert( - 'TaskAnnotations', - { - 'task_uuid': uuid, - 'entry': annotation.entry, - 'description': annotation.description, - }, - conflictAlgorithm: ConflictAlgorithm.replace, - ); - } - } - debugPrint('Annotations updated for $uuid'); - }); - debugPrint('Task $uuid edited successfully'); + + debugPrint('task in saveEditedTaskInDB: $uuid with due $newDue'); + await _database!.update( + 'Tasks', + { + 'description': newDescription, + 'project': newProject, + 'status': newStatus, + 'priority': newPriority, + 'due': newDue, + 'modified': DateTime.now().toIso8601String(), + }, + where: 'uuid = ?', + whereArgs: [uuid], + ); + debugPrint('task${uuid}edited'); + if (newTags.isNotEmpty) { + TaskForC? task = await getTaskByUuid(uuid); + await setTagsForTask(uuid, task?.id ?? 0, newTags.toList()); + } } Future> findTasksWithoutUUIDs() async { @@ -560,110 +263,219 @@ class TaskDatabase { whereArgs: [''], ); - List tasks = []; - for (var taskMap in maps) { - // Note: If task_uuid is NULL or empty, the _taskFromDbMap will attempt to query related tables - // with a potentially invalid UUID. Ensure your data integrity that tasks always have a UUID - // if they are expected to have tags/depends/annotations. - tasks.add(await _taskFromDbMap(taskMap)); - } - return tasks; + return await Future.wait( + maps.map((mapItem) => getObjectForTask(mapItem)).toList(), + ); } Future> getTasksByProject(String project) async { - await ensureDatabaseIsOpen(); - - List> taskMaps = await _database!.query( + List> maps = await _database!.query( 'Tasks', where: 'project = ?', whereArgs: [project], ); - debugPrint("DB Stored for $project: ${taskMaps.length} tasks"); - - List tasks = []; - for (var taskMap in taskMaps) { - tasks.add(await _taskFromDbMap(taskMap)); - } - return tasks; + debugPrint("DB Stored for $maps"); + return await Future.wait( + maps.map((mapItem) => getObjectForTask(mapItem)).toList(), + ); } Future> fetchUniqueProjects() async { - await ensureDatabaseIsOpen(); + var taskDatabase = TaskDatabase(); + await taskDatabase.open(); + await taskDatabase.ensureDatabaseIsOpen(); - final List> result = await _database!.rawQuery( - 'SELECT DISTINCT project FROM Tasks WHERE project IS NOT NULL AND project != ""'); + final List> result = await taskDatabase._database! + .rawQuery( + 'SELECT DISTINCT project FROM Tasks WHERE project IS NOT NULL'); return result.map((row) => row['project'] as String).toList(); } Future> searchTasks(String query) async { - await ensureDatabaseIsOpen(); - - final List> taskMaps = await _database!.query( - 'Tasks', + final List> maps = await _database!.query( + 'tasks', where: 'description LIKE ? OR project LIKE ?', whereArgs: ['%$query%', '%$query%'], ); - - List tasks = []; - for (var taskMap in taskMaps) { - tasks.add(await _taskFromDbMap(taskMap)); - } - return tasks; + return await Future.wait( + maps.map((mapItem) => getObjectForTask(mapItem)).toList(), + ); } Future close() async { - if (_database != null && _database!.isOpen) { - await _database!.close(); - _database = null; - } + await _database!.close(); } - Future deleteTask( - {String? description, - String? due, - String? project, - String? priority, - String? uuid}) async { - await ensureDatabaseIsOpen(); + Future deleteTask({description, due, project, priority}) async { + await _database!.delete( + 'Tasks', + where: 'description = ? AND due = ? AND project = ? AND priority = ?', + whereArgs: [description, due, project, priority], + ); + } - if (uuid != null && uuid.isNotEmpty) { - // If UUID is provided, use it for a more robust deletion which triggers cascade - await markTaskAsDeleted(uuid); - } else { - // Fallback for deleting without UUID (less reliable, does not cascade automatically for related tables) - List whereClauses = []; - List whereArgs = []; +// Get tags using a composite key + Future> getTagsForTask(String uuid, int id) async { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return []; + } + final List> maps = await db.query( + 'Tags', + columns: ['name'], + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], + ); + return List.generate(maps.length, (i) { + return maps[i]['name'] as String; + }); + } - if (description != null) { - whereClauses.add('description = ?'); - whereArgs.add(description); - } - if (due != null) { - whereClauses.add('due = ?'); - whereArgs.add(due); - } - if (project != null) { - whereClauses.add('project = ?'); - whereArgs.add(project); - } - if (priority != null) { - whereClauses.add('priority = ?'); - whereArgs.add(priority); + // Set tags using a composite key + Future setTagsForTask(String uuid, int id, List tags) async { + debugPrint('Setting tags for task $uuid: $tags'); + try { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return; } + await db.transaction((txn) async { + // Delete existing tags for the task + await txn.delete( + 'Tags', + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], + ); + // Insert new tags + for (String tag in tags) { + if (tag.trim().isNotEmpty) { + await txn.insert( + 'Tags', + {'name': tag, 'task_uuid': uuid, 'task_id': id}, + ); + } + } + }); + } catch (e) { + debugPrint('Error setting tags for task $uuid: $e'); + } + } - if (whereClauses.isNotEmpty) { - await _database!.delete( - 'Tasks', - where: whereClauses.join(' AND '), - whereArgs: whereArgs, + // depends methods + Future> getDependsForTask(String uuid, int id) async { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return []; + } + final List> maps = await db.query( + 'Depends', + columns: ['d_uuid'], + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], + ); + return List.generate(maps.length, (i) { + return maps[i]['d_uuid'] as String; + }); + } + + Future setDependsForTask( + String uuid, int id, List depends) async { + try { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return; + } + await db.transaction((txn) async { + await txn.delete( + 'Depends', + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], ); - debugPrint( - 'Task deleted using old method (description, due, project, priority). NOTE: Related data in TaskTags, TaskDepends, TaskAnnotations for this task UUID might remain if UUID was not used for deletion.'); - } else { - debugPrint( - 'Delete task called without sufficient identifying parameters (UUID or other fields). No action taken.'); + for (String dUuid in depends) { + if (dUuid.trim().isNotEmpty) { + await txn.insert( + 'Depends', + {'d_uuid': dUuid, 'task_uuid': uuid, 'task_id': id}, + ); + } + } + }); + } catch (e) { + debugPrint('Error setting depends for task $uuid: $e'); + } + } + + // annotations methods + Future>> getAnnotationsForTask( + String uuid, int id) async { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return >[]; + } + final List> maps = await db.query( + 'Annotations', + columns: ['entry', 'description'], + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], + ); + return List.generate(maps.length, (i) { + return { + 'entry': maps[i]['entry'] as String, + 'description': maps[i]['description'] as String, + }; + }); + } + + Future setAnnotationsForTask( + String uuid, int id, List> annotations) async { + try { + ensureDatabaseIsOpen(); + final db = _database; + if (db == null) { + return; } + await db.transaction((txn) async { + await txn.delete( + 'Annotations', + where: 'task_uuid = ? AND task_id = ?', + whereArgs: [uuid, id], + ); + for (Map annotation in annotations) { + if (annotation['entry'] != null && + annotation['description'] != null) { + await txn.insert( + 'Annotations', + { + 'entry': annotation['entry'], + 'description': annotation['description'], + 'task_uuid': uuid, + 'task_id': id + }, + ); + } + } + }); + } catch (e) { + debugPrint('Error setting annotations for task $uuid: $e'); } } + + // Assemble TaskForC object + Future getObjectForTask(Map map) async { + final mutableMap = Map.from(map); + mutableMap['tags'] = + await getTagsForTask(mutableMap['uuid'], mutableMap['id']); + mutableMap['depends'] = + await getDependsForTask(mutableMap['uuid'], mutableMap['id']); + mutableMap['annotations'] = + await getAnnotationsForTask(mutableMap['uuid'], mutableMap['id']); + TaskForC task = TaskForC.fromJson(mutableMap); + return task; + } } diff --git a/lib/app/v3/db/update.dart b/lib/app/v3/db/update.dart index cc0f1008..7d0f549f 100644 --- a/lib/app/v3/db/update.dart +++ b/lib/app/v3/db/update.dart @@ -5,8 +5,11 @@ import 'package:taskwarrior/app/v3/net/add_task.dart'; import 'package:taskwarrior/app/v3/net/complete.dart'; import 'package:taskwarrior/app/v3/net/delete.dart'; import 'package:taskwarrior/app/v3/net/modify.dart'; +import 'package:timezone/timezone.dart'; Future updateTasksInDatabase(List tasks) async { + debugPrint( + "Updating tasks in database... Total tasks from server: ${tasks.length}"); var taskDatabase = TaskDatabase(); await taskDatabase.open(); // find tasks without UUID @@ -15,10 +18,15 @@ Future updateTasksInDatabase(List tasks) async { //add tasks without UUID to the server and delete them from database for (var task in tasksWithoutUUID) { try { - await addTaskAndDeleteFromDatabase(task.description, task.project!, - task.due!, task.priority!, task.tags != null ? task.tags! : []); + await addTaskAndDeleteFromDatabase( + task.description, + task.project != null ? task.project! : '', + task.due!, + task.priority!, + task.tags != null ? task.tags! : []); } catch (e) { - debugPrint('Failed to add task without UUID to server: $e'); + debugPrint( + 'Failed to add task without UUID to server: $e ${task.tags} ${task.project}'); } } @@ -43,6 +51,8 @@ Future updateTasksInDatabase(List tasks) async { if (localTask == null) { // Task doesn't exist in the local database, insert it + debugPrint( + 'Inserting new task from server: ${serverTask.description}, modified: ${serverTask.modified}'); await taskDatabase.insertTask(serverTask); } else { var serverTaskModifiedDate = DateTime.parse(serverTask.modified!); @@ -53,7 +63,19 @@ Future updateTasksInDatabase(List tasks) async { await taskDatabase.updateTask(serverTask); } else if (serverTaskModifiedDate.isBefore(localTaskModifiedDate)) { // local task is newer, update server - await modifyTaskOnTaskwarrior(localTask); + debugPrint( + 'Updating task on server: ${localTask.description}, modified: ${localTask.modified}'); + await modifyTaskOnTaskwarrior( + localTask.description, + localTask.project!, + localTask.due!, + localTask.priority!, + localTask.status, + localTask.uuid!, + localTask.id.toString(), + localTask.tags != null + ? localTask.tags!.map((e) => e.toString()).toList() + : []); if (localTask.status == 'completed') { completeTask('email', localTask.uuid!); } else if (localTask.status == 'deleted') { diff --git a/lib/app/v3/models/annotation.dart b/lib/app/v3/models/annotation.dart index 037b5f4c..e312b393 100644 --- a/lib/app/v3/models/annotation.dart +++ b/lib/app/v3/models/annotation.dart @@ -1,17 +1,13 @@ -// Annotation class to mirror the Go Annotation struct class Annotation { - final String entry; - final String description; - - Annotation({required this.entry, required this.description}); - + final String? entry; + final String? description; + Annotation({this.entry, this.description}); factory Annotation.fromJson(Map json) { return Annotation( entry: json['entry'], description: json['description'], ); } - Map toJson() { return { 'entry': entry, diff --git a/lib/app/v3/models/task.dart b/lib/app/v3/models/task.dart index bca01e8c..19503c58 100644 --- a/lib/app/v3/models/task.dart +++ b/lib/app/v3/models/task.dart @@ -1,5 +1,5 @@ -import 'package:flutter/foundation.dart'; -import 'package:taskwarrior/app/v3/models/annotation.dart'; +import 'package:flutter/material.dart'; +import "./annotation.dart"; class TaskForC { final int id; @@ -10,72 +10,65 @@ class TaskForC { final double? urgency; final String? priority; final String? due; - final String? start; // Added: Corresponds to Go's 'Start' final String? end; final String entry; - final String? wait; // Added: Corresponds to Go's 'Wait' final String? modified; - final List? tags; // Changed to List to match Go's []string - final List? depends; // Added: Corresponds to Go's 'Depends' - final String? rtype; // Added: Corresponds to Go's 'RType' - final String? recur; // Added: Corresponds to Go's 'Recur' - final List? - annotations; // Added: Corresponds to Go's 'Annotations' + final List? tags; + // newer feilds in CCSync Model + final String? start; + final String? wait; + final String? rtype; + final String? recur; + final List? depends; + final List? annotations; TaskForC({ required this.id, required this.description, - this.project, // Made nullable to match Go's typical handling of empty strings for non-required fields + required this.project, required this.status, - this.uuid, - this.urgency, - this.priority, - this.due, - this.start, - this.end, + required this.uuid, + required this.urgency, + required this.priority, + required this.due, + required this.end, required this.entry, - this.wait, - this.modified, - this.tags, - this.depends, - this.rtype, - this.recur, - this.annotations, + required this.modified, + required this.tags, + required this.start, + required this.wait, + required this.rtype, + required this.recur, + required this.depends, + required this.annotations, }); - // Factory constructor for parsing JSON from API responses factory TaskForC.fromJson(Map json) { + debugPrint("Annotation fromJson: ${json['annotations'] == null}"); return TaskForC( - id: json['id'], - description: json['description'], - project: json['project'], - status: json['status'], - uuid: json['uuid'], - // Safely parse urgency as double, handling potential null or int from JSON - urgency: (json['urgency'] as num?)?.toDouble(), - priority: json['priority'], - due: json['due'], - start: json['start'], - end: json['end'], - entry: json['entry'], - wait: json['wait'], - modified: json['modified'], - // Ensure tags are parsed as List - tags: (json['tags'] as List?)?.map((e) => e.toString()).toList(), - // Ensure depends are parsed as List - depends: (json['depends'] as List?)?.map((e) => e.toString()).toList(), - rtype: json['rtype'], - recur: json['recur'], - // Parse list of annotation maps into list of Annotation objects - annotations: (json['annotations'] as List?) - ?.map((e) => Annotation.fromJson(e as Map)) - .toList(), - ); + id: json['id'], + description: json['description'], + project: json['project'], + status: json['status'], + uuid: json['uuid'], + urgency: json['urgency'].toDouble(), + priority: json['priority'], + due: json['due'], + end: json['end'], + entry: json['entry'], + modified: json['modified'], + tags: json['tags']?.map((tag) => tag.toString()).toList() ?? [], + start: json['start'], + wait: json['wait'], + rtype: json['rtype'], + recur: json['recur'], + depends: + json['depends']?.map((d) => d.toString()).toList() ?? [], + annotations: []); } - // Method to convert TaskForC object to a JSON map for API requests Map toJson() { - debugPrint("TAGS TO JSON: $tags"); + debugPrint("TAGS: $tags"); return { 'id': id, 'description': description, @@ -85,17 +78,18 @@ class TaskForC { 'urgency': urgency, 'priority': priority, 'due': due, - 'start': start, 'end': end, 'entry': entry, - 'wait': wait, 'modified': modified, 'tags': tags, - 'depends': depends, + 'start': start, + 'wait': wait, 'rtype': rtype, 'recur': recur, - // Convert list of Annotation objects to list of JSON maps - 'annotations': annotations?.map((e) => e.toJson()).toList(), + 'depends': depends, + 'annotations': annotations != null + ? annotations?.map((a) => a.toJson()).toList() + : >[], }; } } diff --git a/lib/app/v3/net/fetch.dart b/lib/app/v3/net/fetch.dart index e1cff2c6..c38ef639 100644 --- a/lib/app/v3/net/fetch.dart +++ b/lib/app/v3/net/fetch.dart @@ -15,6 +15,8 @@ Future> fetchTasks(String uuid, String encryptionSecret) async { var response = await http.get(Uri.parse(url), headers: { "Content-Type": "application/json", }).timeout(const Duration(seconds: 10000)); + debugPrint("Fetch tasks response: ${response.statusCode}"); + debugPrint("Fetch tasks body: ${response.body}"); if (response.statusCode == 200) { List allTasks = jsonDecode(response.body); debugPrint(allTasks.toString()); @@ -22,8 +24,9 @@ Future> fetchTasks(String uuid, String encryptionSecret) async { } else { throw Exception('Failed to load tasks'); } - } catch (e) { - debugPrint('Error fetching tasks: $e'); + } catch (e, s) { + debugPrint('Error fetching tasks: $e\n $s'); + return []; } } diff --git a/lib/app/v3/net/modify.dart b/lib/app/v3/net/modify.dart index 1f435963..1d32976a 100644 --- a/lib/app/v3/net/modify.dart +++ b/lib/app/v3/net/modify.dart @@ -5,15 +5,38 @@ import 'package:path/path.dart'; import 'package:flutter/material.dart'; import 'package:taskwarrior/app/utils/taskchampion/credentials_storage.dart'; import 'package:taskwarrior/app/v3/db/task_database.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; -Future modifyTaskOnTaskwarrior(TaskForC tskc) async { +Future modifyTaskOnTaskwarrior( + String description, + String project, + String due, + String priority, + String status, + String taskuuid, + String id, + List newTags) async { var baseUrl = await CredentialsStorage.getApiUrl(); var c = await CredentialsStorage.getClientId(); var e = await CredentialsStorage.getEncryptionSecret(); String apiUrl = '$baseUrl/modify-task'; debugPrint(c); debugPrint(e); + debugPrint("modifyTaskOnTaskwarrior called"); + debugPrint("description: $description project: $project due: $due " + "priority: $priority status: $status taskuuid: $taskuuid id: $id tags: $newTags" + "body: ${jsonEncode({ + "email": "e", + "encryptionSecret": e, + "UUID": c, + "description": description, + "priority": priority, + "project": project, + "due": due, + "status": status, + "taskuuid": taskuuid, + "taskId": id, + "tags": newTags.isNotEmpty ? newTags : null + })}"); final response = await http.post( Uri.parse(apiUrl), headers: { @@ -23,28 +46,28 @@ Future modifyTaskOnTaskwarrior(TaskForC tskc) async { "email": "e", "encryptionSecret": e, "UUID": c, - "description": tskc.description, - "priority": tskc.priority, - "project": tskc.project, - "due": tskc.due, - "status": tskc.status, - "taskuuid": tskc.uuid, + "description": description, + "priority": priority, + "project": project, + "due": due, + "status": status, + "taskuuid": taskuuid, + "taskId": id, + "tags": newTags.isNotEmpty ? newTags : null }), ); - - if (response.statusCode != 200) { - Get.snackbar( - 'Error', - 'Failed to modify task: ${response.reasonPhrase}', - snackPosition: SnackPosition.BOTTOM, - ); + debugPrint('Modify task response body: ${response.body}'); + if (response.statusCode < 200 || response.statusCode >= 300) { + Get.showSnackbar(GetSnackBar( + title: 'Error', + message: + 'Failed to modify task on Taskwarrior server. ${response.statusCode}', + duration: Duration(seconds: 3), + )); } var taskDatabase = TaskDatabase(); await taskDatabase.open(); await taskDatabase.deleteTask( - description: tskc.description, - due: tskc.due, - project: tskc.project, - priority: tskc.priority); + description: description, due: due, project: project, priority: priority); } diff --git a/test/api_service_test.dart b/test/api_service_test.dart index 1b14d90b..cfa115f8 100644 --- a/test/api_service_test.dart +++ b/test/api_service_test.dart @@ -72,7 +72,13 @@ void main() { end: null, entry: '2024-01-01', modified: '2024-11-01', - tags: ['t1']); + tags: ['t1'], + start: '', + wait: '', + rtype: '', + recur: '', + depends: [], + annotations: []); final json = task.toJson(); @@ -129,10 +135,16 @@ void main() { urgency: 5.0, priority: 'H', due: '2024-12-31', - end: null, + end: '', entry: '2024-01-01', modified: '2024-11-01', - tags: ['t1']); + tags: ['t1'], + start: '', + wait: '', + rtype: '', + recur: '', + depends: [], + annotations: []); await taskDatabase.insertTask(task); @@ -155,7 +167,13 @@ void main() { end: null, entry: '2024-01-01', modified: '2024-11-01', - tags: ['t1']); + tags: ['t1'], + start: '', + wait: '', + rtype: '', + recur: '', + depends: [], + annotations: []); await taskDatabase.insertTask(task); await taskDatabase.deleteAllTasksInDB();