Rebrand from Casera/MyCrib to honeyDue
Total rebrand across KMM project: - Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations) - Gradle: rootProject.name, namespace, applicationId - Android: manifest, strings.xml (all languages), widget resources - iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig - iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc. - Swift source: all class/struct/enum renames - Deep links: casera:// -> honeydue://, .casera -> .honeydue - App icons replaced with honeyDue honeycomb icon - Domains: casera.treytartt.com -> honeyDue.treytartt.com - Bundle IDs: com.tt.casera -> com.tt.honeyDue - Database table names preserved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1463
composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt
Normal file
1463
composeApp/src/commonMain/kotlin/com/tt/honeyDue/network/APILayer.kt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
expect fun getLocalhostAddress(): String
|
||||
expect fun createHttpClient(): HttpClient
|
||||
|
||||
/**
|
||||
* Get the device's preferred language code (e.g., "en", "es", "fr").
|
||||
* This is used to set the Accept-Language header for API requests
|
||||
* so the server can return localized error messages and content.
|
||||
*/
|
||||
expect fun getDeviceLanguage(): String
|
||||
|
||||
/**
|
||||
* Get the device's timezone identifier (e.g., "America/Los_Angeles", "Europe/London").
|
||||
* This is used to set the X-Timezone header for API requests
|
||||
* so the server can calculate overdue tasks correctly based on the user's local time.
|
||||
*/
|
||||
expect fun getDeviceTimezone(): String
|
||||
|
||||
object ApiClient {
|
||||
val httpClient = createHttpClient()
|
||||
|
||||
/**
|
||||
* Get the current base URL based on environment configuration.
|
||||
* To change environment, update ApiConfig.CURRENT_ENV
|
||||
*/
|
||||
fun getBaseUrl(): String = ApiConfig.getBaseUrl()
|
||||
|
||||
/**
|
||||
* Get the media base URL (without /api suffix) for serving media files
|
||||
*/
|
||||
fun getMediaBaseUrl(): String = ApiConfig.getMediaBaseUrl()
|
||||
|
||||
/**
|
||||
* Print current environment configuration
|
||||
*/
|
||||
init {
|
||||
println("🌐 API Client initialized")
|
||||
println("📍 Environment: ${ApiConfig.getEnvironmentName()}")
|
||||
println("🔗 Base URL: ${getBaseUrl()}")
|
||||
println("📁 Media URL: ${getMediaBaseUrl()}")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
/**
|
||||
* API Environment Configuration
|
||||
*
|
||||
* To switch between localhost and dev server, simply change the CURRENT_ENV value:
|
||||
* - Environment.LOCAL for local development
|
||||
* - Environment.DEV for remote dev server
|
||||
*/
|
||||
object ApiConfig {
|
||||
// ⚠️ CHANGE THIS TO TOGGLE ENVIRONMENT ⚠️
|
||||
val CURRENT_ENV = Environment.LOCAL
|
||||
|
||||
enum class Environment {
|
||||
LOCAL,
|
||||
DEV
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL based on current environment and platform
|
||||
*/
|
||||
fun getBaseUrl(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "http://${getLocalhostAddress()}:8000/api"
|
||||
Environment.DEV -> "https://honeyDue.treytartt.com/api"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media base URL (without /api suffix) for serving media files
|
||||
*/
|
||||
fun getMediaBaseUrl(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "http://${getLocalhostAddress()}:8000"
|
||||
Environment.DEV -> "https://honeyDue.treytartt.com"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get environment name for logging
|
||||
*/
|
||||
fun getEnvironmentName(): String {
|
||||
return when (CURRENT_ENV) {
|
||||
Environment.LOCAL -> "Local (${getLocalhostAddress()}:8000)"
|
||||
Environment.DEV -> "Dev Server (honeyDue.treytartt.com)"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Google OAuth Web Client ID
|
||||
* This is the Web application client ID from Google Cloud Console.
|
||||
* It should match the GOOGLE_CLIENT_ID configured in the backend.
|
||||
*
|
||||
* Set via environment: actual client ID must be configured per environment.
|
||||
* To get this value:
|
||||
* 1. Go to Google Cloud Console -> APIs & Services -> Credentials
|
||||
* 2. Create or use an existing OAuth 2.0 Client ID of type "Web application"
|
||||
* 3. Copy the Client ID (format: xxx.apps.googleusercontent.com)
|
||||
* 4. Replace the empty string below with your client ID
|
||||
*
|
||||
* WARNING: An empty string means Google Sign-In is not configured.
|
||||
* The app should check [isGoogleSignInConfigured] before offering Google Sign-In.
|
||||
*/
|
||||
const val GOOGLE_WEB_CLIENT_ID = ""
|
||||
|
||||
/**
|
||||
* Whether Google Sign-In has been configured with a real client ID.
|
||||
* UI should check this before showing Google Sign-In buttons.
|
||||
*/
|
||||
val isGoogleSignInConfigured: Boolean
|
||||
get() = GOOGLE_WEB_CLIENT_ID.isNotEmpty()
|
||||
&& !GOOGLE_WEB_CLIENT_ID.contains("YOUR_")
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
sealed class ApiResult<out T> {
|
||||
data class Success<T>(val data: T) : ApiResult<T>()
|
||||
data class Error(val message: String, val code: Int? = null) : ApiResult<Nothing>()
|
||||
object Loading : ApiResult<Nothing>()
|
||||
object Idle : ApiResult<Nothing>()
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class AuthApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun register(request: RegisterRequest): ApiResult<AuthResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/register/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
// Parse actual error message from backend
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun login(request: LoginRequest): ApiResult<AuthResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/login/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
// Parse actual error message from backend
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun logout(token: String): ApiResult<Unit> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/logout/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Logout failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCurrentUser(token: String): ApiResult<User> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/auth/me/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get user", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun verifyEmail(token: String, request: VerifyEmailRequest): ApiResult<VerifyEmailResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/verify-email/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Verification failed")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Verification failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateProfile(token: String, request: UpdateProfileRequest): ApiResult<User> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/auth/profile/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Profile update failed")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Profile update failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// Password Reset Methods
|
||||
suspend fun forgotPassword(request: ForgotPasswordRequest): ApiResult<ForgotPasswordResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/forgot-password/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Failed to send reset code")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Failed to send reset code", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun verifyResetCode(request: VerifyResetCodeRequest): ApiResult<VerifyResetCodeResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/verify-reset-code/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Invalid code")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Invalid code", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun resetPassword(request: ResetPasswordRequest): ApiResult<ResetPasswordResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/reset-password/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
// Try to parse Django validation errors (Map<String, List<String>>)
|
||||
val errorMessage = try {
|
||||
val validationErrors = response.body<Map<String, List<String>>>()
|
||||
// Flatten all error messages into a single string
|
||||
validationErrors.flatMap { (field, errors) ->
|
||||
errors.map { error ->
|
||||
if (field == "non_field_errors") error else "$field: $error"
|
||||
}
|
||||
}.joinToString(". ")
|
||||
} catch (e: Exception) {
|
||||
// Try simple error format {error: "message"}
|
||||
try {
|
||||
val simpleError = response.body<Map<String, String>>()
|
||||
simpleError["error"] ?: "Failed to reset password"
|
||||
} catch (e2: Exception) {
|
||||
"Failed to reset password"
|
||||
}
|
||||
}
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// Apple Sign In
|
||||
suspend fun appleSignIn(request: AppleSignInRequest): ApiResult<AppleSignInResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/apple-sign-in/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Apple Sign In failed")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Apple Sign In failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// Google Sign In
|
||||
suspend fun googleSignIn(request: GoogleSignInRequest): ApiResult<GoogleSignInResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/auth/google-sign-in/") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Google Sign In failed")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Google Sign In failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class ContractorApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getContractors(
|
||||
token: String,
|
||||
specialty: String? = null,
|
||||
isFavorite: Boolean? = null,
|
||||
isActive: Boolean? = null,
|
||||
search: String? = null
|
||||
): ApiResult<List<ContractorSummary>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/") {
|
||||
header("Authorization", "Token $token")
|
||||
specialty?.let { parameter("specialty", it) }
|
||||
isFavorite?.let { parameter("is_favorite", it) }
|
||||
isActive?.let { parameter("is_active", it) }
|
||||
search?.let { parameter("search", it) }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractors", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContractor(token: String, id: Int): ApiResult<Contractor> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createContractor(token: String, request: ContractorCreateRequest): ApiResult<Contractor> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/contractors/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to create contractor"
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateContractor(token: String, id: Int, request: ContractorUpdateRequest): ApiResult<Contractor> {
|
||||
return try {
|
||||
val response = client.patch("$baseUrl/contractors/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to update contractor"
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteContractor(token: String, id: Int): ApiResult<Unit> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/contractors/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete contractor", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun toggleFavorite(token: String, id: Int): ApiResult<Contractor> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/contractors/$id/toggle-favorite/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to toggle favorite", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContractorTasks(token: String, id: Int): ApiResult<List<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/$id/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor tasks", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContractorsByResidence(token: String, residenceId: Int): ApiResult<List<ContractorSummary>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/by-residence/$residenceId/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractors for residence", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.utils.io.core.*
|
||||
|
||||
class DocumentApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getDocuments(
|
||||
token: String,
|
||||
residenceId: Int? = null,
|
||||
documentType: String? = null,
|
||||
// Deprecated filter args kept for source compatibility with iOS wrappers.
|
||||
// Backend/OpenAPI currently support: residence, document_type, is_active, expiring_soon, search.
|
||||
category: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean? = null,
|
||||
expiringSoon: Int? = null,
|
||||
tags: String? = null,
|
||||
search: String? = null
|
||||
): ApiResult<List<Document>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/documents/") {
|
||||
header("Authorization", "Token $token")
|
||||
residenceId?.let { parameter("residence", it) }
|
||||
documentType?.let { parameter("document_type", it) }
|
||||
isActive?.let { parameter("is_active", it) }
|
||||
expiringSoon?.let { parameter("expiring_soon", it) }
|
||||
search?.let { parameter("search", it) }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch documents", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getDocument(token: String, id: Int): ApiResult<Document> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/documents/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch document", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createDocument(
|
||||
token: String,
|
||||
title: String,
|
||||
documentType: String,
|
||||
residenceId: Int,
|
||||
description: String? = null,
|
||||
category: String? = null,
|
||||
tags: String? = null,
|
||||
notes: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean = true,
|
||||
// Warranty-specific fields
|
||||
itemName: String? = null,
|
||||
modelNumber: String? = null,
|
||||
serialNumber: String? = null,
|
||||
provider: String? = null,
|
||||
providerContact: String? = null,
|
||||
claimPhone: String? = null,
|
||||
claimEmail: String? = null,
|
||||
claimWebsite: String? = null,
|
||||
purchaseDate: String? = null,
|
||||
startDate: String? = null,
|
||||
endDate: String? = null,
|
||||
// File (optional for warranties) - kept for backwards compatibility
|
||||
fileBytes: ByteArray? = null,
|
||||
fileName: String? = null,
|
||||
mimeType: String? = null,
|
||||
// Multiple files support
|
||||
fileBytesList: List<ByteArray>? = null,
|
||||
fileNamesList: List<String>? = null,
|
||||
mimeTypesList: List<String>? = null
|
||||
): ApiResult<Document> {
|
||||
return try {
|
||||
val response = if ((fileBytesList != null && fileBytesList.isNotEmpty() && fileNamesList != null && mimeTypesList != null) ||
|
||||
(fileBytes != null && fileName != null && mimeType != null)) {
|
||||
// If files are provided, use multipart/form-data
|
||||
client.submitFormWithBinaryData(
|
||||
url = "$baseUrl/documents/",
|
||||
formData = formData {
|
||||
append("title", title)
|
||||
append("document_type", documentType)
|
||||
append("residence_id", residenceId.toString())
|
||||
description?.let { append("description", it) }
|
||||
// Backend-supported fields
|
||||
modelNumber?.let { append("model_number", it) }
|
||||
serialNumber?.let { append("serial_number", it) }
|
||||
// Map provider to vendor for backend compatibility
|
||||
provider?.let { append("vendor", it) }
|
||||
purchaseDate?.let { append("purchase_date", it) }
|
||||
// Map endDate to expiry_date for backend compatibility
|
||||
endDate?.let { append("expiry_date", it) }
|
||||
|
||||
// Backend accepts "file" field for single file upload
|
||||
if (fileBytesList != null && fileBytesList.isNotEmpty() && fileNamesList != null && mimeTypesList != null) {
|
||||
// Send first file as "file" (backend only accepts single file)
|
||||
append("file", fileBytesList[0], Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeTypesList.getOrElse(0) { "application/octet-stream" })
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"${fileNamesList.getOrElse(0) { "file_0" }}\"")
|
||||
})
|
||||
} else if (fileBytes != null && fileName != null && mimeType != null) {
|
||||
append("file", fileBytes, Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
|
||||
})
|
||||
}
|
||||
}
|
||||
) {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
} else {
|
||||
// If no file, use JSON
|
||||
val request = DocumentCreateRequest(
|
||||
title = title,
|
||||
documentType = documentType,
|
||||
description = description,
|
||||
modelNumber = modelNumber,
|
||||
serialNumber = serialNumber,
|
||||
vendor = provider, // Map provider to vendor
|
||||
purchaseDate = purchaseDate,
|
||||
expiryDate = endDate, // Map endDate to expiryDate
|
||||
residenceId = residenceId
|
||||
)
|
||||
client.post("$baseUrl/documents/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to create document"
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateDocument(
|
||||
token: String,
|
||||
id: Int,
|
||||
title: String? = null,
|
||||
documentType: String? = null,
|
||||
description: String? = null,
|
||||
category: String? = null,
|
||||
tags: String? = null,
|
||||
notes: String? = null,
|
||||
contractorId: Int? = null,
|
||||
isActive: Boolean? = null,
|
||||
// Warranty-specific fields
|
||||
itemName: String? = null,
|
||||
modelNumber: String? = null,
|
||||
serialNumber: String? = null,
|
||||
provider: String? = null,
|
||||
providerContact: String? = null,
|
||||
claimPhone: String? = null,
|
||||
claimEmail: String? = null,
|
||||
claimWebsite: String? = null,
|
||||
purchaseDate: String? = null,
|
||||
startDate: String? = null,
|
||||
endDate: String? = null
|
||||
): ApiResult<Document> {
|
||||
return try {
|
||||
// Backend update handler uses JSON via c.Bind (not multipart)
|
||||
val request = DocumentUpdateRequest(
|
||||
title = title,
|
||||
documentType = documentType,
|
||||
description = description,
|
||||
modelNumber = modelNumber,
|
||||
serialNumber = serialNumber,
|
||||
vendor = provider, // Map provider to vendor
|
||||
purchaseDate = purchaseDate,
|
||||
expiryDate = endDate // Map endDate to expiryDate
|
||||
)
|
||||
val response = client.patch("$baseUrl/documents/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to update document"
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteDocument(token: String, id: Int): ApiResult<Unit> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/documents/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete document", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun downloadDocument(token: String, url: String): ApiResult<ByteArray> {
|
||||
return try {
|
||||
val response = client.get(url) {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to download document", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun activateDocument(token: String, id: Int): ApiResult<Document> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/documents/$id/activate/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
// Backend returns wrapped response: {message: string, document: DocumentResponse}
|
||||
val wrapper: DocumentActionResponse = response.body()
|
||||
ApiResult.Success(wrapper.document)
|
||||
} else {
|
||||
ApiResult.Error("Failed to activate document", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deactivateDocument(token: String, id: Int): ApiResult<Document> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/documents/$id/deactivate/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
// Backend returns wrapped response: {message: string, document: DocumentResponse}
|
||||
val wrapper: DocumentActionResponse = response.body()
|
||||
ApiResult.Success(wrapper.document)
|
||||
} else {
|
||||
ApiResult.Error("Failed to deactivate document", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun uploadDocumentImage(
|
||||
token: String,
|
||||
documentId: Int,
|
||||
imageBytes: ByteArray,
|
||||
fileName: String = "image.jpg",
|
||||
mimeType: String = "image/jpeg",
|
||||
caption: String? = null
|
||||
): ApiResult<Document> {
|
||||
return try {
|
||||
val response = client.submitFormWithBinaryData(
|
||||
url = "$baseUrl/documents/$documentId/images/",
|
||||
formData = formData {
|
||||
append("image", imageBytes, Headers.build {
|
||||
append(HttpHeaders.ContentType, mimeType)
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
|
||||
})
|
||||
caption?.let { append("caption", it) }
|
||||
}
|
||||
) {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<String>()
|
||||
} catch (e: Exception) {
|
||||
"Failed to upload document image"
|
||||
}
|
||||
ApiResult.Error(errorBody, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteDocumentImage(token: String, documentId: Int, imageId: Int): ApiResult<Document> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/documents/$documentId/images/$imageId/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete document image", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.ErrorResponse
|
||||
import io.ktor.client.call.body
|
||||
import io.ktor.client.statement.HttpResponse
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
object ErrorParser {
|
||||
private val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses error response from the backend.
|
||||
* The backend returns: {"error": "message"}
|
||||
* Falls back to detail field or field-specific errors if present.
|
||||
*/
|
||||
suspend fun parseError(response: HttpResponse): String {
|
||||
return try {
|
||||
val errorResponse = response.body<ErrorResponse>()
|
||||
|
||||
// Build detailed error message
|
||||
val message = StringBuilder()
|
||||
|
||||
// Primary: use the error field (main error message from backend)
|
||||
message.append(errorResponse.error)
|
||||
|
||||
// Secondary: append detail if present and different from error
|
||||
errorResponse.detail?.let { detail ->
|
||||
if (detail.isNotBlank() && detail != errorResponse.error) {
|
||||
message.append(": $detail")
|
||||
}
|
||||
}
|
||||
|
||||
// Add field-specific errors if present
|
||||
errorResponse.errors?.let { fieldErrors ->
|
||||
if (fieldErrors.isNotEmpty()) {
|
||||
message.append("\n\nDetails:")
|
||||
fieldErrors.forEach { (field, errors) ->
|
||||
message.append("\n• $field: ${errors.joinToString(", ")}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.toString()
|
||||
} catch (e: Exception) {
|
||||
// Fallback: try to parse as simple {"error": "message"} map
|
||||
try {
|
||||
val simpleError = response.body<Map<String, String>>()
|
||||
simpleError["error"] ?: simpleError["message"] ?: simpleError["detail"]
|
||||
?: "An error occurred (${response.status.value})"
|
||||
} catch (e2: Exception) {
|
||||
// Last resort: read as plain text
|
||||
try {
|
||||
response.body<String>()
|
||||
} catch (e3: Exception) {
|
||||
"An error occurred (${response.status.value})"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
|
||||
/**
|
||||
* Result type for conditional HTTP requests with ETag support.
|
||||
* Used to efficiently check if data has changed on the server.
|
||||
*/
|
||||
sealed class ConditionalResult<T> {
|
||||
/**
|
||||
* Server returned new data (HTTP 200).
|
||||
* Includes the new ETag for future conditional requests.
|
||||
*/
|
||||
data class Success<T>(val data: T, val etag: String?) : ConditionalResult<T>()
|
||||
|
||||
/**
|
||||
* Data has not changed since the provided ETag (HTTP 304).
|
||||
* Client should continue using cached data.
|
||||
*/
|
||||
class NotModified<T> : ConditionalResult<T>()
|
||||
|
||||
/**
|
||||
* Request failed with an error.
|
||||
*/
|
||||
data class Error<T>(val message: String, val statusCode: Int? = null) : ConditionalResult<T>()
|
||||
}
|
||||
|
||||
class LookupsApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getResidenceTypes(token: String): ApiResult<List<ResidenceType>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/types/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residence types", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskFrequencies(token: String): ApiResult<List<TaskFrequency>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/frequencies/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task frequencies", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskPriorities(token: String): ApiResult<List<TaskPriority>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/priorities/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task priorities", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTaskCategories(token: String): ApiResult<List<TaskCategory>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/categories/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task categories", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getContractorSpecialties(token: String): ApiResult<List<ContractorSpecialty>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/contractors/specialties/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch contractor specialties", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getStaticData(token: String? = null): ApiResult<StaticDataResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/static_data/") {
|
||||
// Token is optional - endpoint is public
|
||||
token?.let { header("Authorization", "Token $it") }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch static data", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches unified seeded data (all lookups + task templates) with ETag support.
|
||||
*
|
||||
* @param currentETag The ETag from a previous response. If provided and data hasn't changed,
|
||||
* server returns 304 Not Modified.
|
||||
* @param token Optional auth token (endpoint is public).
|
||||
* @return ConditionalResult with data and new ETag, NotModified if unchanged, or Error.
|
||||
*/
|
||||
suspend fun getSeededData(
|
||||
currentETag: String? = null,
|
||||
token: String? = null
|
||||
): ConditionalResult<SeededDataResponse> {
|
||||
return try {
|
||||
val response: HttpResponse = client.get("$baseUrl/static_data/") {
|
||||
// Token is optional - endpoint is public
|
||||
token?.let { header("Authorization", "Token $it") }
|
||||
// Send If-None-Match header for conditional request
|
||||
currentETag?.let { header("If-None-Match", it) }
|
||||
}
|
||||
|
||||
when {
|
||||
response.status == HttpStatusCode.NotModified -> {
|
||||
// Data hasn't changed since provided ETag
|
||||
ConditionalResult.NotModified()
|
||||
}
|
||||
response.status.isSuccess() -> {
|
||||
// Data has changed or first request - get new data and ETag
|
||||
val data: SeededDataResponse = response.body()
|
||||
val newETag = response.headers["ETag"]
|
||||
ConditionalResult.Success(data, newETag)
|
||||
}
|
||||
else -> {
|
||||
ConditionalResult.Error(
|
||||
"Failed to fetch seeded data",
|
||||
response.status.value
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ConditionalResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class NotificationApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
/**
|
||||
* Register a device for push notifications
|
||||
*/
|
||||
suspend fun registerDevice(
|
||||
token: String,
|
||||
request: DeviceRegistrationRequest
|
||||
): ApiResult<DeviceRegistrationResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/notifications/devices/register/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorBody = try {
|
||||
response.body<Map<String, String>>()
|
||||
} catch (e: Exception) {
|
||||
mapOf("error" to "Device registration failed")
|
||||
}
|
||||
ApiResult.Error(errorBody["error"] ?: "Device registration failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun unregisterDevice(
|
||||
token: String,
|
||||
deviceId: Int
|
||||
): ApiResult<Unit> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/notifications/devices/$deviceId/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(Unit)
|
||||
} else {
|
||||
ApiResult.Error("Device unregistration failed", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user's notification preferences
|
||||
*/
|
||||
suspend fun getNotificationPreferences(token: String): ApiResult<NotificationPreference> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/notifications/preferences/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get preferences", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification preferences
|
||||
*/
|
||||
suspend fun updateNotificationPreferences(
|
||||
token: String,
|
||||
request: UpdateNotificationPreferencesRequest
|
||||
): ApiResult<NotificationPreference> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/notifications/preferences/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update preferences", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getNotificationHistory(token: String): ApiResult<List<Notification>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/notifications/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
val listResponse: NotificationListResponse = response.body()
|
||||
ApiResult.Success(listResponse.results)
|
||||
} else {
|
||||
ApiResult.Error("Failed to get notification history", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markNotificationAsRead(
|
||||
token: String,
|
||||
notificationId: Int
|
||||
): ApiResult<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/notifications/$notificationId/read/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to mark notification as read", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun markAllNotificationsAsRead(token: String): ApiResult<MessageResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/notifications/mark-all-read/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to mark all notifications as read", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUnreadCount(token: String): ApiResult<UnreadCountResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/notifications/unread-count/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to get unread count", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getResidences(token: String): ApiResult<List<ResidenceResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residences", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getResidence(token: String, id: Int): ApiResult<ResidenceResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch residence", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createResidence(token: String, request: ResidenceCreateRequest): ApiResult<WithSummaryResponse<ResidenceResponse>> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to create residence", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateResidence(token: String, id: Int, request: ResidenceCreateRequest): ApiResult<WithSummaryResponse<ResidenceResponse>> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/residences/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update residence", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteResidence(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/residences/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete residence", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getSummary(token: String): ApiResult<TotalSummary> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/summary/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch summary", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMyResidences(token: String): ApiResult<MyResidencesResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/my-residences/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch my residences", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// Share Code Management
|
||||
suspend fun generateSharePackage(token: String, residenceId: Int): ApiResult<SharedResidence> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/$residenceId/generate-share-package/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun generateShareCode(token: String, residenceId: Int): ApiResult<GenerateShareCodeResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/$residenceId/generate-share-code/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getShareCode(token: String, residenceId: Int): ApiResult<ShareCodeResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/$residenceId/share-code/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun joinWithCode(token: String, code: String): ApiResult<JoinResidenceResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/join-with-code/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(JoinResidenceRequest(code))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// User Management
|
||||
suspend fun getResidenceUsers(token: String, residenceId: Int): ApiResult<ResidenceUsersResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/residences/$residenceId/users/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun removeUser(token: String, residenceId: Int, userId: Int): ApiResult<RemoveUserResponse> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/residences/$residenceId/users/$userId/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// PDF Report Generation
|
||||
suspend fun generateTasksReport(token: String, residenceId: Int, email: String? = null): ApiResult<GenerateReportResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/residences/$residenceId/generate-tasks-report/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
if (email != null) {
|
||||
setBody(mapOf("email" to email))
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@kotlinx.serialization.Serializable
|
||||
data class GenerateReportResponse(
|
||||
val message: String,
|
||||
val residence_name: String,
|
||||
val recipient_email: String
|
||||
)
|
||||
|
||||
// Removed: PaginatedResponse - no longer using paginated responses
|
||||
// All API endpoints now return direct lists instead of paginated responses
|
||||
@@ -0,0 +1,171 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class SubscriptionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getSubscriptionStatus(token: String): ApiResult<SubscriptionStatus> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/subscription/status/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch subscription status", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getUpgradeTriggers(token: String? = null): ApiResult<Map<String, UpgradeTriggerData>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/subscription/upgrade-triggers/") {
|
||||
// Token is optional - endpoint is public
|
||||
token?.let { header("Authorization", "Token $it") }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch upgrade triggers", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getFeatureBenefits(token: String): ApiResult<List<FeatureBenefit>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/subscription/features/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch feature benefits", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getActivePromotions(token: String): ApiResult<List<Promotion>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/subscription/promotions/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch promotions", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify/process iOS purchase with backend
|
||||
* Used for both new purchases and restore
|
||||
*/
|
||||
suspend fun verifyIOSReceipt(
|
||||
token: String,
|
||||
receiptData: String,
|
||||
transactionId: String
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/subscription/purchase/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf(
|
||||
"platform" to "ios",
|
||||
"receipt_data" to receiptData,
|
||||
"transaction_id" to transactionId
|
||||
))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to verify iOS receipt", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify/process Android purchase with backend
|
||||
* Used for both new purchases and restore
|
||||
*/
|
||||
suspend fun verifyAndroidPurchase(
|
||||
token: String,
|
||||
purchaseToken: String,
|
||||
productId: String
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/subscription/purchase/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(mapOf(
|
||||
"platform" to "android",
|
||||
"purchase_token" to purchaseToken,
|
||||
"product_id" to productId
|
||||
))
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to verify Android purchase", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore subscription from iOS/Android receipt
|
||||
*/
|
||||
suspend fun restoreSubscription(
|
||||
token: String,
|
||||
platform: String,
|
||||
receiptData: String? = null,
|
||||
purchaseToken: String? = null,
|
||||
productId: String? = null
|
||||
): ApiResult<VerificationResponse> {
|
||||
return try {
|
||||
val body = mutableMapOf<String, String>("platform" to platform)
|
||||
if (platform == "ios" && receiptData != null) {
|
||||
body["receipt_data"] = receiptData
|
||||
} else if (platform == "android" && purchaseToken != null) {
|
||||
body["purchase_token"] = purchaseToken
|
||||
productId?.let { body["product_id"] = it }
|
||||
}
|
||||
|
||||
val response = client.post("$baseUrl/subscription/restore/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(body)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to restore subscription", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class TaskApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getTasks(
|
||||
token: String,
|
||||
days: Int? = null
|
||||
): ApiResult<TaskColumnsResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
days?.let { parameter("days", it) }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTask(token: String, id: Int): ApiResult<TaskResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createTask(token: String, request: TaskCreateRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/tasks/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateTask(token: String, id: Int, request: TaskCreateRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteTask(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getTasksByResidence(
|
||||
token: String,
|
||||
residenceId: Int,
|
||||
days: Int? = null
|
||||
): ApiResult<TaskColumnsResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/by-residence/$residenceId/") {
|
||||
header("Authorization", "Token $token")
|
||||
days?.let { parameter("days", it) }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic PATCH method for partial task updates.
|
||||
* Used for status changes and archive/unarchive operations.
|
||||
* Returns TaskWithSummaryResponse to update dashboard stats in one call.
|
||||
*/
|
||||
suspend fun patchTask(token: String, id: Int, request: TaskPatchRequest): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.patch("$baseUrl/tasks/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
// Convenience methods for common task actions
|
||||
// These use dedicated POST endpoints for state changes
|
||||
|
||||
suspend fun cancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "cancel")
|
||||
}
|
||||
|
||||
suspend fun uncancelTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "uncancel")
|
||||
}
|
||||
|
||||
suspend fun markInProgress(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return patchTask(token, id, TaskPatchRequest(inProgress = true))
|
||||
}
|
||||
|
||||
suspend fun clearInProgress(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return patchTask(token, id, TaskPatchRequest(inProgress = false))
|
||||
}
|
||||
|
||||
suspend fun archiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "archive")
|
||||
}
|
||||
|
||||
suspend fun unarchiveTask(token: String, id: Int): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return postTaskAction(token, id, "unarchive")
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for POST task action endpoints (cancel, uncancel, archive, unarchive)
|
||||
*/
|
||||
private suspend fun postTaskAction(token: String, id: Int, action: String): ApiResult<WithSummaryResponse<TaskResponse>> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/tasks/$id/$action/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
}
|
||||
when (response.status) {
|
||||
HttpStatusCode.OK -> {
|
||||
val data = response.body<WithSummaryResponse<TaskResponse>>()
|
||||
ApiResult.Success(data)
|
||||
}
|
||||
HttpStatusCode.NotFound -> ApiResult.Error("Task not found", 404)
|
||||
HttpStatusCode.Forbidden -> ApiResult.Error("Access denied", 403)
|
||||
HttpStatusCode.BadRequest -> {
|
||||
val errorBody = response.body<String>()
|
||||
ApiResult.Error(errorBody, 400)
|
||||
}
|
||||
else -> ApiResult.Error("Task $action failed: ${response.status}", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all completions for a specific task
|
||||
*/
|
||||
suspend fun getTaskCompletions(token: String, taskId: Int): ApiResult<List<TaskCompletionResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/$taskId/completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
val errorMessage = ErrorParser.parseError(response)
|
||||
ApiResult.Error(errorMessage, response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.*
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.request.forms.*
|
||||
import io.ktor.http.*
|
||||
|
||||
class TaskCompletionApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
suspend fun getCompletions(token: String): ApiResult<List<TaskCompletionResponse>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch completions", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCompletion(token: String, id: Int): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/task-completions/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch completion", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createCompletion(token: String, request: TaskCompletionCreateRequest): ApiResult<WithSummaryResponse<TaskCompletionResponse>> {
|
||||
return try {
|
||||
val response = client.post("$baseUrl/task-completions/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to create completion", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun updateCompletion(token: String, id: Int, request: TaskCompletionCreateRequest): ApiResult<TaskCompletionResponse> {
|
||||
return try {
|
||||
val response = client.put("$baseUrl/task-completions/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(request)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to update completion", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deleteCompletion(token: String, id: Int): ApiResult<WithSummaryResponse<String>> {
|
||||
return try {
|
||||
val response = client.delete("$baseUrl/task-completions/$id/") {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to delete completion", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createCompletionWithImages(
|
||||
token: String,
|
||||
request: TaskCompletionCreateRequest,
|
||||
images: List<ByteArray> = emptyList(),
|
||||
imageFileNames: List<String> = emptyList()
|
||||
): ApiResult<WithSummaryResponse<TaskCompletionResponse>> {
|
||||
return try {
|
||||
val response = client.submitFormWithBinaryData(
|
||||
url = "$baseUrl/task-completions/",
|
||||
formData = formData {
|
||||
// Add text fields
|
||||
append("task_id", request.taskId.toString())
|
||||
request.completedAt?.let { append("completed_at", it) }
|
||||
request.actualCost?.let { append("actual_cost", it.toString()) }
|
||||
request.notes?.let { append("notes", it) }
|
||||
request.rating?.let { append("rating", it.toString()) }
|
||||
|
||||
// Add image files
|
||||
images.forEachIndexed { index, imageBytes ->
|
||||
val fileName = imageFileNames.getOrNull(index) ?: "image_$index.jpg"
|
||||
append(
|
||||
"images",
|
||||
imageBytes,
|
||||
Headers.build {
|
||||
append(HttpHeaders.ContentType, "image/jpeg")
|
||||
append(HttpHeaders.ContentDisposition, "filename=\"$fileName\"")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
header("Authorization", "Token $token")
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to create completion with images", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.tt.honeyDue.network
|
||||
|
||||
import com.tt.honeyDue.models.TaskTemplate
|
||||
import com.tt.honeyDue.models.TaskTemplatesGroupedResponse
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
|
||||
/**
|
||||
* API client for task templates.
|
||||
* Task templates are public (no auth required) and used for autocomplete when adding tasks.
|
||||
*/
|
||||
class TaskTemplateApi(private val client: HttpClient = ApiClient.httpClient) {
|
||||
private val baseUrl = ApiClient.getBaseUrl()
|
||||
|
||||
/**
|
||||
* Get all task templates as a flat list
|
||||
*/
|
||||
suspend fun getTemplates(): ApiResult<List<TaskTemplate>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch task templates", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all task templates grouped by category
|
||||
*/
|
||||
suspend fun getTemplatesGrouped(): ApiResult<TaskTemplatesGroupedResponse> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/grouped/")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch grouped task templates", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search task templates by query string
|
||||
*/
|
||||
suspend fun searchTemplates(query: String): ApiResult<List<TaskTemplate>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/search/") {
|
||||
parameter("q", query)
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to search task templates", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates by category ID
|
||||
*/
|
||||
suspend fun getTemplatesByCategory(categoryId: Int): ApiResult<List<TaskTemplate>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/by-category/$categoryId/")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch templates by category", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get templates filtered by climate region.
|
||||
* Accepts either a state abbreviation or ZIP code — backend resolves to climate zone.
|
||||
*/
|
||||
suspend fun getTemplatesByRegion(state: String? = null, zip: String? = null): ApiResult<List<TaskTemplate>> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/by-region/") {
|
||||
state?.let { parameter("state", it) }
|
||||
zip?.let { parameter("zip", it) }
|
||||
}
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Failed to fetch regional templates", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single template by ID
|
||||
*/
|
||||
suspend fun getTemplate(id: Int): ApiResult<TaskTemplate> {
|
||||
return try {
|
||||
val response = client.get("$baseUrl/tasks/templates/$id/")
|
||||
|
||||
if (response.status.isSuccess()) {
|
||||
ApiResult.Success(response.body())
|
||||
} else {
|
||||
ApiResult.Error("Template not found", response.status.value)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user