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:
@@ -0,0 +1,70 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
/**
|
||||
* Shared encoder/decoder for `.honeydue` payloads across Android and iOS.
|
||||
*
|
||||
* This keeps package JSON shape in one place while each platform owns
|
||||
* native share-sheet presentation details.
|
||||
*/
|
||||
object honeyDueShareCodec {
|
||||
private val json = Json {
|
||||
prettyPrint = true
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
|
||||
fun encodeContractorPackage(contractor: Contractor, exportedBy: String? = null): String {
|
||||
return encodeSharedContractor(contractor.toSharedContractor(exportedBy))
|
||||
}
|
||||
|
||||
fun encodeSharedContractor(sharedContractor: SharedContractor): String {
|
||||
return json.encodeToString(SharedContractor.serializer(), sharedContractor)
|
||||
}
|
||||
|
||||
fun encodeSharedResidence(sharedResidence: SharedResidence): String {
|
||||
return json.encodeToString(SharedResidence.serializer(), sharedResidence)
|
||||
}
|
||||
|
||||
fun decodeSharedContractorOrNull(jsonContent: String): SharedContractor? {
|
||||
return try {
|
||||
json.decodeFromString(SharedContractor.serializer(), jsonContent)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun decodeSharedResidenceOrNull(jsonContent: String): SharedResidence? {
|
||||
return try {
|
||||
json.decodeFromString(SharedResidence.serializer(), jsonContent)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun createContractorImportRequestOrNull(
|
||||
jsonContent: String,
|
||||
availableSpecialties: List<ContractorSpecialty>
|
||||
): ContractorCreateRequest? {
|
||||
val shared = decodeSharedContractorOrNull(jsonContent) ?: return null
|
||||
val specialtyIds = shared.resolveSpecialtyIds(availableSpecialties)
|
||||
return shared.toCreateRequest(specialtyIds)
|
||||
}
|
||||
|
||||
fun extractResidenceShareCodeOrNull(jsonContent: String): String? {
|
||||
return decodeSharedResidenceOrNull(jsonContent)?.shareCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filesystem-safe package filename with `.honeydue` extension.
|
||||
*/
|
||||
fun safeShareFileName(displayName: String): String {
|
||||
val safeName = displayName
|
||||
.replace(" ", "_")
|
||||
.replace("/", "-")
|
||||
.take(50)
|
||||
return "$safeName.honeydue"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ContractorUser(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@SerialName("last_name") val lastName: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Contractor(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("added_by") val addedBy: Int,
|
||||
@SerialName("created_by") val createdBy: ContractorUser? = null,
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val website: String? = null,
|
||||
val notes: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val specialties: List<ContractorSpecialty> = emptyList(),
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorCreateRequest(
|
||||
val name: String,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val website: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
val notes: String? = null,
|
||||
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorUpdateRequest(
|
||||
val name: String? = null,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val website: String? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean? = null,
|
||||
val notes: String? = null,
|
||||
@SerialName("specialty_ids") val specialtyIds: List<Int>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorSummary(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val specialties: List<ContractorSpecialty> = emptyList(),
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite") val isFavorite: Boolean = false,
|
||||
@SerialName("task_count") val taskCount: Int = 0
|
||||
)
|
||||
|
||||
// Extension to convert full Contractor to ContractorSummary
|
||||
fun Contractor.toSummary() = ContractorSummary(
|
||||
id = id,
|
||||
residenceId = residenceId,
|
||||
name = name,
|
||||
company = company,
|
||||
phone = phone,
|
||||
specialties = specialties,
|
||||
rating = rating,
|
||||
isFavorite = isFavorite,
|
||||
taskCount = taskCount
|
||||
)
|
||||
@@ -0,0 +1,218 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import com.tt.honeyDue.data.DataManager
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User reference for task-related responses - matching Go API TaskUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskUserResponse(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = ""
|
||||
) {
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Task response matching Go API TaskResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskResponse(
|
||||
val id: Int,
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("created_by") val createdBy: TaskUserResponse? = null,
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("assigned_to") val assignedTo: TaskUserResponse? = null,
|
||||
val title: String,
|
||||
val description: String = "",
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
val category: TaskCategory? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
val priority: TaskPriority? = null,
|
||||
@SerialName("in_progress") val inProgress: Boolean = false,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
val frequency: TaskFrequency? = null,
|
||||
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency, user-specified days
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("next_due_date") val nextDueDate: String? = null, // For recurring tasks, updated after each completion
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("contractor_id") val contractorId: Int? = null,
|
||||
@SerialName("is_cancelled") val isCancelled: Boolean = false,
|
||||
@SerialName("is_archived") val isArchived: Boolean = false,
|
||||
@SerialName("parent_task_id") val parentTaskId: Int? = null,
|
||||
@SerialName("completion_count") val completionCount: Int = 0,
|
||||
@SerialName("kanban_column") val kanbanColumn: String? = null, // Which kanban column this task belongs to
|
||||
val completions: List<TaskCompletionResponse> = emptyList(),
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
) {
|
||||
// Helper for backwards compatibility with old code
|
||||
val archived: Boolean get() = isArchived
|
||||
|
||||
// Aliases for backwards compatibility with UI code expecting old field names
|
||||
val residence: Int get() = residenceId
|
||||
|
||||
// Helper to get created by username
|
||||
val createdByUsername: String get() = createdBy?.displayName ?: ""
|
||||
|
||||
// Computed properties for UI compatibility - resolves from cache if not embedded in response
|
||||
// This allows the API to skip Preloading these lookups for performance
|
||||
val categoryName: String? get() = category?.name ?: DataManager.getTaskCategory(categoryId)?.name
|
||||
val categoryDescription: String? get() = category?.description ?: DataManager.getTaskCategory(categoryId)?.description
|
||||
val frequencyName: String? get() = frequency?.name ?: DataManager.getTaskFrequency(frequencyId)?.name
|
||||
val frequencyDisplayName: String? get() = frequency?.displayName ?: DataManager.getTaskFrequency(frequencyId)?.displayName
|
||||
val frequencyDaySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days
|
||||
val priorityName: String? get() = priority?.name ?: DataManager.getTaskPriority(priorityId)?.name
|
||||
val priorityDisplayName: String? get() = priority?.displayName ?: DataManager.getTaskPriority(priorityId)?.displayName
|
||||
|
||||
// Effective due date: use nextDueDate if set (for recurring tasks), otherwise use dueDate
|
||||
val effectiveDueDate: String? get() = nextDueDate ?: dueDate
|
||||
|
||||
// Fields that don't exist in Go API - return null/default
|
||||
val nextScheduledDate: String? get() = nextDueDate // Now we have this from the API
|
||||
val showCompletedButton: Boolean get() = true // Always show complete button since status is now just in_progress boolean
|
||||
val daySpan: Int? get() = frequency?.days ?: DataManager.getTaskFrequency(frequencyId)?.days
|
||||
val notifyDays: Int? get() = null // Not in Go API
|
||||
}
|
||||
|
||||
/**
|
||||
* Task completion response matching Go API TaskCompletionResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCompletionResponse(
|
||||
val id: Int,
|
||||
@SerialName("task_id") val taskId: Int,
|
||||
@SerialName("completed_by") val completedBy: TaskUserResponse? = null,
|
||||
@SerialName("completed_at") val completedAt: String,
|
||||
val notes: String = "",
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
val rating: Int? = null,
|
||||
val images: List<TaskCompletionImage> = emptyList(),
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("task") val updatedTask: TaskResponse? = null // Updated task after completion (for UI kanban update)
|
||||
) {
|
||||
// Helper for backwards compatibility
|
||||
val completionDate: String get() = completedAt
|
||||
val completedByName: String? get() = completedBy?.displayName
|
||||
|
||||
// Backwards compatibility for UI that expects these fields
|
||||
val task: Int get() = taskId
|
||||
val contractor: Int? get() = null // Not in Go API - would need to be fetched separately
|
||||
val contractorDetails: ContractorMinimal? get() = null // Not in Go API
|
||||
}
|
||||
|
||||
/**
|
||||
* Task create request matching Go API CreateTaskRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCreateRequest(
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
val title: String,
|
||||
val description: String? = null,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
@SerialName("in_progress") val inProgress: Boolean = false,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
@SerialName("contractor_id") val contractorId: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Task update request matching Go API UpdateTaskRequest
|
||||
* All fields are optional - only provided fields will be updated.
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskUpdateRequest(
|
||||
val title: String? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
@SerialName("priority_id") val priorityId: Int? = null,
|
||||
@SerialName("in_progress") val inProgress: Boolean? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
@SerialName("custom_interval_days") val customIntervalDays: Int? = null, // For "Custom" frequency
|
||||
@SerialName("assigned_to_id") val assignedToId: Int? = null,
|
||||
@SerialName("due_date") val dueDate: String? = null,
|
||||
@SerialName("estimated_cost") val estimatedCost: Double? = null,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
@SerialName("contractor_id") val contractorId: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Task action response (for cancel, archive, mark in progress, etc.)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskActionResponse(
|
||||
val message: String,
|
||||
val task: TaskResponse
|
||||
)
|
||||
|
||||
/**
|
||||
* Kanban column response matching Go API KanbanColumnResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskColumn(
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
@SerialName("button_types") val buttonTypes: List<String>,
|
||||
val icons: Map<String, String>,
|
||||
val color: String,
|
||||
val tasks: List<TaskResponse>,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Kanban board response matching Go API KanbanBoardResponse
|
||||
* NOTE: Summary statistics are calculated client-side from kanban data
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskColumnsResponse(
|
||||
val columns: List<TaskColumn>,
|
||||
@SerialName("days_threshold") val daysThreshold: Int,
|
||||
@SerialName("residence_id") val residenceId: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Task patch request for partial updates (status changes, archive/unarchive)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskPatchRequest(
|
||||
@SerialName("in_progress") val inProgress: Boolean? = null,
|
||||
@SerialName("is_archived") val archived: Boolean? = null,
|
||||
@SerialName("is_cancelled") val cancelled: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Task completion image model
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCompletionImage(
|
||||
val id: Int,
|
||||
@SerialName("image_url") val imageUrl: String,
|
||||
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/completion-image/{id}
|
||||
val caption: String? = null,
|
||||
@SerialName("uploaded_at") val uploadedAt: String? = null
|
||||
) {
|
||||
// Alias for backwards compatibility
|
||||
val image: String get() = imageUrl
|
||||
}
|
||||
|
||||
// Type aliases for backwards compatibility with existing code
|
||||
typealias CustomTask = TaskResponse
|
||||
typealias TaskDetail = TaskResponse
|
||||
typealias TaskCompletion = TaskCompletionResponse
|
||||
@@ -0,0 +1,161 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WarrantyStatus(
|
||||
@SerialName("status_text") val statusText: String,
|
||||
@SerialName("status_color") val statusColor: String,
|
||||
@SerialName("is_expiring_soon") val isExpiringSoon: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DocumentUser(
|
||||
val id: Int,
|
||||
val username: String = "",
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = ""
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DocumentImage(
|
||||
val id: Int? = null,
|
||||
@SerialName("image_url") val imageUrl: String,
|
||||
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/document-image/{id}
|
||||
val caption: String? = null,
|
||||
@SerialName("uploaded_at") val uploadedAt: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DocumentActionResponse(
|
||||
val message: String,
|
||||
val document: Document
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Document(
|
||||
val id: Int? = null,
|
||||
val title: String,
|
||||
@SerialName("document_type") val documentType: String,
|
||||
val description: String? = null,
|
||||
@SerialName("file_url") val fileUrl: String? = null, // URL to the file
|
||||
@SerialName("media_url") val mediaUrl: String? = null, // Authenticated endpoint: /api/media/document/{id}
|
||||
@SerialName("file_name") val fileName: String? = null,
|
||||
@SerialName("file_size") val fileSize: Int? = null,
|
||||
@SerialName("mime_type") val mimeType: String? = null,
|
||||
// Warranty-specific fields
|
||||
@SerialName("model_number") val modelNumber: String? = null,
|
||||
@SerialName("serial_number") val serialNumber: String? = null,
|
||||
val vendor: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: String? = null,
|
||||
@SerialName("expiry_date") val expiryDate: String? = null,
|
||||
// Relationships
|
||||
@SerialName("residence_id") val residenceId: Int? = null,
|
||||
val residence: Int,
|
||||
@SerialName("created_by_id") val createdById: Int? = null,
|
||||
@SerialName("created_by") val createdBy: DocumentUser? = null,
|
||||
@SerialName("task_id") val taskId: Int? = null,
|
||||
// Images
|
||||
val images: List<DocumentImage> = emptyList(),
|
||||
// Status
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("created_at") val createdAt: String? = null,
|
||||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
// Client-side convenience fields (not from backend, kept for UI compatibility)
|
||||
// These fields are populated client-side or kept optional so deserialization doesn't fail
|
||||
val category: String? = null,
|
||||
val tags: String? = null,
|
||||
val notes: String? = null,
|
||||
@SerialName("item_name") val itemName: String? = null,
|
||||
val provider: String? = null,
|
||||
@SerialName("provider_contact") val providerContact: String? = null,
|
||||
@SerialName("claim_phone") val claimPhone: String? = null,
|
||||
@SerialName("claim_email") val claimEmail: String? = null,
|
||||
@SerialName("claim_website") val claimWebsite: String? = null,
|
||||
@SerialName("start_date") val startDate: String? = null,
|
||||
@SerialName("days_until_expiration") val daysUntilExpiration: Int? = null,
|
||||
@SerialName("warranty_status") val warrantyStatus: WarrantyStatus? = null
|
||||
) {
|
||||
// Backward-compatible alias: endDate maps to expiryDate
|
||||
val endDate: String? get() = expiryDate
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class DocumentCreateRequest(
|
||||
val title: String,
|
||||
@SerialName("document_type") val documentType: String,
|
||||
val description: String? = null,
|
||||
// Note: file will be handled separately as multipart/form-data
|
||||
// Warranty-specific fields
|
||||
@SerialName("model_number") val modelNumber: String? = null,
|
||||
@SerialName("serial_number") val serialNumber: String? = null,
|
||||
val vendor: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: String? = null,
|
||||
@SerialName("expiry_date") val expiryDate: String? = null,
|
||||
// Relationships
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
@SerialName("task_id") val taskId: Int? = null,
|
||||
// Images
|
||||
@SerialName("image_urls") val imageUrls: List<String>? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DocumentUpdateRequest(
|
||||
val title: String? = null,
|
||||
@SerialName("document_type") val documentType: String? = null,
|
||||
val description: String? = null,
|
||||
// Warranty-specific fields
|
||||
@SerialName("model_number") val modelNumber: String? = null,
|
||||
@SerialName("serial_number") val serialNumber: String? = null,
|
||||
val vendor: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: String? = null,
|
||||
@SerialName("expiry_date") val expiryDate: String? = null,
|
||||
// Relationships
|
||||
@SerialName("task_id") val taskId: Int? = null
|
||||
)
|
||||
|
||||
// Removed: DocumentListResponse - no longer using paginated responses
|
||||
// API now returns List<Document> directly
|
||||
|
||||
// Document type choices
|
||||
enum class DocumentType(val value: String, val displayName: String) {
|
||||
WARRANTY("warranty", "Warranty"),
|
||||
MANUAL("manual", "User Manual"),
|
||||
RECEIPT("receipt", "Receipt/Invoice"),
|
||||
INSPECTION("inspection", "Inspection Report"),
|
||||
PERMIT("permit", "Permit"),
|
||||
DEED("deed", "Deed/Title"),
|
||||
INSURANCE("insurance", "Insurance"),
|
||||
CONTRACT("contract", "Contract"),
|
||||
PHOTO("photo", "Photo"),
|
||||
OTHER("other", "Other");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): DocumentType {
|
||||
return values().find { it.value == value } ?: OTHER
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Document/Warranty category choices
|
||||
enum class DocumentCategory(val value: String, val displayName: String) {
|
||||
APPLIANCE("appliance", "Appliance"),
|
||||
HVAC("hvac", "HVAC"),
|
||||
PLUMBING("plumbing", "Plumbing"),
|
||||
ELECTRICAL("electrical", "Electrical"),
|
||||
ROOFING("roofing", "Roofing"),
|
||||
STRUCTURAL("structural", "Structural"),
|
||||
LANDSCAPING("landscaping", "Landscaping"),
|
||||
GENERAL("general", "General"),
|
||||
OTHER("other", "Other");
|
||||
|
||||
companion object {
|
||||
fun fromValue(value: String): DocumentCategory {
|
||||
return values().find { it.value == value } ?: OTHER
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Error response model that handles the backend's error format.
|
||||
* The backend returns: {"error": "message"}
|
||||
* All fields except 'error' are optional for backwards compatibility.
|
||||
*/
|
||||
@Serializable
|
||||
data class ErrorResponse(
|
||||
val error: String,
|
||||
val detail: String? = null,
|
||||
@SerialName("status_code") val statusCode: Int? = null,
|
||||
val errors: Map<String, List<String>>? = null
|
||||
)
|
||||
@@ -0,0 +1,140 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Residence type lookup - matching Go API
|
||||
* Note: Go API returns arrays directly, no wrapper
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceType(
|
||||
val id: Int,
|
||||
val name: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Task frequency lookup - matching Go API TaskFrequencyResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskFrequency(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val days: Int? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
val displayName: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Task priority lookup - matching Go API TaskPriorityResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskPriority(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val level: Int = 0,
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
) {
|
||||
// Helper for display
|
||||
val displayName: String
|
||||
get() = name
|
||||
}
|
||||
|
||||
/**
|
||||
* Task category lookup - matching Go API TaskCategoryResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategory(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String = "",
|
||||
val icon: String = "",
|
||||
val color: String = "",
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Contractor specialty lookup
|
||||
*/
|
||||
@Serializable
|
||||
data class ContractorSpecialty(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
val icon: String? = null,
|
||||
@SerialName("display_order") val displayOrder: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Minimal contractor info for task references
|
||||
*/
|
||||
@Serializable
|
||||
data class ContractorMinimal(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val company: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Static data response - all lookups in one call
|
||||
* Note: This may need adjustment based on Go API implementation
|
||||
*/
|
||||
@Serializable
|
||||
data class StaticDataResponse(
|
||||
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>
|
||||
)
|
||||
|
||||
/**
|
||||
* Unified seeded data response - all lookups + task templates in one call
|
||||
* Supports ETag-based conditional fetching for efficient caching
|
||||
*/
|
||||
@Serializable
|
||||
data class SeededDataResponse(
|
||||
@SerialName("residence_types") val residenceTypes: List<ResidenceType>,
|
||||
@SerialName("task_categories") val taskCategories: List<TaskCategory>,
|
||||
@SerialName("task_priorities") val taskPriorities: List<TaskPriority>,
|
||||
@SerialName("task_frequencies") val taskFrequencies: List<TaskFrequency>,
|
||||
@SerialName("contractor_specialties") val contractorSpecialties: List<ContractorSpecialty>,
|
||||
@SerialName("task_templates") val taskTemplates: TaskTemplatesGroupedResponse
|
||||
)
|
||||
|
||||
// Legacy wrapper responses for backward compatibility
|
||||
// These can be removed once all code is migrated to use arrays directly
|
||||
|
||||
@Serializable
|
||||
data class ResidenceTypeResponse(
|
||||
val count: Int,
|
||||
val results: List<ResidenceType>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskFrequencyResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskFrequency>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskPriorityResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskPriority>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TaskCategoryResponse(
|
||||
val count: Int,
|
||||
val results: List<TaskCategory>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ContractorSpecialtyResponse(
|
||||
val count: Int,
|
||||
val results: List<ContractorSpecialty>
|
||||
)
|
||||
@@ -0,0 +1,120 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DeviceRegistrationRequest(
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
@SerialName("registration_id")
|
||||
val registrationId: String,
|
||||
val platform: String, // "android" or "ios"
|
||||
val name: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeviceRegistrationResponse(
|
||||
val id: Int,
|
||||
val name: String? = null,
|
||||
@SerialName("device_id")
|
||||
val deviceId: String,
|
||||
@SerialName("registration_id")
|
||||
val registrationId: String,
|
||||
val platform: String,
|
||||
val active: Boolean,
|
||||
@SerialName("date_created")
|
||||
val dateCreated: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NotificationPreference(
|
||||
@SerialName("task_due_soon")
|
||||
val taskDueSoon: Boolean = true,
|
||||
@SerialName("task_overdue")
|
||||
val taskOverdue: Boolean = true,
|
||||
@SerialName("task_completed")
|
||||
val taskCompleted: Boolean = true,
|
||||
@SerialName("task_assigned")
|
||||
val taskAssigned: Boolean = true,
|
||||
@SerialName("residence_shared")
|
||||
val residenceShared: Boolean = true,
|
||||
@SerialName("warranty_expiring")
|
||||
val warrantyExpiring: Boolean = true,
|
||||
@SerialName("daily_digest")
|
||||
val dailyDigest: Boolean = true,
|
||||
// Email preferences
|
||||
@SerialName("email_task_completed")
|
||||
val emailTaskCompleted: Boolean = true,
|
||||
// Custom notification times (UTC hour 0-23, null means use system default)
|
||||
@SerialName("task_due_soon_hour")
|
||||
val taskDueSoonHour: Int? = null,
|
||||
@SerialName("task_overdue_hour")
|
||||
val taskOverdueHour: Int? = null,
|
||||
@SerialName("warranty_expiring_hour")
|
||||
val warrantyExpiringHour: Int? = null,
|
||||
@SerialName("daily_digest_hour")
|
||||
val dailyDigestHour: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateNotificationPreferencesRequest(
|
||||
@SerialName("task_due_soon")
|
||||
val taskDueSoon: Boolean? = null,
|
||||
@SerialName("task_overdue")
|
||||
val taskOverdue: Boolean? = null,
|
||||
@SerialName("task_completed")
|
||||
val taskCompleted: Boolean? = null,
|
||||
@SerialName("task_assigned")
|
||||
val taskAssigned: Boolean? = null,
|
||||
@SerialName("residence_shared")
|
||||
val residenceShared: Boolean? = null,
|
||||
@SerialName("warranty_expiring")
|
||||
val warrantyExpiring: Boolean? = null,
|
||||
@SerialName("daily_digest")
|
||||
val dailyDigest: Boolean? = null,
|
||||
// Email preferences
|
||||
@SerialName("email_task_completed")
|
||||
val emailTaskCompleted: Boolean? = null,
|
||||
// Custom notification times (UTC hour 0-23)
|
||||
@SerialName("task_due_soon_hour")
|
||||
val taskDueSoonHour: Int? = null,
|
||||
@SerialName("task_overdue_hour")
|
||||
val taskOverdueHour: Int? = null,
|
||||
@SerialName("warranty_expiring_hour")
|
||||
val warrantyExpiringHour: Int? = null,
|
||||
@SerialName("daily_digest_hour")
|
||||
val dailyDigestHour: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Notification(
|
||||
val id: Int,
|
||||
@SerialName("notification_type")
|
||||
val notificationType: String,
|
||||
val title: String,
|
||||
val body: String,
|
||||
val data: Map<String, String> = emptyMap(),
|
||||
val sent: Boolean,
|
||||
@SerialName("sent_at")
|
||||
val sentAt: String? = null,
|
||||
val read: Boolean,
|
||||
@SerialName("read_at")
|
||||
val readAt: String? = null,
|
||||
@SerialName("created_at")
|
||||
val createdAt: String,
|
||||
@SerialName("task_id")
|
||||
val taskId: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class NotificationListResponse(
|
||||
val count: Int,
|
||||
val results: List<Notification>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UnreadCountResponse(
|
||||
@SerialName("unread_count")
|
||||
val unreadCount: Int
|
||||
)
|
||||
@@ -0,0 +1,269 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User reference for residence responses - matching Go API ResidenceUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceUserResponse(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = ""
|
||||
) {
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Residence response matching Go API ResidenceResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceResponse(
|
||||
val id: Int,
|
||||
@SerialName("owner_id") val ownerId: Int,
|
||||
val owner: ResidenceUserResponse? = null,
|
||||
val users: List<ResidenceUserResponse> = emptyList(),
|
||||
val name: String,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("property_type") val propertyType: ResidenceType? = null,
|
||||
@SerialName("street_address") val streetAddress: String = "",
|
||||
@SerialName("apartment_unit") val apartmentUnit: String = "",
|
||||
val city: String = "",
|
||||
@SerialName("state_province") val stateProvince: String = "",
|
||||
@SerialName("postal_code") val postalCode: String = "",
|
||||
val country: String = "",
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String = "",
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean = false,
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("overdue_count") val overdueCount: Int = 0,
|
||||
@SerialName("created_at") val createdAt: String,
|
||||
@SerialName("updated_at") val updatedAt: String
|
||||
) {
|
||||
// Helper to get owner username
|
||||
val ownerUsername: String get() = owner?.displayName ?: ""
|
||||
|
||||
// Helper to get property type name
|
||||
val propertyTypeName: String? get() = propertyType?.name
|
||||
|
||||
// Backwards compatibility for UI code
|
||||
// Note: isPrimaryOwner requires comparing with current user - can't be computed here
|
||||
// UI components should check ownerId == currentUserId instead
|
||||
|
||||
// Stub task summary for UI compatibility (Go API doesn't return this per-residence)
|
||||
val taskSummary: ResidenceTaskSummary get() = ResidenceTaskSummary()
|
||||
|
||||
// Stub summary for UI compatibility
|
||||
val summary: ResidenceSummaryResponse get() = ResidenceSummaryResponse(id = id, name = name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Residence create request matching Go API CreateResidenceRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceCreateRequest(
|
||||
val name: String,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Residence update request matching Go API UpdateResidenceRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceUpdateRequest(
|
||||
val name: String? = null,
|
||||
@SerialName("property_type_id") val propertyTypeId: Int? = null,
|
||||
@SerialName("street_address") val streetAddress: String? = null,
|
||||
@SerialName("apartment_unit") val apartmentUnit: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province") val stateProvince: String? = null,
|
||||
@SerialName("postal_code") val postalCode: String? = null,
|
||||
val country: String? = null,
|
||||
val bedrooms: Int? = null,
|
||||
val bathrooms: Double? = null,
|
||||
@SerialName("square_footage") val squareFootage: Int? = null,
|
||||
@SerialName("lot_size") val lotSize: Double? = null,
|
||||
@SerialName("year_built") val yearBuilt: Int? = null,
|
||||
val description: String? = null,
|
||||
@SerialName("purchase_date") val purchaseDate: String? = null,
|
||||
@SerialName("purchase_price") val purchasePrice: Double? = null,
|
||||
@SerialName("is_primary") val isPrimary: Boolean? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Share code response matching Go API ShareCodeResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class ShareCodeResponse(
|
||||
val id: Int,
|
||||
val code: String,
|
||||
@SerialName("residence_id") val residenceId: Int,
|
||||
@SerialName("created_by_id") val createdById: Int,
|
||||
@SerialName("is_active") val isActive: Boolean,
|
||||
@SerialName("expires_at") val expiresAt: String?,
|
||||
@SerialName("created_at") val createdAt: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Generate share code request
|
||||
*/
|
||||
@Serializable
|
||||
data class GenerateShareCodeRequest(
|
||||
@SerialName("expires_in_hours") val expiresInHours: Int? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Generate share code response matching Go API GenerateShareCodeResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class GenerateShareCodeResponse(
|
||||
val message: String,
|
||||
@SerialName("share_code") val shareCode: ShareCodeResponse
|
||||
)
|
||||
|
||||
/**
|
||||
* Join residence request
|
||||
*/
|
||||
@Serializable
|
||||
data class JoinResidenceRequest(
|
||||
val code: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Join residence response matching Go API JoinResidenceResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class JoinResidenceResponse(
|
||||
val message: String,
|
||||
val residence: ResidenceResponse,
|
||||
val summary: TotalSummary
|
||||
)
|
||||
|
||||
/**
|
||||
* Total summary for dashboard display
|
||||
*/
|
||||
@Serializable
|
||||
data class TotalSummary(
|
||||
@SerialName("total_residences") val totalResidences: Int = 0,
|
||||
@SerialName("total_tasks") val totalTasks: Int = 0,
|
||||
@SerialName("total_pending") val totalPending: Int = 0,
|
||||
@SerialName("total_overdue") val totalOverdue: Int = 0,
|
||||
@SerialName("tasks_due_next_week") val tasksDueNextWeek: Int = 0,
|
||||
@SerialName("tasks_due_next_month") val tasksDueNextMonth: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Generic wrapper for CRUD responses that include TotalSummary.
|
||||
* Used for Task and TaskCompletion operations to eliminate extra API calls
|
||||
* for updating dashboard stats.
|
||||
*
|
||||
* Usage examples:
|
||||
* - WithSummaryResponse<TaskResponse> for task CRUD
|
||||
* - WithSummaryResponse<TaskCompletionResponse> for completion CRUD
|
||||
* - WithSummaryResponse<String> for delete operations (data = "task deleted")
|
||||
*/
|
||||
@Serializable
|
||||
data class WithSummaryResponse<T>(
|
||||
val data: T,
|
||||
val summary: TotalSummary
|
||||
)
|
||||
|
||||
/**
|
||||
* My residences response - list of user's residences
|
||||
* NOTE: Summary statistics are calculated client-side from kanban data
|
||||
*/
|
||||
@Serializable
|
||||
data class MyResidencesResponse(
|
||||
val residences: List<ResidenceResponse>
|
||||
)
|
||||
|
||||
/**
|
||||
* Residence summary response for dashboard
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceSummaryResponse(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
@SerialName("task_count") val taskCount: Int = 0,
|
||||
@SerialName("pending_count") val pendingCount: Int = 0,
|
||||
@SerialName("overdue_count") val overdueCount: Int = 0
|
||||
)
|
||||
|
||||
/**
|
||||
* Task category summary for residence
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategorySummary(
|
||||
val name: String,
|
||||
@SerialName("display_name") val displayName: String,
|
||||
val icons: TaskCategoryIcons,
|
||||
val color: String,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Icons for task category (Android/iOS)
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCategoryIcons(
|
||||
val android: String = "",
|
||||
val ios: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Task summary per residence (for UI backwards compatibility)
|
||||
*/
|
||||
@Serializable
|
||||
data class ResidenceTaskSummary(
|
||||
val categories: List<TaskCategorySummary> = emptyList()
|
||||
)
|
||||
|
||||
/**
|
||||
* Residence users response - API returns a flat list of all users with access
|
||||
*/
|
||||
typealias ResidenceUsersResponse = List<ResidenceUserResponse>
|
||||
|
||||
/**
|
||||
* Remove user response
|
||||
*/
|
||||
@Serializable
|
||||
data class RemoveUserResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
// Type aliases for backwards compatibility with existing code
|
||||
typealias Residence = ResidenceResponse
|
||||
typealias ResidenceShareCode = ShareCodeResponse
|
||||
typealias ResidenceUser = ResidenceUserResponse
|
||||
typealias TaskSummary = ResidenceTaskSummary
|
||||
typealias TaskColumnCategory = TaskCategorySummary
|
||||
@@ -0,0 +1,173 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
/**
|
||||
* Package type identifiers for .honeydue files
|
||||
*/
|
||||
object honeyDuePackageType {
|
||||
const val CONTRACTOR = "contractor"
|
||||
const val RESIDENCE = "residence"
|
||||
}
|
||||
|
||||
/**
|
||||
* Data model for .honeydue file format used to share contractors between users.
|
||||
* Contains only the data needed to recreate a contractor, without server-specific IDs.
|
||||
*/
|
||||
@Serializable
|
||||
data class SharedContractor(
|
||||
/** File format version for future compatibility */
|
||||
val version: Int = 1,
|
||||
|
||||
/** Package type discriminator */
|
||||
val type: String = honeyDuePackageType.CONTRACTOR,
|
||||
|
||||
val name: String,
|
||||
val company: String? = null,
|
||||
val phone: String? = null,
|
||||
val email: String? = null,
|
||||
val website: String? = null,
|
||||
val notes: String? = null,
|
||||
|
||||
@SerialName("street_address")
|
||||
val streetAddress: String? = null,
|
||||
val city: String? = null,
|
||||
@SerialName("state_province")
|
||||
val stateProvince: String? = null,
|
||||
@SerialName("postal_code")
|
||||
val postalCode: String? = null,
|
||||
|
||||
/** Specialty names (not IDs) for cross-account compatibility */
|
||||
@SerialName("specialty_names")
|
||||
val specialtyNames: List<String> = emptyList(),
|
||||
|
||||
val rating: Double? = null,
|
||||
@SerialName("is_favorite")
|
||||
val isFavorite: Boolean = false,
|
||||
|
||||
/** ISO8601 timestamp when the contractor was exported */
|
||||
@SerialName("exported_at")
|
||||
val exportedAt: String? = null,
|
||||
|
||||
/** Username of the person who exported the contractor */
|
||||
@SerialName("exported_by")
|
||||
val exportedBy: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Data model for .honeydue file format used to share residences between users.
|
||||
* Contains the share code needed to join the residence.
|
||||
*/
|
||||
@Serializable
|
||||
data class SharedResidence(
|
||||
/** File format version for future compatibility */
|
||||
val version: Int = 1,
|
||||
|
||||
/** Package type discriminator */
|
||||
val type: String = honeyDuePackageType.RESIDENCE,
|
||||
|
||||
/** The share code for joining the residence */
|
||||
@SerialName("share_code")
|
||||
val shareCode: String,
|
||||
|
||||
/** Name of the residence being shared */
|
||||
@SerialName("residence_name")
|
||||
val residenceName: String,
|
||||
|
||||
/** Email of the person sharing the residence */
|
||||
@SerialName("shared_by")
|
||||
val sharedBy: String? = null,
|
||||
|
||||
/** ISO8601 timestamp when the code expires */
|
||||
@SerialName("expires_at")
|
||||
val expiresAt: String? = null,
|
||||
|
||||
/** ISO8601 timestamp when the package was created */
|
||||
@SerialName("exported_at")
|
||||
val exportedAt: String? = null,
|
||||
|
||||
/** Username of the person who created the package */
|
||||
@SerialName("exported_by")
|
||||
val exportedBy: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Detect the type of a .honeydue package from its JSON content.
|
||||
* Returns null if the type cannot be determined.
|
||||
*/
|
||||
fun detecthoneyDuePackageType(jsonContent: String): String? {
|
||||
return try {
|
||||
val json = Json { ignoreUnknownKeys = true }
|
||||
val jsonObject = json.decodeFromString<JsonObject>(jsonContent)
|
||||
jsonObject["type"]?.jsonPrimitive?.content ?: honeyDuePackageType.CONTRACTOR // Default for backward compatibility
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a full Contractor to SharedContractor for export.
|
||||
*/
|
||||
@OptIn(ExperimentalTime::class)
|
||||
fun Contractor.toSharedContractor(exportedBy: String? = null): SharedContractor {
|
||||
return SharedContractor(
|
||||
version = 1,
|
||||
type = honeyDuePackageType.CONTRACTOR,
|
||||
name = name,
|
||||
company = company,
|
||||
phone = phone,
|
||||
email = email,
|
||||
website = website,
|
||||
notes = notes,
|
||||
streetAddress = streetAddress,
|
||||
city = city,
|
||||
stateProvince = stateProvince,
|
||||
postalCode = postalCode,
|
||||
specialtyNames = specialties.map { it.name },
|
||||
rating = rating,
|
||||
isFavorite = isFavorite,
|
||||
exportedAt = Clock.System.now().toString(),
|
||||
exportedBy = exportedBy
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert SharedContractor to ContractorCreateRequest for import.
|
||||
* @param specialtyIds The resolved specialty IDs from the importing account's lookup data
|
||||
*/
|
||||
fun SharedContractor.toCreateRequest(specialtyIds: List<Int>): ContractorCreateRequest {
|
||||
return ContractorCreateRequest(
|
||||
name = name,
|
||||
residenceId = null, // Imported contractors have no residence association
|
||||
company = company,
|
||||
phone = phone,
|
||||
email = email,
|
||||
website = website,
|
||||
streetAddress = streetAddress,
|
||||
city = city,
|
||||
stateProvince = stateProvince,
|
||||
postalCode = postalCode,
|
||||
rating = rating,
|
||||
isFavorite = isFavorite,
|
||||
notes = notes,
|
||||
specialtyIds = specialtyIds.ifEmpty { null }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve specialty names to IDs using the available specialties in the importing account.
|
||||
* Case-insensitive matching.
|
||||
*/
|
||||
fun SharedContractor.resolveSpecialtyIds(availableSpecialties: List<ContractorSpecialty>): List<Int> {
|
||||
return specialtyNames.mapNotNull { name ->
|
||||
availableSpecialties.find { specialty ->
|
||||
specialty.name.equals(name, ignoreCase = true)
|
||||
}?.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SubscriptionStatus(
|
||||
val tier: String = "free",
|
||||
@SerialName("is_active") val isActive: Boolean = false,
|
||||
@SerialName("subscribed_at") val subscribedAt: String? = null,
|
||||
@SerialName("expires_at") val expiresAt: String? = null,
|
||||
@SerialName("auto_renew") val autoRenew: Boolean = true,
|
||||
val usage: UsageStats,
|
||||
val limits: Map<String, TierLimits>, // {"free": {...}, "pro": {...}}
|
||||
@SerialName("limitations_enabled") val limitationsEnabled: Boolean = false, // Master toggle
|
||||
@SerialName("trial_start") val trialStart: String? = null,
|
||||
@SerialName("trial_end") val trialEnd: String? = null,
|
||||
@SerialName("trial_active") val trialActive: Boolean = false,
|
||||
@SerialName("subscription_source") val subscriptionSource: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UsageStats(
|
||||
@SerialName("properties_count") val propertiesCount: Int,
|
||||
@SerialName("tasks_count") val tasksCount: Int,
|
||||
@SerialName("contractors_count") val contractorsCount: Int,
|
||||
@SerialName("documents_count") val documentsCount: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TierLimits(
|
||||
val properties: Int? = null, // null = unlimited
|
||||
val tasks: Int? = null,
|
||||
val contractors: Int? = null,
|
||||
val documents: Int? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpgradeTriggerData(
|
||||
val title: String,
|
||||
val message: String,
|
||||
@SerialName("promo_html") val promoHtml: String? = null,
|
||||
@SerialName("button_text") val buttonText: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class FeatureBenefit(
|
||||
@SerialName("feature_name") val featureName: String,
|
||||
@SerialName("free_tier_text") val freeTierText: String,
|
||||
@SerialName("pro_tier_text") val proTierText: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Promotion(
|
||||
@SerialName("promotion_id") val promotionId: String,
|
||||
val title: String,
|
||||
val message: String,
|
||||
val link: String? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ReceiptVerificationRequest(
|
||||
@SerialName("receipt_data") val receiptData: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PurchaseVerificationRequest(
|
||||
@SerialName("purchase_token") val purchaseToken: String,
|
||||
@SerialName("product_id") val productId: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Nested subscription info returned by backend purchase/restore endpoints.
|
||||
*/
|
||||
@Serializable
|
||||
data class VerificationSubscriptionInfo(
|
||||
val tier: String = "",
|
||||
@SerialName("subscribed_at") val subscribedAt: String? = null,
|
||||
@SerialName("expires_at") val expiresAt: String? = null,
|
||||
@SerialName("auto_renew") val autoRenew: Boolean = true,
|
||||
@SerialName("cancelled_at") val cancelledAt: String? = null,
|
||||
val platform: String = "",
|
||||
@SerialName("is_active") val isActive: Boolean = false,
|
||||
@SerialName("is_pro") val isPro: Boolean = false
|
||||
)
|
||||
|
||||
/**
|
||||
* Response from backend purchase/restore endpoints.
|
||||
* Backend returns: { "message": "...", "subscription": { "tier": "pro", ... } }
|
||||
*/
|
||||
@Serializable
|
||||
data class VerificationResponse(
|
||||
val message: String = "",
|
||||
val subscription: VerificationSubscriptionInfo? = null
|
||||
) {
|
||||
/** Backward-compatible: success when subscription is present */
|
||||
val success: Boolean get() = subscription != null
|
||||
|
||||
/** Backward-compatible: tier extracted from nested subscription */
|
||||
val tier: String? get() = subscription?.tier
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Task completion create request matching Go API CreateTaskCompletionRequest
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskCompletionCreateRequest(
|
||||
@SerialName("task_id") val taskId: Int,
|
||||
@SerialName("completed_at") val completedAt: String? = null, // Defaults to now on server
|
||||
val notes: String? = null,
|
||||
@SerialName("actual_cost") val actualCost: Double? = null,
|
||||
val rating: Int? = null, // 1-5 star rating
|
||||
@SerialName("image_urls") val imageUrls: List<String>? = null // Multiple image URLs
|
||||
)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a task template fetched from the backend API.
|
||||
* Users can select these when adding a new task to auto-fill form fields.
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskTemplate(
|
||||
val id: Int,
|
||||
val title: String,
|
||||
val description: String = "",
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
val category: TaskCategory? = null,
|
||||
@SerialName("frequency_id") val frequencyId: Int? = null,
|
||||
val frequency: TaskFrequency? = null,
|
||||
@SerialName("icon_ios") val iconIos: String = "",
|
||||
@SerialName("icon_android") val iconAndroid: String = "",
|
||||
val tags: List<String> = emptyList(),
|
||||
@SerialName("display_order") val displayOrder: Int = 0,
|
||||
@SerialName("is_active") val isActive: Boolean = true
|
||||
) {
|
||||
/**
|
||||
* Human-readable frequency display
|
||||
*/
|
||||
val frequencyDisplay: String
|
||||
get() = frequency?.displayName ?: "One time"
|
||||
|
||||
/**
|
||||
* Category name for display
|
||||
*/
|
||||
val categoryName: String
|
||||
get() = category?.name ?: "Uncategorized"
|
||||
}
|
||||
|
||||
/**
|
||||
* Response for grouped templates by category
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskTemplateCategoryGroup(
|
||||
@SerialName("category_name") val categoryName: String,
|
||||
@SerialName("category_id") val categoryId: Int? = null,
|
||||
val templates: List<TaskTemplate>,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
/**
|
||||
* Response for all templates grouped by category
|
||||
*/
|
||||
@Serializable
|
||||
data class TaskTemplatesGroupedResponse(
|
||||
val categories: List<TaskTemplateCategoryGroup>,
|
||||
@SerialName("total_count") val totalCount: Int
|
||||
)
|
||||
@@ -0,0 +1,208 @@
|
||||
package com.tt.honeyDue.models
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* User model matching Go API UserResponse/CurrentUserResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: Int,
|
||||
val username: String,
|
||||
val email: String,
|
||||
@SerialName("first_name") val firstName: String = "",
|
||||
@SerialName("last_name") val lastName: String = "",
|
||||
@SerialName("is_active") val isActive: Boolean = true,
|
||||
@SerialName("date_joined") val dateJoined: String,
|
||||
@SerialName("last_login") val lastLogin: String? = null,
|
||||
// Profile is included in CurrentUserResponse (/auth/me)
|
||||
val profile: UserProfile? = null,
|
||||
// Verified is returned directly in LoginResponse, and also in profile for CurrentUserResponse
|
||||
@SerialName("verified") private val _verified: Boolean = false
|
||||
) {
|
||||
// Computed property for display name
|
||||
val displayName: String
|
||||
get() = when {
|
||||
firstName.isNotBlank() && lastName.isNotBlank() -> "$firstName $lastName"
|
||||
firstName.isNotBlank() -> firstName
|
||||
lastName.isNotBlank() -> lastName
|
||||
else -> username
|
||||
}
|
||||
|
||||
// Check if user is verified - from direct field (login) OR from profile (currentUser)
|
||||
val isVerified: Boolean
|
||||
get() = _verified || (profile?.verified ?: false)
|
||||
|
||||
// Alias for backwards compatibility
|
||||
val verified: Boolean
|
||||
get() = isVerified
|
||||
}
|
||||
|
||||
/**
|
||||
* User profile model matching Go API UserProfileResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class UserProfile(
|
||||
val id: Int,
|
||||
@SerialName("user_id") val userId: Int,
|
||||
val verified: Boolean = false,
|
||||
val bio: String = "",
|
||||
@SerialName("phone_number") val phoneNumber: String = "",
|
||||
@SerialName("date_of_birth") val dateOfBirth: String? = null,
|
||||
@SerialName("profile_picture") val profilePicture: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Register request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterRequest(
|
||||
val username: String,
|
||||
val email: String,
|
||||
val password: String,
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@SerialName("last_name") val lastName: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Login request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class LoginRequest(
|
||||
val username: String,
|
||||
val password: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Auth response for login - matching Go API LoginResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class AuthResponse(
|
||||
val token: String,
|
||||
val user: User
|
||||
)
|
||||
|
||||
/**
|
||||
* Auth response for registration - matching Go API RegisterResponse
|
||||
*/
|
||||
@Serializable
|
||||
data class RegisterResponse(
|
||||
val token: String,
|
||||
val user: User,
|
||||
val message: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify email request
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailRequest(
|
||||
val code: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Verify email response
|
||||
*/
|
||||
@Serializable
|
||||
data class VerifyEmailResponse(
|
||||
val message: String,
|
||||
val verified: Boolean
|
||||
)
|
||||
|
||||
/**
|
||||
* Update profile request
|
||||
*/
|
||||
@Serializable
|
||||
data class UpdateProfileRequest(
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@SerialName("last_name") val lastName: String? = null,
|
||||
val email: String? = null
|
||||
)
|
||||
|
||||
// Password Reset Models
|
||||
|
||||
@Serializable
|
||||
data class ForgotPasswordRequest(
|
||||
val email: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ForgotPasswordResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VerifyResetCodeRequest(
|
||||
val email: String,
|
||||
val code: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class VerifyResetCodeResponse(
|
||||
val message: String,
|
||||
@SerialName("reset_token") val resetToken: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResetPasswordRequest(
|
||||
@SerialName("reset_token") val resetToken: String,
|
||||
@SerialName("new_password") val newPassword: String
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ResetPasswordResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Generic message response used by many endpoints
|
||||
*/
|
||||
@Serializable
|
||||
data class MessageResponse(
|
||||
val message: String
|
||||
)
|
||||
|
||||
// Apple Sign In Models
|
||||
|
||||
/**
|
||||
* Apple Sign In request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class AppleSignInRequest(
|
||||
@SerialName("id_token") val idToken: String,
|
||||
@SerialName("user_id") val userId: String,
|
||||
val email: String? = null,
|
||||
@SerialName("first_name") val firstName: String? = null,
|
||||
@SerialName("last_name") val lastName: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Apple Sign In response matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class AppleSignInResponse(
|
||||
val token: String,
|
||||
val user: User,
|
||||
@SerialName("is_new_user") val isNewUser: Boolean
|
||||
)
|
||||
|
||||
// Google Sign In Models
|
||||
|
||||
/**
|
||||
* Google Sign In request matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class GoogleSignInRequest(
|
||||
@SerialName("id_token") val idToken: String
|
||||
)
|
||||
|
||||
/**
|
||||
* Google Sign In response matching Go API
|
||||
*/
|
||||
@Serializable
|
||||
data class GoogleSignInResponse(
|
||||
val token: String,
|
||||
val user: User,
|
||||
@SerialName("is_new_user") val isNewUser: Boolean
|
||||
)
|
||||
Reference in New Issue
Block a user