diff --git a/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt new file mode 100644 index 0000000..0c86eaf --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/AppDatabase.kt @@ -0,0 +1,31 @@ +package com.planify.mobile.data.local + +import androidx.room.Database +import androidx.room.RoomDatabase +import com.planify.mobile.data.local.dao.ProjectDao +import com.planify.mobile.data.local.dao.SectionDao +import com.planify.mobile.data.local.dao.TaskDao +import com.planify.mobile.data.local.entity.LabelEntity +import com.planify.mobile.data.local.entity.ProjectEntity +import com.planify.mobile.data.local.entity.ReminderEntity +import com.planify.mobile.data.local.entity.SectionEntity +import com.planify.mobile.data.local.entity.SourceEntity +import com.planify.mobile.data.local.entity.TaskEntity + +@Database( + entities = [ + TaskEntity::class, + ProjectEntity::class, + SectionEntity::class, + LabelEntity::class, + ReminderEntity::class, + SourceEntity::class, + ], + version = 1, + exportSchema = true, +) +abstract class AppDatabase : RoomDatabase() { + abstract fun taskDao(): TaskDao + abstract fun projectDao(): ProjectDao + abstract fun sectionDao(): SectionDao +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt new file mode 100644 index 0000000..09fc679 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/ProjectDao.kt @@ -0,0 +1,40 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.ProjectEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface ProjectDao { + + @Query("SELECT * FROM projects WHERE is_deleted = 0 ORDER BY child_order ASC") + fun getAllProjects(): Flow> + + @Query("SELECT * FROM projects WHERE is_favorite = 1 AND is_deleted = 0 ORDER BY child_order ASC") + fun getFavoriteProjects(): Flow> + + @Query("SELECT * FROM projects WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC") + fun getSubProjects(parentId: String): Flow> + + @Query("SELECT * FROM projects WHERE id = :id") + suspend fun getById(id: String): ProjectEntity? + + @Query("SELECT * FROM projects WHERE is_inbox = 1 LIMIT 1") + suspend fun getInboxProject(): ProjectEntity? + + @Query("SELECT * FROM projects WHERE calendar_url = :calendarUrl LIMIT 1") + suspend fun getByCalendarUrl(calendarUrl: String): ProjectEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(project: ProjectEntity) + + @Update + suspend fun update(project: ProjectEntity) + + @Query("UPDATE projects SET is_deleted = 1 WHERE id = :id") + suspend fun softDelete(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt new file mode 100644 index 0000000..d51e3bf --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/SectionDao.kt @@ -0,0 +1,28 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.SectionEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface SectionDao { + + @Query("SELECT * FROM sections WHERE project_id = :projectId AND is_deleted = 0 ORDER BY `order` ASC") + fun getSectionsByProject(projectId: String): Flow> + + @Query("SELECT * FROM sections WHERE id = :id") + suspend fun getById(id: String): SectionEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(section: SectionEntity) + + @Update + suspend fun update(section: SectionEntity) + + @Query("UPDATE sections SET is_deleted = 1 WHERE id = :id") + suspend fun softDelete(id: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt b/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt new file mode 100644 index 0000000..f4a05b0 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/dao/TaskDao.kt @@ -0,0 +1,62 @@ +package com.planify.mobile.data.local.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Update +import com.planify.mobile.data.local.entity.TaskEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface TaskDao { + + @Query("SELECT * FROM tasks WHERE project_id = :projectId AND parent_id IS NULL AND is_deleted = 0 ORDER BY child_order ASC") + fun getTasksByProject(projectId: String): Flow> + + @Query("SELECT * FROM tasks WHERE section_id = :sectionId AND parent_id IS NULL AND is_deleted = 0 ORDER BY child_order ASC") + fun getTasksBySection(sectionId: String): Flow> + + @Query(""" + SELECT t.* FROM tasks t + INNER JOIN projects p ON t.project_id = p.id + WHERE p.is_inbox = 1 AND t.parent_id IS NULL AND t.is_deleted = 0 + ORDER BY t.child_order ASC + """) + fun getInboxTasks(): Flow> + + @Query(""" + SELECT * FROM tasks + WHERE date(due_date) = date('now') AND checked = 0 AND is_deleted = 0 + ORDER BY child_order ASC + """) + fun getTodayTasks(): Flow> + + @Query(""" + SELECT * FROM tasks + WHERE date(due_date) < date('now') AND checked = 0 AND is_deleted = 0 + ORDER BY due_date ASC + """) + fun getOverdueTasks(): Flow> + + @Query("SELECT * FROM tasks WHERE parent_id = :parentId AND is_deleted = 0 ORDER BY child_order ASC") + fun getSubTasks(parentId: String): Flow> + + @Query("SELECT * FROM tasks WHERE id = :id") + suspend fun getTaskById(id: String): TaskEntity? + + @Query("SELECT * FROM tasks WHERE ical_url = :icalUrl LIMIT 1") + suspend fun getTaskByIcalUrl(icalUrl: String): TaskEntity? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(task: TaskEntity) + + @Update + suspend fun update(task: TaskEntity) + + @Query("UPDATE tasks SET checked = :checked, completed_at = :completedAt, updated_at = :updatedAt WHERE id = :id") + suspend fun setChecked(id: String, checked: Boolean, completedAt: String?, updatedAt: String) + + @Query("UPDATE tasks SET is_deleted = 1, updated_at = :updatedAt WHERE id = :id") + suspend fun softDelete(id: String, updatedAt: String) +} diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt new file mode 100644 index 0000000..ae052bd --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/LabelEntity.kt @@ -0,0 +1,17 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "labels") +data class LabelEntity( + @PrimaryKey val id: String, + val name: String, + val color: String, + val order: Int = 0, + @ColumnInfo(name = "source_id") val sourceId: String? = null, + @ColumnInfo(name = "backend_type") val backendType: String = "LOCAL", + @ColumnInfo(name = "is_favorite") val isFavorite: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt new file mode 100644 index 0000000..242ad55 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/ProjectEntity.kt @@ -0,0 +1,26 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "projects") +data class ProjectEntity( + @PrimaryKey val id: String, + val name: String, + val color: String = "#4CAF50", + val emoji: String? = null, + @ColumnInfo(name = "parent_id") val parentId: String? = null, + @ColumnInfo(name = "source_id") val sourceId: String? = null, + @ColumnInfo(name = "backend_type") val backendType: String = "LOCAL", + @ColumnInfo(name = "is_inbox") val isInbox: Boolean = false, + @ColumnInfo(name = "is_favorite") val isFavorite: Boolean = false, + @ColumnInfo(name = "is_archived") val isArchived: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, + @ColumnInfo(name = "view_style") val viewStyle: String = "LIST", + @ColumnInfo(name = "sorted_by") val sortedBy: String = "MANUAL", + @ColumnInfo(name = "sort_ascending") val sortAscending: Boolean = true, + @ColumnInfo(name = "child_order") val childOrder: Int = 0, + @ColumnInfo(name = "calendar_url") val calendarUrl: String? = null, + @ColumnInfo(name = "sync_id") val syncId: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt new file mode 100644 index 0000000..2f20dde --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/ReminderEntity.kt @@ -0,0 +1,27 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "reminders", + foreignKeys = [ + ForeignKey( + entity = TaskEntity::class, + parentColumns = ["id"], + childColumns = ["task_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index("task_id")] +) +data class ReminderEntity( + @PrimaryKey val id: String, + @ColumnInfo(name = "task_id") val taskId: String, + val type: String, + @ColumnInfo(name = "due_date") val dueDate: String? = null, + @ColumnInfo(name = "minutes_offset") val minutesOffset: Int? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt new file mode 100644 index 0000000..b91b1c1 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/SectionEntity.kt @@ -0,0 +1,31 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "sections", + foreignKeys = [ + ForeignKey( + entity = ProjectEntity::class, + parentColumns = ["id"], + childColumns = ["project_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [Index("project_id")] +) +data class SectionEntity( + @PrimaryKey val id: String, + val name: String, + @ColumnInfo(name = "project_id") val projectId: String, + val order: Int = 0, + @ColumnInfo(name = "is_archived") val isArchived: Boolean = false, + @ColumnInfo(name = "is_deleted") val isDeleted: Boolean = false, + val collapsed: Boolean = false, + @ColumnInfo(name = "ical_url") val icalUrl: String? = null, + val etag: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt new file mode 100644 index 0000000..5522b04 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/SourceEntity.kt @@ -0,0 +1,17 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "sources") +data class SourceEntity( + @PrimaryKey val id: String, + val type: String, + @ColumnInfo(name = "display_name") val displayName: String, + @ColumnInfo(name = "added_at") val addedAt: String, + @ColumnInfo(name = "updated_at") val updatedAt: String, + @ColumnInfo(name = "is_visible") val isVisible: Boolean = true, + @ColumnInfo(name = "last_sync") val lastSync: String? = null, + val data: String = "{}", +) diff --git a/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt new file mode 100644 index 0000000..868c8ff --- /dev/null +++ b/app/src/main/java/com/planify/mobile/data/local/entity/TaskEntity.kt @@ -0,0 +1,48 @@ +package com.planify.mobile.data.local.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +@Entity( + tableName = "tasks", + foreignKeys = [ + ForeignKey( + entity = ProjectEntity::class, + parentColumns = ["id"], + childColumns = ["project_id"], + onDelete = ForeignKey.CASCADE + ) + ], + indices = [ + Index("project_id"), + Index("section_id"), + Index("parent_id") + ] +) +data class TaskEntity( + @PrimaryKey val id: String, + val content: String, + val description: String = "", + @ColumnInfo(name = "project_id") val projectId: String, + @ColumnInfo(name = "section_id") val sectionId: String? = null, + @ColumnInfo(name = "parent_id") val parentId: String? = null, + val priority: Int = 4, + val checked: Boolean = false, + @ColumnInfo(name = "due_date") val dueDate: String? = null, + @ColumnInfo(name = "deadline_date") val deadlineDate: String? = null, + val labels: String = "[]", + val pinned: Boolean = false, + val collapsed: Boolean = false, + @ColumnInfo(name = "child_order") val childOrder: Int = 0, + @ColumnInfo(name = "added_at") val addedAt: String = "", + @ColumnInfo(name = "updated_at") val updatedAt: String = "", + @ColumnInfo(name = "completed_at") val completedAt: String? = null, + @ColumnInfo(name = "item_type") val itemType: String = "TASK", + @ColumnInfo(name = "calendar_event_uid") val calendarEventUid: String? = null, + @ColumnInfo(name = "ical_url") val icalUrl: String? = null, + val etag: String? = null, + @ColumnInfo(name = "responsible_uid") val responsibleUid: String? = null, +) diff --git a/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt new file mode 100644 index 0000000..e8d5238 --- /dev/null +++ b/app/src/main/java/com/planify/mobile/di/DatabaseModule.kt @@ -0,0 +1,27 @@ +package com.planify.mobile.di + +import android.content.Context +import androidx.room.Room +import com.planify.mobile.data.local.AppDatabase +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object DatabaseModule { + + @Provides + @Singleton + fun provideDatabase(@ApplicationContext context: Context): AppDatabase = + Room.databaseBuilder(context, AppDatabase::class.java, "planify.db") + .fallbackToDestructiveMigration() + .build() + + @Provides fun provideTaskDao(db: AppDatabase) = db.taskDao() + @Provides fun provideProjectDao(db: AppDatabase) = db.projectDao() + @Provides fun provideSectionDao(db: AppDatabase) = db.sectionDao() +}