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:
Trey t
2026-03-07 06:33:57 -06:00
parent 9c574c4343
commit 1e2adf7660
450 changed files with 1730 additions and 1788 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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()}")
}
}

View File

@@ -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_")
}

View File

@@ -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>()
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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})"
}
}
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}

View File

@@ -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")
}
}
}