From 8730456b3edcff8f0a511914223ec5258f1d3be8 Mon Sep 17 00:00:00 2001 From: Shubham Ingale Date: Mon, 21 Jul 2025 20:09:55 +0530 Subject: [PATCH 1/2] ccsync intgr: models and dbs changed --- lib/app/v3/db/task_database.dart | 691 ++++++++++++++++++++++++------ lib/app/v3/models/annotation.dart | 22 +- lib/app/v3/models/task.dart | 128 +++--- 3 files changed, 637 insertions(+), 204 deletions(-) diff --git a/lib/app/v3/db/task_database.dart b/lib/app/v3/db/task_database.dart index 0216e810..b1da7997 100644 --- a/lib/app/v3/db/task_database.dart +++ b/lib/app/v3/db/task_database.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; -import 'package:taskwarrior/app/v3/models/task.dart'; +import 'package:taskwarrior/app/v3/models/annotation.dart'; +import 'package:taskwarrior/app/v3/models/task.dart'; // Path to your updated TaskForC model class TaskDatabase { Database? _database; @@ -10,124 +11,441 @@ class TaskDatabase { var databasesPath = await getDatabasesPath(); String path = join(databasesPath, 'tasks.db'); - _database = await openDatabase(path, - version: 1, - onOpen: (db) async => await addTagsColumnIfNeeded(db), - onCreate: (Database db, version) async { + _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 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 - ) - '''); - }); - } - - Future addTagsColumnIfNeeded(Database db) async { - try { - await db.rawQuery("SELECT tags FROM Tasks LIMIT 0"); - } catch (e) { - await db.execute("ALTER TABLE Tasks ADD COLUMN tags TEXT"); - debugPrint("Added Column tags"); - } + 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 + ) + '''); + 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 + ) + '''); + 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("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"); + } + } + }, + ); } Future ensureDatabaseIsOpen() async { - if (_database == null) { + if (_database == null || !_database!.isOpen) { 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> maps = await _database!.query('Tasks'); - debugPrint("Database fetch ${maps.last}"); - var a = List.generate(maps.length, (i) { - return TaskForC( - id: maps[i]['id'], - description: maps[i]['description'], - project: maps[i]['project'], - status: maps[i]['status'], - uuid: maps[i]['uuid'], - urgency: maps[i]['urgency'], - priority: maps[i]['priority'], - due: maps[i]['due'], - end: maps[i]['end'], - entry: maps[i]['entry'], - modified: maps[i]['modified'], - tags: maps[i]['tags'] != null ? maps[i]['tags'].split(' ') : []); - }); - // debugPrint('Tasks from db'); - // debugPrint(a.toString()); - return a; + 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)); + } + return tasks; } Future deleteAllTasksInDB() async { await ensureDatabaseIsOpen(); - await _database!.delete('Tasks'); - debugPrint('Deleted all tasks'); - await open(); - debugPrint('Created new task table'); + 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'); } Future printDatabaseContents() async { await ensureDatabaseIsOpen(); - List> maps = await _database!.query('Tasks'); - for (var map in maps) { - map.forEach((key, value) { - debugPrint('Key: $key, Value: $value, Type: ${value.runtimeType}'); - }); + 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()); } } Future insertTask(TaskForC task) async { await ensureDatabaseIsOpen(); - debugPrint("Database Insert"); - var dbi = await _database!.insert( - 'Tasks', - task.toDbJson(), - conflictAlgorithm: ConflictAlgorithm.replace, - ); - debugPrint("Database Insert ${task.toDbJson()} $dbi"); + 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}"); } Future updateTask(TaskForC task) async { await ensureDatabaseIsOpen(); - - await _database!.update( - 'Tasks', - task.toDbJson(), - where: 'uuid = ?', - whereArgs: [task.uuid], - ); + 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}"); } Future getTaskByUuid(String uuid) async { await ensureDatabaseIsOpen(); - List> maps = await _database!.query( + final List> taskMaps = await _database!.query( 'Tasks', where: 'uuid = ?', whereArgs: [uuid], ); - if (maps.isNotEmpty) { - return TaskForC.fromDbJson(maps.first); - } else { + if (taskMaps.isEmpty) { return null; } + + return await _taskFromDbMap(taskMaps.first); } Future markTaskAsCompleted(String uuid) async { @@ -139,46 +457,98 @@ class TaskDatabase { where: 'uuid = ?', whereArgs: [uuid], ); - debugPrint('task${uuid}completed'); - debugPrint({DateTime.now().toIso8601String()}.toString()); + debugPrint('Task $uuid marked as completed'); } Future markTaskAsDeleted(String uuid) async { await ensureDatabaseIsOpen(); - await _database!.update( + // 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( 'Tasks', - {'status': 'deleted'}, where: 'uuid = ?', whereArgs: [uuid], ); - debugPrint('task${uuid}deleted'); + debugPrint('Task $uuid deleted (and related data cascaded)'); } Future saveEditedTaskInDB( String uuid, String newDescription, - String newProject, + String? newProject, String newStatus, - String newPriority, - String newDue, + String? newPriority, + String? newDue, + List? newTags, + List? newDepends, + List? newAnnotations, ) async { await ensureDatabaseIsOpen(); - - debugPrint('task${uuid}deleted'); - await _database!.update( - 'Tasks', - { - 'description': newDescription, - 'project': newProject, - 'status': newStatus, - 'priority': newPriority, - 'due': newDue, - }, - where: 'uuid = ?', - whereArgs: [uuid], - ); - debugPrint('task${uuid}edited'); + 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'); } Future> findTasksWithoutUUIDs() async { @@ -190,67 +560,110 @@ class TaskDatabase { whereArgs: [''], ); - return List.generate(maps.length, (i) { - return TaskForC.fromDbJson(maps[i]); - }); + 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; } Future> getTasksByProject(String project) async { - List> maps = await _database!.query( + await ensureDatabaseIsOpen(); + + List> taskMaps = await _database!.query( 'Tasks', where: 'project = ?', whereArgs: [project], ); - debugPrint("DB Stored for $maps"); - return List.generate(maps.length, (i) { - return TaskForC( - uuid: maps[i]['uuid'], - id: maps[i]['id'], - description: maps[i]['description'], - project: maps[i]['project'], - status: maps[i]['status'], - urgency: maps[i]['urgency'], - priority: maps[i]['priority'], - due: maps[i]['due'], - end: maps[i]['end'], - entry: maps[i]['entry'], - modified: maps[i]['modified'], - tags: maps[i]['tags'].toString().split(' ')); - }); + debugPrint("DB Stored for $project: ${taskMaps.length} tasks"); + + List tasks = []; + for (var taskMap in taskMaps) { + tasks.add(await _taskFromDbMap(taskMap)); + } + return tasks; } Future> fetchUniqueProjects() async { - var taskDatabase = TaskDatabase(); - await taskDatabase.open(); - await taskDatabase.ensureDatabaseIsOpen(); + await ensureDatabaseIsOpen(); - final List> result = await taskDatabase._database! - .rawQuery( - 'SELECT DISTINCT project FROM Tasks WHERE project IS NOT NULL'); + final List> result = await _database!.rawQuery( + 'SELECT DISTINCT project FROM Tasks WHERE project IS NOT NULL AND project != ""'); return result.map((row) => row['project'] as String).toList(); } Future> searchTasks(String query) async { - final List> maps = await _database!.query( - 'tasks', + await ensureDatabaseIsOpen(); + + final List> taskMaps = await _database!.query( + 'Tasks', where: 'description LIKE ? OR project LIKE ?', whereArgs: ['%$query%', '%$query%'], ); - return List.generate(maps.length, (i) { - return TaskForC.fromDbJson(maps[i]); - }); + + List tasks = []; + for (var taskMap in taskMaps) { + tasks.add(await _taskFromDbMap(taskMap)); + } + return tasks; } Future close() async { - await _database!.close(); + if (_database != null && _database!.isOpen) { + await _database!.close(); + _database = null; + } } - Future deleteTask({description, due, project, priority}) async { - await _database!.delete( - 'Tasks', - where: 'description = ? AND due = ? AND project = ? AND priority = ?', - whereArgs: [description, due, project, priority], - ); + Future deleteTask( + {String? description, + String? due, + String? project, + String? priority, + String? uuid}) async { + await ensureDatabaseIsOpen(); + + 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 = []; + + 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); + } + + if (whereClauses.isNotEmpty) { + await _database!.delete( + 'Tasks', + where: whereClauses.join(' AND '), + whereArgs: whereArgs, + ); + 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.'); + } + } } } diff --git a/lib/app/v3/models/annotation.dart b/lib/app/v3/models/annotation.dart index 9f8036e7..037b5f4c 100644 --- a/lib/app/v3/models/annotation.dart +++ b/lib/app/v3/models/annotation.dart @@ -1,5 +1,21 @@ +// Annotation class to mirror the Go Annotation struct class Annotation { - final String? entry; - final String? description; - Annotation({this.entry, this.description}); + final String entry; + final String description; + + Annotation({required this.entry, required this.description}); + + factory Annotation.fromJson(Map json) { + return Annotation( + entry: json['entry'], + description: json['description'], + ); + } + + Map toJson() { + return { + 'entry': entry, + 'description': description, + }; + } } diff --git a/lib/app/v3/models/task.dart b/lib/app/v3/models/task.dart index c93884f0..bca01e8c 100644 --- a/lib/app/v3/models/task.dart +++ b/lib/app/v3/models/task.dart @@ -1,4 +1,5 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:taskwarrior/app/v3/models/annotation.dart'; class TaskForC { final int id; @@ -9,76 +10,72 @@ 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; + 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' - TaskForC( - {required this.id, - required this.description, - required this.project, - required this.status, - required this.uuid, - required this.urgency, - required this.priority, - required this.due, - required this.end, - required this.entry, - required this.modified, - required this.tags}); + 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.status, + this.uuid, + this.urgency, + this.priority, + this.due, + this.start, + this.end, + required this.entry, + this.wait, + this.modified, + this.tags, + this.depends, + this.rtype, + this.recur, + this.annotations, + }); + // Factory constructor for parsing JSON from API responses factory TaskForC.fromJson(Map json) { return TaskForC( - 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']); - } - factory TaskForC.fromDbJson(Map json) { - debugPrint("FROM: $json"); - return TaskForC( - 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'].toString().split(' ')); + 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(), + ); } + // Method to convert TaskForC object to a JSON map for API requests Map toJson() { - debugPrint("TAGS: $tags"); - return { - 'id': id, - 'description': description, - 'project': project, - 'status': status, - 'uuid': uuid, - 'urgency': urgency, - 'priority': priority, - 'due': due, - 'end': end, - 'entry': entry, - 'modified': modified, - 'tags': tags - }; - } - - Map toDbJson() { + debugPrint("TAGS TO JSON: $tags"); return { 'id': id, 'description': description, @@ -88,10 +85,17 @@ class TaskForC { 'urgency': urgency, 'priority': priority, 'due': due, + 'start': start, 'end': end, 'entry': entry, + 'wait': wait, 'modified': modified, - 'tags': tags != null ? tags?.join(" ") : "" + 'tags': tags, + 'depends': depends, + 'rtype': rtype, + 'recur': recur, + // Convert list of Annotation objects to list of JSON maps + 'annotations': annotations?.map((e) => e.toJson()).toList(), }; } } From 71f38601dc10cd015e38f5b2d45e627b87d00f11 Mon Sep 17 00:00:00 2001 From: Shubham Ingale Date: Mon, 21 Jul 2025 20:10:58 +0530 Subject: [PATCH 2/2] ccsync intgr: show details updated --- lib/app/modules/home/views/show_details.dart | 142 +++++++++++++++---- 1 file changed, 115 insertions(+), 27 deletions(-) diff --git a/lib/app/modules/home/views/show_details.dart b/lib/app/modules/home/views/show_details.dart index 2a0dea77..e1a5bc44 100644 --- a/lib/app/modules/home/views/show_details.dart +++ b/lib/app/modules/home/views/show_details.dart @@ -8,8 +8,9 @@ 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/task.dart'; -import 'package:taskwarrior/app/v3/net/modify.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; @@ -25,6 +26,14 @@ class _TaskDetailsState extends State { 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; @@ -33,11 +42,22 @@ class _TaskDetailsState extends State { void initState() { super.initState(); description = widget.task.description; - project = widget.task.project!; + project = widget.task.project ?? '-'; status = widget.task.status; - priority = widget.task.priority!; + priority = widget.task.priority ?? '-'; due = widget.task.due ?? '-'; - due = _buildDate(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(); @@ -47,6 +67,10 @@ class _TaskDetailsState extends State { @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(); } @@ -109,7 +133,7 @@ class _TaskDetailsState extends State { _buildSelectableDetail( '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPagePriority}:', priority, - ['H', 'M', 'L'], (value) { + ['H', 'M', 'L', '-'], (value) { setState(() { priority = value; hasChanges = true; @@ -123,10 +147,52 @@ class _TaskDetailsState extends State { hasChanges = true; }); }), - _buildDetail('UUID:', widget.task.uuid!), + _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.toString()), + widget.task.urgency?.toStringAsFixed(2) ?? '-'), _buildDetail( '${SentenceManager(currentLanguage: AppSettings.selectedLanguage).sentences.detailPageEnd}:', _buildDate(widget.task.end)), @@ -136,6 +202,11 @@ class _TaskDetailsState extends State { _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') + : '-'), ], ), ), @@ -183,7 +254,9 @@ class _TaskDetailsState extends State { onTap: () async { final DateTime? pickedDate = await showDatePicker( context: context, - initialDate: value != '-' ? DateTime.parse(value) : DateTime.now(), + initialDate: value != '-' + ? DateTime.tryParse(value) ?? DateTime.now() + : DateTime.now(), firstDate: DateTime(2000), lastDate: DateTime(2101), builder: (BuildContext context, Widget? child) { @@ -196,8 +269,9 @@ class _TaskDetailsState extends State { if (pickedDate != null) { final TimeOfDay? pickedTime = await showTimePicker( context: context, - initialTime: TimeOfDay.fromDateTime( - value != '-' ? DateTime.parse(value) : DateTime.now()), + initialTime: TimeOfDay.fromDateTime(value != '-' + ? DateTime.tryParse(value) ?? DateTime.now() + : DateTime.now()), ); if (pickedTime != null) { final DateTime fullDateTime = DateTime( @@ -207,6 +281,9 @@ class _TaskDetailsState extends State { 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)); } } }, @@ -414,25 +491,36 @@ class _TaskDetailsState extends State { } Future _saveTask() async { - await taskDatabase.saveEditedTaskInDB( - widget.task.uuid!, - description, - project, - status, - priority, - due, - ); + // 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; }); - modifyTaskOnTaskwarrior( - description, - project, - due, - priority, - status, - widget.task.uuid!, - ); + // Assuming modifyTaskOnTaskwarrior takes the updated TaskForC object + // await modifyTaskOnTaskwarrior(updatedTask); } }