F3.2 Android: agregar cliente de Actas y cierre
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
package com.korexlabs.dhinspeccion.data
|
||||
|
||||
import android.content.Context
|
||||
import com.korexlabs.dhinspeccion.BuildConfig
|
||||
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.PATCH
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.PUT
|
||||
import retrofit2.http.Part
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
import java.io.File
|
||||
import java.time.Instant
|
||||
|
||||
data class MobileActSummary(
|
||||
val id: String,
|
||||
val visitId: String,
|
||||
val code: String,
|
||||
val status: String,
|
||||
val occurredAt: String,
|
||||
val title: String,
|
||||
val summary: String,
|
||||
val observations: String? = null,
|
||||
val currentVersion: Int = 0,
|
||||
val closedAt: String? = null,
|
||||
val closureSha256: String? = null,
|
||||
val assetCount: Int = 0,
|
||||
val findingCount: Int = 0,
|
||||
)
|
||||
|
||||
data class MobileActDetail(
|
||||
val id: String,
|
||||
val visitId: String,
|
||||
val code: String,
|
||||
val status: String,
|
||||
val occurredAt: String,
|
||||
val title: String,
|
||||
val summary: String,
|
||||
val observations: String? = null,
|
||||
val currentVersion: Int = 0,
|
||||
val closedAt: String? = null,
|
||||
val closureSha256: String? = null,
|
||||
val assetCount: Int = 0,
|
||||
val findingCount: Int = 0,
|
||||
val assets: List<AssetSummary> = emptyList(),
|
||||
)
|
||||
|
||||
data class MobileActListMeta(
|
||||
val page: Int = 1,
|
||||
val pageSize: Int = 100,
|
||||
val total: Int = 0,
|
||||
val totalPages: Int = 0,
|
||||
)
|
||||
|
||||
data class MobileActListResponse(
|
||||
val data: List<MobileActSummary> = emptyList(),
|
||||
val meta: MobileActListMeta = MobileActListMeta(),
|
||||
)
|
||||
|
||||
data class CreateMobileActRequest(
|
||||
val occurredAt: String,
|
||||
val title: String,
|
||||
val summary: String,
|
||||
val observations: String? = null,
|
||||
val assetIds: List<String>,
|
||||
)
|
||||
|
||||
data class UpdateMobileActRequest(
|
||||
val assetIds: List<String>,
|
||||
)
|
||||
|
||||
data class MobileResponsibleRequest(
|
||||
val attendanceStatus: String,
|
||||
val fullName: String? = null,
|
||||
val documentType: String? = null,
|
||||
val documentNumber: String? = null,
|
||||
val position: String? = null,
|
||||
val email: String? = null,
|
||||
val phone: String? = null,
|
||||
val absenceReason: String? = null,
|
||||
)
|
||||
|
||||
data class MobileResponsible(
|
||||
val actId: String,
|
||||
val attendanceStatus: String,
|
||||
val fullName: String? = null,
|
||||
val documentType: String? = null,
|
||||
val documentNumber: String? = null,
|
||||
val position: String? = null,
|
||||
val email: String? = null,
|
||||
val phone: String? = null,
|
||||
val absenceReason: String? = null,
|
||||
)
|
||||
|
||||
data class MobileActClosureHeader(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val status: String,
|
||||
val visitId: String,
|
||||
val currentVersion: Int = 0,
|
||||
val closedAt: String? = null,
|
||||
val closureSha256: String? = null,
|
||||
)
|
||||
|
||||
data class MobileVisitClosureHeader(
|
||||
val id: String,
|
||||
val code: String,
|
||||
val status: String,
|
||||
val actualClosedAt: String? = null,
|
||||
)
|
||||
|
||||
data class MobileSignature(
|
||||
val id: String,
|
||||
val signerType: String,
|
||||
val signerUserId: String? = null,
|
||||
val signerName: String,
|
||||
val status: String,
|
||||
val reason: String? = null,
|
||||
val companyManifestation: String? = null,
|
||||
val companyStatement: String? = null,
|
||||
val signedAt: String? = null,
|
||||
val imageSha256: String? = null,
|
||||
)
|
||||
|
||||
data class MobileClosureRecord(
|
||||
val schemaVersion: String,
|
||||
val preparedSha256: String,
|
||||
val preparedAt: String,
|
||||
val finalSha256: String? = null,
|
||||
val deviceClosedAt: String? = null,
|
||||
val serverClosedAt: String? = null,
|
||||
val uploadMode: String? = null,
|
||||
val isCurrent: Boolean = true,
|
||||
)
|
||||
|
||||
data class MobileClosureConsents(
|
||||
val version: String = "",
|
||||
val inspector: String = "",
|
||||
val company: String = "",
|
||||
)
|
||||
|
||||
data class MobileActClosure(
|
||||
val act: MobileActClosureHeader,
|
||||
val visit: MobileVisitClosureHeader,
|
||||
val responsible: MobileResponsible? = null,
|
||||
val closure: MobileClosureRecord? = null,
|
||||
val signatures: List<MobileSignature> = emptyList(),
|
||||
val consents: MobileClosureConsents = MobileClosureConsents(),
|
||||
)
|
||||
|
||||
data class MobileCompanyOutcomeRequest(
|
||||
val status: String,
|
||||
val reason: String,
|
||||
)
|
||||
|
||||
data class MobileCloseActRequest(
|
||||
val clientClosedAt: String = Instant.now().toString(),
|
||||
val uploadMode: String = "ONLINE",
|
||||
)
|
||||
|
||||
data class MobileCloseVisitRequest(
|
||||
val clientClosedAt: String = Instant.now().toString(),
|
||||
)
|
||||
|
||||
private interface MobileActsApi {
|
||||
@GET("inspection-visits/{visitId}/acts")
|
||||
suspend fun listActs(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("visitId") visitId: String,
|
||||
@Query("pageSize") pageSize: Int = 100,
|
||||
): MobileActListResponse
|
||||
|
||||
@GET("inspection-acts/{actId}")
|
||||
suspend fun act(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
): MobileActDetail
|
||||
|
||||
@POST("inspection-visits/{visitId}/acts")
|
||||
suspend fun createAct(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("visitId") visitId: String,
|
||||
@Body request: CreateMobileActRequest,
|
||||
): MobileActDetail
|
||||
|
||||
@PATCH("inspection-acts/{actId}")
|
||||
suspend fun updateAct(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Body request: UpdateMobileActRequest,
|
||||
): MobileActDetail
|
||||
|
||||
@GET("inspection-acts/{actId}/closure")
|
||||
suspend fun closure(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
): MobileActClosure
|
||||
|
||||
@PUT("inspection-acts/{actId}/responsible")
|
||||
suspend fun responsible(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Body request: MobileResponsibleRequest,
|
||||
): MobileActClosure
|
||||
|
||||
@POST("inspection-acts/{actId}/ready")
|
||||
suspend fun ready(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
): MobileActClosure
|
||||
|
||||
@POST("inspection-acts/{actId}/reopen")
|
||||
suspend fun reopen(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
): MobileActClosure
|
||||
|
||||
@Multipart
|
||||
@POST("inspection-acts/{actId}/signatures/inspector")
|
||||
suspend fun signInspector(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Part file: MultipartBody.Part,
|
||||
@Part("consentAccepted") consentAccepted: RequestBody,
|
||||
@Part("clientSignedAt") clientSignedAt: RequestBody,
|
||||
@Part("latitude") latitude: RequestBody?,
|
||||
@Part("longitude") longitude: RequestBody?,
|
||||
@Part("accuracyM") accuracyM: RequestBody?,
|
||||
@Part("deviceLabel") deviceLabel: RequestBody,
|
||||
): MobileActClosure
|
||||
|
||||
@Multipart
|
||||
@POST("inspection-acts/{actId}/signatures/company")
|
||||
suspend fun signCompany(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Part file: MultipartBody.Part,
|
||||
@Part("consentAccepted") consentAccepted: RequestBody,
|
||||
@Part("clientSignedAt") clientSignedAt: RequestBody,
|
||||
@Part("latitude") latitude: RequestBody?,
|
||||
@Part("longitude") longitude: RequestBody?,
|
||||
@Part("accuracyM") accuracyM: RequestBody?,
|
||||
@Part("deviceLabel") deviceLabel: RequestBody,
|
||||
@Part("manifestation") manifestation: RequestBody?,
|
||||
@Part("statement") statement: RequestBody?,
|
||||
): MobileActClosure
|
||||
|
||||
@POST("inspection-acts/{actId}/company-outcome")
|
||||
suspend fun companyOutcome(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Body request: MobileCompanyOutcomeRequest,
|
||||
): MobileActClosure
|
||||
|
||||
@POST("inspection-acts/{actId}/close")
|
||||
suspend fun closeAct(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("actId") actId: String,
|
||||
@Body request: MobileCloseActRequest,
|
||||
): MobileActClosure
|
||||
|
||||
@POST("inspection-visits/{visitId}/close")
|
||||
suspend fun closeVisit(
|
||||
@Header("Authorization") authorization: String,
|
||||
@Path("visitId") visitId: String,
|
||||
@Body request: MobileCloseVisitRequest,
|
||||
): VisitDetail
|
||||
|
||||
@POST("auth/mobile/refresh")
|
||||
suspend fun refresh(@Body request: RefreshRequest): MobileSessionResponse
|
||||
}
|
||||
|
||||
class MobileActsRepository(context: Context) {
|
||||
private val store = SecureSessionStore(context.applicationContext)
|
||||
private val refreshMutex = Mutex()
|
||||
private val moshi = Moshi.Builder().addLast(KotlinJsonAdapterFactory()).build()
|
||||
private val api: MobileActsApi = Retrofit.Builder()
|
||||
.baseUrl(BuildConfig.API_BASE_URL)
|
||||
.client(OkHttpClient.Builder().build())
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.build()
|
||||
.create(MobileActsApi::class.java)
|
||||
|
||||
suspend fun list(visitId: String): MobileActListResponse = authorized { session ->
|
||||
api.listActs("Bearer ${session.accessToken}", visitId)
|
||||
}
|
||||
|
||||
suspend fun get(actId: String): MobileActDetail = authorized { session ->
|
||||
api.act("Bearer ${session.accessToken}", actId)
|
||||
}
|
||||
|
||||
suspend fun create(visitId: String, assetId: String, visitCode: String): MobileActDetail = authorized { session ->
|
||||
api.createAct(
|
||||
"Bearer ${session.accessToken}",
|
||||
visitId,
|
||||
CreateMobileActRequest(
|
||||
occurredAt = Instant.now().toString(),
|
||||
title = "Acta de inspección $visitCode",
|
||||
summary = "Acta de inspección en curso. Los Hallazgos y observaciones se incorporan de forma trazable durante la visita.",
|
||||
assetIds = listOf(assetId),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun ensureAsset(actId: String, assetId: String): MobileActDetail {
|
||||
val detail = get(actId)
|
||||
if (detail.assets.any { it.id == assetId }) return detail
|
||||
val ids = (detail.assets.map { it.id } + assetId).distinct()
|
||||
return authorized { session ->
|
||||
api.updateAct("Bearer ${session.accessToken}", actId, UpdateMobileActRequest(ids))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun closure(actId: String): MobileActClosure = authorized { session ->
|
||||
api.closure("Bearer ${session.accessToken}", actId)
|
||||
}
|
||||
|
||||
suspend fun setResponsible(actId: String, request: MobileResponsibleRequest): MobileActClosure = authorized { session ->
|
||||
api.responsible("Bearer ${session.accessToken}", actId, request)
|
||||
}
|
||||
|
||||
suspend fun prepare(actId: String): MobileActClosure = authorized { session ->
|
||||
api.ready("Bearer ${session.accessToken}", actId)
|
||||
}
|
||||
|
||||
suspend fun reopen(actId: String): MobileActClosure = authorized { session ->
|
||||
api.reopen("Bearer ${session.accessToken}", actId)
|
||||
}
|
||||
|
||||
suspend fun signInspector(
|
||||
actId: String,
|
||||
png: File,
|
||||
latitude: Double?,
|
||||
longitude: Double?,
|
||||
accuracyM: Double?,
|
||||
): MobileActClosure = signature(actId, png, latitude, longitude, accuracyM, company = false)
|
||||
|
||||
suspend fun signCompany(
|
||||
actId: String,
|
||||
png: File,
|
||||
latitude: Double?,
|
||||
longitude: Double?,
|
||||
accuracyM: Double?,
|
||||
manifestation: String = "CONFORMITY",
|
||||
statement: String? = null,
|
||||
): MobileActClosure = signature(
|
||||
actId = actId,
|
||||
png = png,
|
||||
latitude = latitude,
|
||||
longitude = longitude,
|
||||
accuracyM = accuracyM,
|
||||
company = true,
|
||||
manifestation = manifestation,
|
||||
statement = statement,
|
||||
)
|
||||
|
||||
suspend fun companyOutcome(actId: String, status: String, reason: String): MobileActClosure = authorized { session ->
|
||||
api.companyOutcome(
|
||||
"Bearer ${session.accessToken}",
|
||||
actId,
|
||||
MobileCompanyOutcomeRequest(status, reason.trim()),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun closeAct(actId: String): MobileActClosure = authorized { session ->
|
||||
api.closeAct("Bearer ${session.accessToken}", actId, MobileCloseActRequest())
|
||||
}
|
||||
|
||||
suspend fun closeVisit(visitId: String): VisitDetail = authorized { session ->
|
||||
api.closeVisit("Bearer ${session.accessToken}", visitId, MobileCloseVisitRequest())
|
||||
}
|
||||
|
||||
private suspend fun signature(
|
||||
actId: String,
|
||||
png: File,
|
||||
latitude: Double?,
|
||||
longitude: Double?,
|
||||
accuracyM: Double?,
|
||||
company: Boolean,
|
||||
manifestation: String? = null,
|
||||
statement: String? = null,
|
||||
): MobileActClosure = authorized { session ->
|
||||
val text = "text/plain".toMediaType()
|
||||
val file = MultipartBody.Part.createFormData(
|
||||
"file",
|
||||
png.name,
|
||||
png.asRequestBody("image/png".toMediaType()),
|
||||
)
|
||||
val consent = "true".toRequestBody(text)
|
||||
val signedAt = Instant.now().toString().toRequestBody(text)
|
||||
val device = "DH Android".toRequestBody(text)
|
||||
val lat = latitude?.toString()?.toRequestBody(text)
|
||||
val lon = longitude?.toString()?.toRequestBody(text)
|
||||
val accuracy = accuracyM?.toString()?.toRequestBody(text)
|
||||
if (company) {
|
||||
api.signCompany(
|
||||
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
|
||||
lat, lon, accuracy, device,
|
||||
manifestation?.toRequestBody(text),
|
||||
statement?.trim()?.takeIf { it.isNotBlank() }?.toRequestBody(text),
|
||||
)
|
||||
} else {
|
||||
api.signInspector(
|
||||
"Bearer ${session.accessToken}", actId, file, consent, signedAt,
|
||||
lat, lon, accuracy, device,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun <T> authorized(block: suspend (StoredSession) -> T): T {
|
||||
var session = store.load() ?: throw IllegalStateException("Sesión no iniciada")
|
||||
try {
|
||||
return block(session)
|
||||
} catch (error: HttpException) {
|
||||
if (error.code() != 401) throw error
|
||||
}
|
||||
session = refresh(session.refreshToken)
|
||||
return block(session)
|
||||
}
|
||||
|
||||
private suspend fun refresh(previousRefreshToken: String): StoredSession = refreshMutex.withLock {
|
||||
val latest = store.load() ?: throw IllegalStateException("Sesión no iniciada")
|
||||
if (latest.refreshToken != previousRefreshToken) return@withLock latest
|
||||
try {
|
||||
store.save(api.refresh(RefreshRequest(previousRefreshToken)))
|
||||
} catch (error: Throwable) {
|
||||
store.clear()
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user