SyncModels.kt 404 lignes · 11549 octets
package fr.ebii.card2vcf.sync

import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNamingStrategy

/** JSON config partagée : snake_case serveur Projectiaon, champs inconnus ignorés. */
@OptIn(ExperimentalSerializationApi::class)
val syncJson: Json = Json {
    ignoreUnknownKeys = true
    namingStrategy = JsonNamingStrategy.SnakeCase
    encodeDefaults = true
}

// ---- Auth clé ----

@Serializable
data class AuthCleRequest(
    val nom: String,
    val motDePasse: String,
)

@Serializable
data class AuthCleResponse(
    val nom: String,
    val cleChiffree: String,
    val sel: String,
    val kdf: String,
    val cipher: String,
)

// ---- Sync status / pull ----

@Serializable
data class SyncChanges(
    val contacts: Int = 0,
    val entreprises: Int = 0,
    val projets: Int = 0,
    val taches: Int = 0,
    val interactions: Int = 0,
    val rdv: Int = 0,
    val reservations: Int = 0,
    val indisponibilites: Int = 0,
    val tombstones: Int = 0,
)

@Serializable
data class SyncStatusResponse(
    val serverTime: String,
    val changes: SyncChanges,
    val total: Int,
)

@Serializable
data class SyncPullResponse(
    val serverTime: String,
    val contacts: List<ContactDto> = emptyList(),
    val entreprises: List<EntrepriseDto> = emptyList(),
    val projets: List<ProjetDto> = emptyList(),
    val taches: List<TacheSyncDto> = emptyList(),
    val interactions: List<InteractionDto> = emptyList(),
    val rdv: List<RendezVousDto> = emptyList(),
    val reservations: List<ReservationDto> = emptyList(),
    val indisponibilites: List<IndisponibiliteDto> = emptyList(),
    val tombstones: List<TombstoneDto> = emptyList(),
    val workflows: List<WorkflowDto> = emptyList(),
    /** Notes markdown des projets ; défaut `emptyList()` = compat anciens serveurs sans ce champ. */
    val notes: List<NoteProjetDto> = emptyList(),
)

@Serializable
data class TombstoneDto(
    @SerialName("type") val entityType: String,
    val id: String,
    val projetId: String? = null,
    val cibleType: String? = null,
    val cibleId: String? = null,
    val utilisateur: String? = null,
    val supprimeLe: String,
)

// ---- Entités sync (champs minimaux pour LWW / merge) ----

@Serializable
data class ContactDto(
    val id: String,
    val prenom: String = "",
    val nom: String = "",
    val entrepriseId: String? = null,
    val fonction: String = "",
    val statut: String = "prospect",
    val etape: String = "nouveau",
    val tags: List<String> = emptyList(),
    val creePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
    /** Nom de fichier serveur (`carte.jpg`), pas les octets. */
    val carteVisite: String? = null,
    /** Nom de fichier serveur (`photo.jpg`), pas les octets. */
    val photo: String? = null,
)

@Serializable
data class EntrepriseDto(
    val id: String,
    val nom: String,
    val secteur: String = "",
    val creePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
)

@Serializable
data class MembreProjetDto(
    val utilisateur: String,
    val niveau: String = "lecteur",
)

@Serializable
data class ProjetDto(
    val id: String,
    val nom: String,
    val description: String = "",
    val workflowId: String,
    val creePar: String = "",
    val creeLe: String,
    val membres: List<MembreProjetDto> = emptyList(),
    /** Le projet utilise-t-il la planification ? Défaut faux = serveur antérieur. */
    val planification: Boolean = false,
    val echeance: String? = null,
    /** 5 = lun-ven, 6 = lun-sam, 7 = tous les jours. */
    val joursOuvres: Int = 7,
    val misAJourLe: String? = null,
)

/** Élément de checklist d'une tâche (miroir Rust `SousTache`). */
@Serializable
data class SousTacheDto(
    val id: String,
    val titre: String,
    val fait: Boolean = false,
)

/** Tâche aplatie : `projet_id` + champs Tache au même niveau (miroir Rust `TacheSync`). */
@Serializable
data class TacheSyncDto(
    val projetId: String,
    val id: String,
    val titre: String,
    val description: String = "",
    val colonneId: String,
    val ordre: Int = 0,
    val auteur: String = "",
    val creeLe: String,
    val assigneA: String? = null,
    /** Premier jour planifié, `AAAA-MM-JJ`. */
    val debut: String? = null,
    /** Durée en jours **ouvrés** selon le rythme du projet. */
    val dureeJours: Int? = null,
    val etiquette: String? = null,
    /** Transportés et stockés pour ne pas les perdre à l'écriture, non éditables. */
    val dependDe: List<String> = emptyList(),
    val parentId: String? = null,
    val sousTaches: List<SousTacheDto> = emptyList(),
    val misAJourLe: String? = null,
)

@Serializable
data class InteractionDto(
    val id: String,
    val contactId: String,
    val typeInteraction: String = "note",
    val sujet: String = "",
    val description: String = "",
    val creePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
    /** Nom de fichier audio côté serveur (ex. `interaction_uuid.wav`). */
    val pieceJointe: String? = null,
    /** Statut de transcription : "en_attente" | "terminee" | "echec" | null. */
    val transcription: String? = null,
    /** Message d'erreur de transcription côté serveur (null si pas d'erreur). */
    val transcriptionErreur: String? = null,
)

@Serializable
data class ColonneDto(
    val id: String,
    val nom: String,
    val ordre: Int = 0,
)

/** Cible d'une réservation/indisponibilité (miroir enum Rust `CibleRessource`, tag `type` + `id`). */
@Serializable
data class CibleRessourceDto(
    val type: String,
    val id: String,
)

@Serializable
data class RendezVousDto(
    val id: String,
    val titre: String = "",
    val description: String = "",
    val utilisateur: String = "",
    val contactIds: List<String> = emptyList(),
    val lieu: String = "",
    val debut: String,
    val fin: String,
    val projetId: String? = null,
    val creePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
)

@Serializable
data class ReservationDto(
    val id: String,
    val cible: CibleRessourceDto,
    val debut: String,
    val fin: String,
    val motif: String = "",
    val projetId: String? = null,
    val rdvId: String? = null,
    val statut: String = "active",
    val reservePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
)

@Serializable
data class IndisponibiliteDto(
    val id: String,
    val cible: CibleRessourceDto,
    val nature: String,
    val debut: String,
    val fin: String,
    val commentaire: String = "",
    val creePar: String = "",
    val creeLe: String,
    val misAJourLe: String? = null,
)

/** Note markdown d'un projet (contrat P1-S1 / P1-A2 / P2-A3). */
@Serializable
data class NoteProjetDto(
    val id: String,
    val projetId: String,
    val titre: String,
    val contenu: String,
    val auteur: String = "",
    val creeLe: String,
    val majLe: String? = null,
    /** Nom de fichier audio côté serveur (ex. `note_uuid.wav`), null si aucun audio. */
    val audio: String? = null,
    /** Statut de transcription : "en_attente" | "terminee" | "echec" | null. */
    val transcription: String? = null,
    /** Message d'erreur de transcription côté serveur (null si pas d'erreur). */
    val transcriptionErreur: String? = null,
)

/** Ressource catalogue allégée (salle/matériel/véhicule) : champs communs uniquement. */
@Serializable
data class RessourceItemDto(
    val id: String,
    val nom: String,
    val description: String = "",
    val lieu: String = "",
    val actif: Boolean = true,
)

@Serializable
data class RessourcesCatalogueDto(
    val salles: List<RessourceItemDto> = emptyList(),
    val materiels: List<RessourceItemDto> = emptyList(),
    val vehicules: List<RessourceItemDto> = emptyList(),
)

@Serializable
data class WorkflowDto(
    val id: String,
    val nom: String,
    val description: String = "",
    val colonnes: List<ColonneDto> = emptyList(),
    val creePar: String = "",
    val creeLe: String,
)

// ---- Requêtes de mutation locales (payload des SyncOp, miroir des `*Input` serveur) ----

@Serializable
data class CreateTacheRequest(
    val titre: String,
    val description: String = "",
    val colonneId: String = "",
    val assigneA: String? = null,
    val debut: String? = null,
    val dureeJours: Int? = null,
    val etiquette: String? = null,
)

/**
 * Mise à jour d'une tâche.
 *
 * Le serveur applique une sémantique partielle : un champ **absent** du JSON
 * est laissé tel quel, `null` l'efface. Cette classe ne déclare donc que ce que
 * le téléphone édite — sous-tâches, dépendances et regroupement en sont
 * volontairement exclus pour ne pas être écrasés.
 *
 * `syncJson` encode les valeurs par défaut, y compris les nuls : vider le champ
 * date dans le dialogue efface bien la date côté serveur.
 */
@Serializable
data class UpdateTacheRequest(
    val titre: String,
    val description: String = "",
    val assigneA: String? = null,
    @SerialName("assigne_a_pose") val assigneAPose: Boolean = true,
    val debut: String? = null,
    val dureeJours: Int? = null,
    val etiquette: String? = null,
)

@Serializable
data class MoveTacheRequest(
    val colonneId: String,
)

@Serializable
data class CreateInteractionRequest(
    val sujet: String,
    val description: String = "",
    val typeInteraction: String = "note",
    val demandeTranscription: Boolean = false,
)

@Serializable
data class ContactValeurDto(
    val valeur: String,
    val categorie: String = "pro",
)

/** Payload create/update contact (miroir serveur `ContactInput`). */
@Serializable
data class CreateContactRequest(
    val prenom: String = "",
    val nom: String = "",
    val entrepriseId: String? = null,
    val entrepriseNom: String? = null,
    val fonction: String = "",
    val emails: List<ContactValeurDto> = emptyList(),
    val telephones: List<ContactValeurDto> = emptyList(),
    val notes: String = "",
    val statut: String = "prospect",
    val etape: String = "nouveau",
    val tags: List<String> = emptyList(),
)

/** Payload create projet (miroir serveur `ProjetInput`, body `/api/projets`). */
@Serializable
data class CreateProjetRequest(
    val nom: String,
    val description: String = "",
    val workflowId: String,
)

/** Payload create/update RDV (miroir serveur, body `/api/rdv`). */
@Serializable
data class RdvUpsertRequest(
    val titre: String = "",
    val description: String = "",
    val lieu: String = "",
    val debut: String,
    val fin: String,
    val contactIds: List<String> = emptyList(),
    val projetId: String? = null,
)

/** Payload create/update réservation (miroir serveur, body `/api/ressources/:genre/:id/reservations`). */
@Serializable
data class ReservationUpsertRequest(
    val debut: String,
    val fin: String,
    val motif: String = "",
    val projetId: String? = null,
    val rdvId: String? = null,
)

/** Payload création note projet (body `POST /api/projets/:id/notes`). */
@Serializable
data class CreateNoteProjetRequest(
    val titre: String,
    val contenu: String = "",
    val demandeTranscription: Boolean = false,
)

/** Payload mise à jour note projet (body `PUT /api/projets/:id/notes/:nid`). */
@Serializable
data class UpdateNoteProjetRequest(
    val titre: String,
    val contenu: String = "",
)