CrmDatabase.kt 142 lignes · 6002 octets
package fr.ebii.card2vcf.data

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import fr.ebii.card2vcf.sync.SyncMetaDao
import fr.ebii.card2vcf.sync.SyncMetaEntity
import fr.ebii.card2vcf.sync.SyncOpDao
import fr.ebii.card2vcf.sync.SyncOpEntity

@Database(
    entities = [
        CrmContactEntity::class,
        CrmContactFts::class,
        DuplicateExclusionEntity::class,
        EntrepriseEntity::class,
        ProjetEntity::class,
        TacheEntity::class,
        InteractionEntity::class,
        WorkflowEntity::class,
        SyncOpEntity::class,
        SyncMetaEntity::class,
        RdvEntity::class,
        ReservationEntity::class,
        IndisponibiliteEntity::class,
        NoteProjetEntity::class,
    ],
    version = 8,
    exportSchema = false,
)
@TypeConverters(Converters::class)
abstract class CrmDatabase : RoomDatabase() {
    abstract fun crmContactDao(): CrmContactDao
    abstract fun entrepriseDao(): EntrepriseDao
    abstract fun projetDao(): ProjetDao
    abstract fun tacheDao(): TacheDao
    abstract fun interactionDao(): InteractionDao
    abstract fun workflowDao(): WorkflowDao
    abstract fun syncOpDao(): SyncOpDao
    abstract fun syncMetaDao(): SyncMetaDao
    abstract fun rdvDao(): RdvDao
    abstract fun reservationDao(): ReservationDao
    abstract fun indisponibiliteDao(): IndisponibiliteDao
    abstract fun noteProjetDao(): NoteProjetDao

    companion object {
        @Volatile private var instance: CrmDatabase? = null

        /** v3→v4 : colonne `lastError` sur `sync_ops` (état d'échec de push par op). */
        val MIGRATION_3_4 = object : Migration(3, 4) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE sync_ops ADD COLUMN lastError TEXT")
            }
        }

        /** v5→v6 : colonnes `audioPath` et `transcriptionStatut` sur la table `interactions`. */
        val MIGRATION_5_6 = object : Migration(5, 6) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE interactions ADD COLUMN audioPath TEXT")
                db.execSQL("ALTER TABLE interactions ADD COLUMN transcriptionStatut TEXT")
            }
        }

        /** v6→v7 : colonne `transcriptionErreur` sur `interactions` et `notes_projet`. */
        val MIGRATION_6_7 = object : Migration(6, 7) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE interactions ADD COLUMN transcriptionErreur TEXT")
                db.execSQL("ALTER TABLE notes_projet ADD COLUMN transcriptionErreur TEXT")
            }
        }

        /** v4→v5 : table `notes_projet` (notes markdown de projet, champs audio pour phase 2). */
        val MIGRATION_4_5 = object : Migration(4, 5) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL(
                    """
                    CREATE TABLE IF NOT EXISTS notes_projet (
                        localId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
                        serverId TEXT,
                        projetServerId TEXT NOT NULL,
                        titre TEXT NOT NULL,
                        texte TEXT NOT NULL,
                        audioPath TEXT,
                        transcriptionStatut TEXT,
                        auteur TEXT NOT NULL,
                        createdAt INTEGER NOT NULL,
                        updatedAt INTEGER
                    )
                    """.trimIndent(),
                )
            }
        }

        /**
         * v7→v8 : planification des tâches (début, durée en jours ouvrés,
         * étiquette, regroupement, dépendances, sous-tâches) et réglages de
         * planification du projet. Le serveur envoyait déjà ces champs dans le
         * pull ; le client les ignorait.
         */
        val MIGRATION_7_8 = object : Migration(7, 8) {
            override fun migrate(db: SupportSQLiteDatabase) {
                db.execSQL("ALTER TABLE taches ADD COLUMN debut TEXT")
                db.execSQL("ALTER TABLE taches ADD COLUMN dureeJours INTEGER")
                db.execSQL("ALTER TABLE taches ADD COLUMN etiquette TEXT")
                db.execSQL("ALTER TABLE taches ADD COLUMN parentId TEXT")
                db.execSQL("ALTER TABLE taches ADD COLUMN dependDeJson TEXT NOT NULL DEFAULT '[]'")
                db.execSQL("ALTER TABLE taches ADD COLUMN sousTachesJson TEXT NOT NULL DEFAULT '[]'")
                db.execSQL("ALTER TABLE projets ADD COLUMN planification INTEGER NOT NULL DEFAULT 0")
                db.execSQL("ALTER TABLE projets ADD COLUMN echeance TEXT")
                db.execSQL("ALTER TABLE projets ADD COLUMN joursOuvres INTEGER NOT NULL DEFAULT 7")
            }
        }

        /**
         * v1/v2/v3 sync schema : pas de migration incrémentale — reset local à l'upgrade (acceptable v1-v3).
         * À partir de v3→v4 les migrations sont additives ; le fallback destructif reste en filet.
         * Les tests utilisent Room.inMemoryDatabaseBuilder sans fallback destructif.
         */
        fun get(context: Context): CrmDatabase =
            instance ?: synchronized(this) {
                instance ?: Room.databaseBuilder(
                    context.applicationContext,
                    CrmDatabase::class.java,
                    "card2vcf-crm.db",
                )
                    .addMigrations(
                        MIGRATION_3_4,
                        MIGRATION_4_5,
                        MIGRATION_5_6,
                        MIGRATION_6_7,
                        MIGRATION_7_8,
                    )
                    .fallbackToDestructiveMigration()
                    .build()
                    .also { instance = it }
            }
    }
}