From c4c6dd6c24c51da80cdfa5105b25370e4bb4424b Mon Sep 17 00:00:00 2001 From: enlineawork Date: Mon, 7 Sep 2026 08:48:36 -0300 Subject: [PATCH] =?UTF-8?q?F2.4=20Android:=20agregar=20evidencias=20fotogr?= =?UTF-8?q?=C3=A1ficas=20al=20Hallazgo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dhinspeccion/data/FieldFindingsMobile.kt | 96 ++++++++++++++++++- 1 file changed, 94 insertions(+), 2 deletions(-) diff --git a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt index 66099da..8523801 100644 --- a/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt +++ b/android-app/app/src/main/java/com/korexlabs/dhinspeccion/data/FieldFindingsMobile.kt @@ -6,15 +6,24 @@ import com.squareup.moshi.Moshi import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.MultipartBody import okhttp3.OkHttpClient +import okhttp3.RequestBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.HttpException import retrofit2.Retrofit import retrofit2.converter.moshi.MoshiConverterFactory import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.Header +import retrofit2.http.Multipart import retrofit2.http.POST +import retrofit2.http.Part import retrofit2.http.Path +import java.io.File +import java.time.Instant data class FieldFindingAct( val id: String, @@ -64,6 +73,30 @@ data class FieldFindingItem( val correctionDueOn: String? = null, ) +data class FieldFindingEvidence( + val id: String, + val findingId: String, + val kind: String, + val purpose: String, + val originalName: String, + val mimeType: String, + val sizeBytes: Long, + val sha256: String, + val title: String? = null, + val description: String? = null, + val capturedAt: String? = null, + val latitude: Double? = null, + val longitude: Double? = null, + val accuracyM: Double? = null, + val deviceLabel: String? = null, + val source: String, + val createdAt: String, +) + +data class FieldFindingEvidenceListResponse( + val data: List = emptyList(), +) + data class FieldFindingOptionsResponse( val act: FieldFindingAct, val capture: CaptureStatus = CaptureStatus(), @@ -104,13 +137,36 @@ private interface FieldFindingsApi { @Body request: CreateFieldFindingRequest, ): FieldFindingCreateResponse + @GET("inspection-findings/{findingId}/evidence") + suspend fun evidence( + @Header("Authorization") authorization: String, + @Path("findingId") findingId: String, + ): FieldFindingEvidenceListResponse + + @Multipart + @POST("inspection-findings/{findingId}/evidence") + suspend fun uploadEvidence( + @Header("Authorization") authorization: String, + @Path("findingId") findingId: String, + @Part file: MultipartBody.Part, + @Part("kind") kind: RequestBody, + @Part("purpose") purpose: RequestBody, + @Part("title") title: RequestBody?, + @Part("description") description: RequestBody?, + @Part("capturedAt") capturedAt: RequestBody, + @Part("latitude") latitude: RequestBody, + @Part("longitude") longitude: RequestBody, + @Part("accuracyM") accuracyM: RequestBody?, + @Part("deviceLabel") deviceLabel: RequestBody, + ): FieldFindingEvidence + @POST("auth/mobile/refresh") suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse } /** - * Cliente separado para F2.3. Comparte el almacén cifrado de sesión de la APK, - * pero mantiene el contrato de Hallazgos desacoplado del cliente F2.2. + * Cliente de campo para Hallazgos y sus evidencias append-only. + * Comparte el almacén cifrado de sesión y nunca persiste la contraseña. */ class FieldFindingsRepository(context: Context) { private val store = SecureSessionStore(context.applicationContext) @@ -136,6 +192,42 @@ class FieldFindingsRepository(context: Context) { api.create("Bearer ${session.accessToken}", visitId, assetId, request) } + suspend fun evidence(findingId: String): FieldFindingEvidenceListResponse = authorized { session -> + api.evidence("Bearer ${session.accessToken}", findingId) + } + + suspend fun uploadObservationPhoto( + findingId: String, + file: File, + latitude: Double, + longitude: Double, + accuracyM: Double?, + title: String? = null, + description: String? = null, + capturedAt: String = Instant.now().toString(), + ): FieldFindingEvidence = authorized { session -> + val text = "text/plain".toMediaType() + val part = MultipartBody.Part.createFormData( + "file", + file.name, + file.asRequestBody("image/jpeg".toMediaType()), + ) + api.uploadEvidence( + authorization = "Bearer ${session.accessToken}", + findingId = findingId, + file = part, + kind = "PHOTO".toRequestBody(text), + purpose = "OBSERVATION".toRequestBody(text), + title = title?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text), + description = description?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text), + capturedAt = capturedAt.toRequestBody(text), + latitude = latitude.toString().toRequestBody(text), + longitude = longitude.toString().toRequestBody(text), + accuracyM = accuracyM?.toString()?.toRequestBody(text), + deviceLabel = "DH Android".toRequestBody(text), + ) + } + private suspend fun authorized(block: suspend (StoredSession) -> T): T { var session = store.load() ?: throw IllegalStateException("Sesión no iniciada") try {