SyncModels.swift
809 lignes · 31525 octets
import Foundation /// Shared JSON config: snake_case wire format (Projectiaon server), unknown fields ignored. /// Mirror of Kotlin `syncJson` (`JsonNamingStrategy.SnakeCase`, `ignoreUnknownKeys`, `encodeDefaults`). enum SyncJson { static func encoder() -> JSONEncoder { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase return encoder } static func decoder() -> JSONDecoder { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase return decoder } static func encodeToString<T: Encodable>(_ value: T) -> String { guard let data = try? encoder().encode(value) else { return "{}" } return String(data: data, encoding: .utf8) ?? "{}" } static func decode<T: Decodable>(_ type: T.Type, from string: String) throws -> T { try decoder().decode(type, from: Data(string.utf8)) } } // ---- Auth clé ---- struct AuthCleRequest: Codable, Equatable { var nom: String var motDePasse: String } struct AuthCleResponse: Codable, Equatable { var nom: String var cleChiffree: String var sel: String var kdf: String var cipher: String } // ---- Sync status / pull ---- struct SyncChanges: Codable, Equatable { var contacts: Int = 0 var entreprises: Int = 0 var projets: Int = 0 var taches: Int = 0 var interactions: Int = 0 var rdv: Int = 0 var reservations: Int = 0 var indisponibilites: Int = 0 var tombstones: Int = 0 } extension SyncChanges { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) contacts = try c.decodeIfPresent(Int.self, forKey: .contacts) ?? 0 entreprises = try c.decodeIfPresent(Int.self, forKey: .entreprises) ?? 0 projets = try c.decodeIfPresent(Int.self, forKey: .projets) ?? 0 taches = try c.decodeIfPresent(Int.self, forKey: .taches) ?? 0 interactions = try c.decodeIfPresent(Int.self, forKey: .interactions) ?? 0 rdv = try c.decodeIfPresent(Int.self, forKey: .rdv) ?? 0 reservations = try c.decodeIfPresent(Int.self, forKey: .reservations) ?? 0 indisponibilites = try c.decodeIfPresent(Int.self, forKey: .indisponibilites) ?? 0 tombstones = try c.decodeIfPresent(Int.self, forKey: .tombstones) ?? 0 } } struct SyncStatusResponse: Codable, Equatable { var serverTime: String var changes: SyncChanges var total: Int } struct SyncPullResponse: Codable, Equatable { var serverTime: String var contacts: [ContactDto] = [] var entreprises: [EntrepriseDto] = [] var projets: [ProjetDto] = [] var taches: [TacheSyncDto] = [] var interactions: [InteractionDto] = [] var rdv: [RendezVousDto] = [] var reservations: [ReservationDto] = [] var indisponibilites: [IndisponibiliteDto] = [] var tombstones: [TombstoneDto] = [] var workflows: [WorkflowDto] = [] var notes: [NoteProjetDto] = [] } extension SyncPullResponse { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) serverTime = try c.decode(String.self, forKey: .serverTime) contacts = try c.decodeIfPresent([ContactDto].self, forKey: .contacts) ?? [] entreprises = try c.decodeIfPresent([EntrepriseDto].self, forKey: .entreprises) ?? [] projets = try c.decodeIfPresent([ProjetDto].self, forKey: .projets) ?? [] taches = try c.decodeIfPresent([TacheSyncDto].self, forKey: .taches) ?? [] interactions = try c.decodeIfPresent([InteractionDto].self, forKey: .interactions) ?? [] rdv = try c.decodeIfPresent([RendezVousDto].self, forKey: .rdv) ?? [] reservations = try c.decodeIfPresent([ReservationDto].self, forKey: .reservations) ?? [] indisponibilites = try c.decodeIfPresent([IndisponibiliteDto].self, forKey: .indisponibilites) ?? [] tombstones = try c.decodeIfPresent([TombstoneDto].self, forKey: .tombstones) ?? [] workflows = try c.decodeIfPresent([WorkflowDto].self, forKey: .workflows) ?? [] notes = try c.decodeIfPresent([NoteProjetDto].self, forKey: .notes) ?? [] } } struct TombstoneDto: Codable, Equatable { var entityType: String var id: String var projetId: String? = nil var cibleType: String? = nil var cibleId: String? = nil var utilisateur: String? = nil var supprimeLe: String enum CodingKeys: String, CodingKey { // Kotlin `@SerialName("type")`. case entityType = "type" case id case projetId case cibleType case cibleId case utilisateur case supprimeLe } } extension TombstoneDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) entityType = try c.decode(String.self, forKey: .entityType) id = try c.decode(String.self, forKey: .id) projetId = try c.decodeIfPresent(String.self, forKey: .projetId) cibleType = try c.decodeIfPresent(String.self, forKey: .cibleType) cibleId = try c.decodeIfPresent(String.self, forKey: .cibleId) utilisateur = try c.decodeIfPresent(String.self, forKey: .utilisateur) supprimeLe = try c.decode(String.self, forKey: .supprimeLe) } } // ---- Entités sync (champs minimaux pour LWW / merge) ---- struct ContactDto: Codable, Equatable { var id: String var prenom: String = "" var nom: String = "" var entrepriseId: String? = nil var fonction: String = "" var statut: String = "prospect" var etape: String = "nouveau" var tags: [String] = [] var creePar: String = "" var creeLe: String var misAJourLe: String? = nil /// Server-side file name (`carte.jpg`), not the bytes. var carteVisite: String? = nil /// Server-side file name (`photo.jpg`), not the bytes. var photo: String? = nil } extension ContactDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) prenom = try c.decodeIfPresent(String.self, forKey: .prenom) ?? "" nom = try c.decodeIfPresent(String.self, forKey: .nom) ?? "" entrepriseId = try c.decodeIfPresent(String.self, forKey: .entrepriseId) fonction = try c.decodeIfPresent(String.self, forKey: .fonction) ?? "" statut = try c.decodeIfPresent(String.self, forKey: .statut) ?? "prospect" etape = try c.decodeIfPresent(String.self, forKey: .etape) ?? "nouveau" tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? [] creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) carteVisite = try c.decodeIfPresent(String.self, forKey: .carteVisite) photo = try c.decodeIfPresent(String.self, forKey: .photo) } } struct EntrepriseDto: Codable, Equatable { var id: String var nom: String var secteur: String = "" var creePar: String = "" var creeLe: String var misAJourLe: String? = nil } extension EntrepriseDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) nom = try c.decode(String.self, forKey: .nom) secteur = try c.decodeIfPresent(String.self, forKey: .secteur) ?? "" creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } struct MembreProjetDto: Codable, Equatable { var utilisateur: String var niveau: String = "lecteur" } extension MembreProjetDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) utilisateur = try c.decode(String.self, forKey: .utilisateur) niveau = try c.decodeIfPresent(String.self, forKey: .niveau) ?? "lecteur" } } struct ProjetDto: Codable, Equatable { var id: String var nom: String var description: String = "" var workflowId: String var creePar: String = "" var creeLe: String var membres: [MembreProjetDto] = [] /// Le projet utilise-t-il la planification ? Défaut faux = serveur antérieur. var planification: Bool = false var echeance: String? = nil /// 5 = lun-ven, 6 = lun-sam, 7 = tous les jours. var joursOuvres: Int = 7 var misAJourLe: String? = nil } extension ProjetDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) nom = try c.decode(String.self, forKey: .nom) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" workflowId = try c.decode(String.self, forKey: .workflowId) creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) membres = try c.decodeIfPresent([MembreProjetDto].self, forKey: .membres) ?? [] planification = try c.decodeIfPresent(Bool.self, forKey: .planification) ?? false echeance = try c.decodeIfPresent(String.self, forKey: .echeance) joursOuvres = try c.decodeIfPresent(Int.self, forKey: .joursOuvres) ?? 7 misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } /// Élément de checklist d'une tâche (miroir Rust `SousTache`). struct SousTacheDto: Codable, Equatable { var id: String var titre: String var fait: Bool = false } extension SousTacheDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) titre = try c.decode(String.self, forKey: .titre) fait = try c.decodeIfPresent(Bool.self, forKey: .fait) ?? false } } /// Flattened task: `projet_id` + Tache fields at the same level (mirror of Rust `TacheSync`). struct TacheSyncDto: Codable, Equatable { var projetId: String var id: String var titre: String var description: String = "" var colonneId: String var ordre: Int = 0 var auteur: String = "" var creeLe: String var assigneA: String? = nil /// Premier jour planifié, `AAAA-MM-JJ`. var debut: String? = nil /// Durée en jours **ouvrés** selon le rythme du projet. var dureeJours: Int? = nil var etiquette: String? = nil /// Transportés et stockés pour ne pas les perdre à l'écriture, non éditables. var dependDe: [String] = [] var parentId: String? = nil var sousTaches: [SousTacheDto] = [] var misAJourLe: String? = nil } extension TacheSyncDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) projetId = try c.decode(String.self, forKey: .projetId) id = try c.decode(String.self, forKey: .id) titre = try c.decode(String.self, forKey: .titre) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" colonneId = try c.decode(String.self, forKey: .colonneId) ordre = try c.decodeIfPresent(Int.self, forKey: .ordre) ?? 0 auteur = try c.decodeIfPresent(String.self, forKey: .auteur) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) assigneA = try c.decodeIfPresent(String.self, forKey: .assigneA) debut = try c.decodeIfPresent(String.self, forKey: .debut) dureeJours = try c.decodeIfPresent(Int.self, forKey: .dureeJours) etiquette = try c.decodeIfPresent(String.self, forKey: .etiquette) dependDe = try c.decodeIfPresent([String].self, forKey: .dependDe) ?? [] parentId = try c.decodeIfPresent(String.self, forKey: .parentId) sousTaches = try c.decodeIfPresent([SousTacheDto].self, forKey: .sousTaches) ?? [] misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } struct NoteProjetDto: Codable, Equatable { var id: String var projetId: String var titre: String var contenu: String var auteur: String = "" var creeLe: String var majLe: String? = nil var audio: String? = nil var transcription: String? = nil var transcriptionErreur: String? = nil } extension NoteProjetDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) projetId = try c.decode(String.self, forKey: .projetId) titre = try c.decode(String.self, forKey: .titre) contenu = try c.decode(String.self, forKey: .contenu) auteur = try c.decodeIfPresent(String.self, forKey: .auteur) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) majLe = try c.decodeIfPresent(String.self, forKey: .majLe) audio = try c.decodeIfPresent(String.self, forKey: .audio) transcription = try c.decodeIfPresent(String.self, forKey: .transcription) transcriptionErreur = try c.decodeIfPresent(String.self, forKey: .transcriptionErreur) } } struct InteractionDto: Codable, Equatable { var id: String var contactId: String var typeInteraction: String = "note" var sujet: String = "" var description: String = "" var creePar: String = "" var creeLe: String var misAJourLe: String? = nil /// Nom de fichier côté serveur (pas un chemin local). var pieceJointe: String? = nil /// Statut de transcription : `en_attente`, `terminee`, `echec`. var transcription: String? = nil var transcriptionErreur: String? = nil } extension InteractionDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) contactId = try c.decode(String.self, forKey: .contactId) typeInteraction = try c.decodeIfPresent(String.self, forKey: .typeInteraction) ?? "note" sujet = try c.decodeIfPresent(String.self, forKey: .sujet) ?? "" description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) pieceJointe = try c.decodeIfPresent(String.self, forKey: .pieceJointe) transcription = try c.decodeIfPresent(String.self, forKey: .transcription) transcriptionErreur = try c.decodeIfPresent(String.self, forKey: .transcriptionErreur) } } struct ColonneDto: Codable, Equatable { var id: String var nom: String var ordre: Int = 0 } extension ColonneDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) nom = try c.decode(String.self, forKey: .nom) ordre = try c.decodeIfPresent(Int.self, forKey: .ordre) ?? 0 } } /// Reservation/unavailability target (mirror of Rust enum `CibleRessource`, tag `type` + `id`). struct CibleRessourceDto: Codable, Equatable { var type: String var id: String } struct RendezVousDto: Codable, Equatable { var id: String var titre: String = "" var description: String = "" var utilisateur: String = "" var contactIds: [String] = [] var lieu: String = "" var debut: String var fin: String var projetId: String? = nil var creePar: String = "" var creeLe: String var misAJourLe: String? = nil } extension RendezVousDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) titre = try c.decodeIfPresent(String.self, forKey: .titre) ?? "" description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" utilisateur = try c.decodeIfPresent(String.self, forKey: .utilisateur) ?? "" contactIds = try c.decodeIfPresent([String].self, forKey: .contactIds) ?? [] lieu = try c.decodeIfPresent(String.self, forKey: .lieu) ?? "" debut = try c.decode(String.self, forKey: .debut) fin = try c.decode(String.self, forKey: .fin) projetId = try c.decodeIfPresent(String.self, forKey: .projetId) creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } struct ReservationDto: Codable, Equatable { var id: String var cible: CibleRessourceDto var debut: String var fin: String var motif: String = "" var projetId: String? = nil var rdvId: String? = nil var statut: String = "active" var reservePar: String = "" var creeLe: String var misAJourLe: String? = nil } extension ReservationDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) cible = try c.decode(CibleRessourceDto.self, forKey: .cible) debut = try c.decode(String.self, forKey: .debut) fin = try c.decode(String.self, forKey: .fin) motif = try c.decodeIfPresent(String.self, forKey: .motif) ?? "" projetId = try c.decodeIfPresent(String.self, forKey: .projetId) rdvId = try c.decodeIfPresent(String.self, forKey: .rdvId) statut = try c.decodeIfPresent(String.self, forKey: .statut) ?? "active" reservePar = try c.decodeIfPresent(String.self, forKey: .reservePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } struct IndisponibiliteDto: Codable, Equatable { var id: String var cible: CibleRessourceDto var nature: String var debut: String var fin: String var commentaire: String = "" var creePar: String = "" var creeLe: String var misAJourLe: String? = nil } extension IndisponibiliteDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) cible = try c.decode(CibleRessourceDto.self, forKey: .cible) nature = try c.decode(String.self, forKey: .nature) debut = try c.decode(String.self, forKey: .debut) fin = try c.decode(String.self, forKey: .fin) commentaire = try c.decodeIfPresent(String.self, forKey: .commentaire) ?? "" creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) misAJourLe = try c.decodeIfPresent(String.self, forKey: .misAJourLe) } } /// Lightweight catalogue resource (salle/matériel/véhicule): shared fields only. struct RessourceItemDto: Codable, Equatable { var id: String var nom: String var description: String = "" var lieu: String = "" var actif: Bool = true } extension RessourceItemDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) nom = try c.decode(String.self, forKey: .nom) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" lieu = try c.decodeIfPresent(String.self, forKey: .lieu) ?? "" actif = try c.decodeIfPresent(Bool.self, forKey: .actif) ?? true } } struct RessourcesCatalogueDto: Codable, Equatable { var salles: [RessourceItemDto] = [] var materiels: [RessourceItemDto] = [] var vehicules: [RessourceItemDto] = [] } extension RessourcesCatalogueDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) salles = try c.decodeIfPresent([RessourceItemDto].self, forKey: .salles) ?? [] materiels = try c.decodeIfPresent([RessourceItemDto].self, forKey: .materiels) ?? [] vehicules = try c.decodeIfPresent([RessourceItemDto].self, forKey: .vehicules) ?? [] } } struct WorkflowDto: Codable, Equatable { var id: String var nom: String var description: String = "" var colonnes: [ColonneDto] = [] var creePar: String = "" var creeLe: String } extension WorkflowDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) id = try c.decode(String.self, forKey: .id) nom = try c.decode(String.self, forKey: .nom) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" colonnes = try c.decodeIfPresent([ColonneDto].self, forKey: .colonnes) ?? [] creePar = try c.decodeIfPresent(String.self, forKey: .creePar) ?? "" creeLe = try c.decode(String.self, forKey: .creeLe) } } // ---- Local mutation requests (SyncOp payloads, mirror of server `*Input`) ---- // Kotlin (`encodeDefaults` + `explicitNulls`) encodes nil optionals as JSON null; // the custom `encode(to:)` below reproduces that. struct CreateTacheRequest: Codable, Equatable { var titre: String var description: String = "" var colonneId: String = "" var assigneA: String? = nil enum CodingKeys: String, CodingKey { case titre, description, colonneId, assigneA } } extension CreateTacheRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) titre = try c.decode(String.self, forKey: .titre) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" colonneId = try c.decodeIfPresent(String.self, forKey: .colonneId) ?? "" assigneA = try c.decodeIfPresent(String.self, forKey: .assigneA) } func encode(to encoder: Encoder) throws { var c = encoder.container(keyedBy: CodingKeys.self) try c.encode(titre, forKey: .titre) try c.encode(description, forKey: .description) try c.encode(colonneId, forKey: .colonneId) try encodeExplicitNull(assigneA, in: &c, forKey: .assigneA) } } struct UpdateTacheRequest: Codable, Equatable { var titre: String var description: String = "" var assigneA: String? = nil /// Kotlin `@SerialName("assigne_a_pose")` — matches the snake_case strategy output. var assigneAPose: Bool = true enum CodingKeys: String, CodingKey { case titre, description, assigneA, assigneAPose } } extension UpdateTacheRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) titre = try c.decode(String.self, forKey: .titre) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" assigneA = try c.decodeIfPresent(String.self, forKey: .assigneA) assigneAPose = try c.decodeIfPresent(Bool.self, forKey: .assigneAPose) ?? true } func encode(to encoder: Encoder) throws { var c = encoder.container(keyedBy: CodingKeys.self) try c.encode(titre, forKey: .titre) try c.encode(description, forKey: .description) try encodeExplicitNull(assigneA, in: &c, forKey: .assigneA) try c.encode(assigneAPose, forKey: .assigneAPose) } } struct MoveTacheRequest: Codable, Equatable { var colonneId: String } struct CreateInteractionRequest: Codable, Equatable { var sujet: String var description: String = "" var typeInteraction: String = "note" var demandeTranscription: Bool = false } extension CreateInteractionRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) sujet = try c.decode(String.self, forKey: .sujet) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" typeInteraction = try c.decodeIfPresent(String.self, forKey: .typeInteraction) ?? "note" demandeTranscription = try c.decodeIfPresent(Bool.self, forKey: .demandeTranscription) ?? false } } /// Create note de projet payload (body of `POST /api/projets/:id/notes`). struct CreateNoteProjetRequest: Codable, Equatable { var titre: String var contenu: String = "" var demandeTranscription: Bool = false } extension CreateNoteProjetRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) titre = try c.decode(String.self, forKey: .titre) contenu = try c.decodeIfPresent(String.self, forKey: .contenu) ?? "" demandeTranscription = try c.decodeIfPresent(Bool.self, forKey: .demandeTranscription) ?? false } } struct ContactValeurDto: Codable, Equatable { var valeur: String var categorie: String = "pro" } extension ContactValeurDto { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) valeur = try c.decode(String.self, forKey: .valeur) categorie = try c.decodeIfPresent(String.self, forKey: .categorie) ?? "pro" } } /// Create/update contact payload (mirror of server `ContactInput`). struct CreateContactRequest: Codable, Equatable { var prenom: String = "" var nom: String = "" var entrepriseId: String? = nil var entrepriseNom: String? = nil var fonction: String = "" var emails: [ContactValeurDto] = [] var telephones: [ContactValeurDto] = [] var notes: String = "" var statut: String = "prospect" var etape: String = "nouveau" var tags: [String] = [] enum CodingKeys: String, CodingKey { case prenom, nom, entrepriseId, entrepriseNom, fonction, emails, telephones, notes, statut, etape, tags } } extension CreateContactRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) prenom = try c.decodeIfPresent(String.self, forKey: .prenom) ?? "" nom = try c.decodeIfPresent(String.self, forKey: .nom) ?? "" entrepriseId = try c.decodeIfPresent(String.self, forKey: .entrepriseId) entrepriseNom = try c.decodeIfPresent(String.self, forKey: .entrepriseNom) fonction = try c.decodeIfPresent(String.self, forKey: .fonction) ?? "" emails = try c.decodeIfPresent([ContactValeurDto].self, forKey: .emails) ?? [] telephones = try c.decodeIfPresent([ContactValeurDto].self, forKey: .telephones) ?? [] notes = try c.decodeIfPresent(String.self, forKey: .notes) ?? "" statut = try c.decodeIfPresent(String.self, forKey: .statut) ?? "prospect" etape = try c.decodeIfPresent(String.self, forKey: .etape) ?? "nouveau" tags = try c.decodeIfPresent([String].self, forKey: .tags) ?? [] } func encode(to encoder: Encoder) throws { var c = encoder.container(keyedBy: CodingKeys.self) try c.encode(prenom, forKey: .prenom) try c.encode(nom, forKey: .nom) try encodeExplicitNull(entrepriseId, in: &c, forKey: .entrepriseId) try encodeExplicitNull(entrepriseNom, in: &c, forKey: .entrepriseNom) try c.encode(fonction, forKey: .fonction) try c.encode(emails, forKey: .emails) try c.encode(telephones, forKey: .telephones) try c.encode(notes, forKey: .notes) try c.encode(statut, forKey: .statut) try c.encode(etape, forKey: .etape) try c.encode(tags, forKey: .tags) } } /// Create projet payload (mirror server `ProjetInput`, body of `/api/projets`). struct CreateProjetRequest: Codable, Equatable { var nom: String var description: String = "" var workflowId: String enum CodingKeys: String, CodingKey { case nom, description, workflowId } } extension CreateProjetRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) nom = try c.decode(String.self, forKey: .nom) description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" workflowId = try c.decode(String.self, forKey: .workflowId) } } /// Create/update RDV payload (mirror server, body of `/api/rdv`). struct RdvUpsertRequest: Codable, Equatable { var titre: String = "" var description: String = "" var lieu: String = "" var debut: String var fin: String var contactIds: [String] = [] var projetId: String? = nil enum CodingKeys: String, CodingKey { case titre, description, lieu, debut, fin, contactIds, projetId } } extension RdvUpsertRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) titre = try c.decodeIfPresent(String.self, forKey: .titre) ?? "" description = try c.decodeIfPresent(String.self, forKey: .description) ?? "" lieu = try c.decodeIfPresent(String.self, forKey: .lieu) ?? "" debut = try c.decode(String.self, forKey: .debut) fin = try c.decode(String.self, forKey: .fin) contactIds = try c.decodeIfPresent([String].self, forKey: .contactIds) ?? [] projetId = try c.decodeIfPresent(String.self, forKey: .projetId) } func encode(to encoder: Encoder) throws { var c = encoder.container(keyedBy: CodingKeys.self) try c.encode(titre, forKey: .titre) try c.encode(description, forKey: .description) try c.encode(lieu, forKey: .lieu) try c.encode(debut, forKey: .debut) try c.encode(fin, forKey: .fin) try c.encode(contactIds, forKey: .contactIds) try encodeExplicitNull(projetId, in: &c, forKey: .projetId) } } /// Create/update reservation payload (mirror server, body of `/api/ressources/:genre/:id/reservations`). struct ReservationUpsertRequest: Codable, Equatable { var debut: String var fin: String var motif: String = "" var projetId: String? = nil var rdvId: String? = nil enum CodingKeys: String, CodingKey { case debut, fin, motif, projetId, rdvId } } extension ReservationUpsertRequest { init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: CodingKeys.self) debut = try c.decode(String.self, forKey: .debut) fin = try c.decode(String.self, forKey: .fin) motif = try c.decodeIfPresent(String.self, forKey: .motif) ?? "" projetId = try c.decodeIfPresent(String.self, forKey: .projetId) rdvId = try c.decodeIfPresent(String.self, forKey: .rdvId) } func encode(to encoder: Encoder) throws { var c = encoder.container(keyedBy: CodingKeys.self) try c.encode(debut, forKey: .debut) try c.encode(fin, forKey: .fin) try c.encode(motif, forKey: .motif) try encodeExplicitNull(projetId, in: &c, forKey: .projetId) try encodeExplicitNull(rdvId, in: &c, forKey: .rdvId) } } /// Encodes `nil` as an explicit JSON null (Kotlin `explicitNulls` default). private func encodeExplicitNull<K: CodingKey, T: Encodable>( _ value: T?, in container: inout KeyedEncodingContainer<K>, forKey key: K ) throws { if let value { try container.encode(value, forKey: key) } else { try container.encodeNil(forKey: key) } }
GitRust