diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt index 1f346e6..302daab 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/network/ResidenceApi.kt @@ -214,8 +214,37 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) { ApiResult.Error(e.message ?: "Unknown error occurred") } } + + // PDF Report Generation + suspend fun generateTasksReport(token: String, residenceId: Int, email: String? = null): ApiResult { + 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 +) + @kotlinx.serialization.Serializable data class PaginatedResponse( val count: Int, diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt index b39e892..3a2787a 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/ui/screens/ResidenceDetailScreen.kt @@ -46,11 +46,14 @@ fun ResidenceDetailScreen( val taskAddNewTaskState by taskViewModel.taskAddNewCustomTaskState.collectAsState() val cancelTaskState by residenceViewModel.cancelTaskState.collectAsState() val uncancelTaskState by residenceViewModel.uncancelTaskState.collectAsState() + val generateReportState by residenceViewModel.generateReportState.collectAsState() var showCompleteDialog by remember { mutableStateOf(false) } var selectedTask by remember { mutableStateOf(null) } var showNewTaskDialog by remember { mutableStateOf(false) } var showManageUsersDialog by remember { mutableStateOf(false) } + var showReportSnackbar by remember { mutableStateOf(false) } + var reportMessage by remember { mutableStateOf("") } LaunchedEffect(residenceId) { residenceViewModel.getResidence(residenceId) { result -> @@ -105,6 +108,24 @@ fun ResidenceDetailScreen( } } + // Handle generate report state + LaunchedEffect(generateReportState) { + when (generateReportState) { + is ApiResult.Success -> { + val response = (generateReportState as ApiResult.Success).data + reportMessage = response.message + showReportSnackbar = true + residenceViewModel.resetGenerateReportState() + } + is ApiResult.Error -> { + reportMessage = (generateReportState as ApiResult.Error).message + showReportSnackbar = true + residenceViewModel.resetGenerateReportState() + } + else -> {} + } + } + if (showCompleteDialog && selectedTask != null) { CompleteTaskDialog( taskId = selectedTask!!.id, @@ -155,7 +176,17 @@ fun ResidenceDetailScreen( ) } + val snackbarHostState = remember { SnackbarHostState() } + + LaunchedEffect(showReportSnackbar) { + if (showReportSnackbar) { + snackbarHostState.showSnackbar(reportMessage) + showReportSnackbar = false + } + } + Scaffold( + snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, topBar = { TopAppBar( title = { Text("Property Details", fontWeight = FontWeight.Bold) }, @@ -169,6 +200,23 @@ fun ResidenceDetailScreen( if (residenceState is ApiResult.Success) { val residence = (residenceState as ApiResult.Success).data + // Generate Report button + IconButton( + onClick = { + residenceViewModel.generateTasksReport(residenceId) + }, + enabled = generateReportState !is ApiResult.Loading + ) { + if (generateReportState is ApiResult.Loading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp + ) + } else { + Icon(Icons.Default.Description, contentDescription = "Generate Report") + } + } + // Manage Users button - only show for primary owners if (residence.isPrimaryOwner) { IconButton(onClick = { diff --git a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt index 2fc5e94..a56c634 100644 --- a/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/example/mycrib/viewmodel/ResidenceViewModel.kt @@ -46,6 +46,9 @@ class ResidenceViewModel : ViewModel() { private val _updateTaskState = MutableStateFlow>(ApiResult.Idle) val updateTaskState: StateFlow> = _updateTaskState + private val _generateReportState = MutableStateFlow>(ApiResult.Idle) + val generateReportState: StateFlow> = _generateReportState + fun loadResidences() { viewModelScope.launch { _residencesState.value = ApiResult.Loading @@ -189,4 +192,20 @@ class ResidenceViewModel : ViewModel() { fun resetUpdateTaskState() { _updateTaskState.value = ApiResult.Idle } + + fun generateTasksReport(residenceId: Int, email: String? = null) { + viewModelScope.launch { + _generateReportState.value = ApiResult.Loading + val token = TokenStorage.getToken() + if (token != null) { + _generateReportState.value = residenceApi.generateTasksReport(token, residenceId, email) + } else { + _generateReportState.value = ApiResult.Error("Not authenticated", 401) + } + } + } + + fun resetGenerateReportState() { + _generateReportState.value = ApiResult.Idle + } } diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 209ca8b..2d56aed 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -15,6 +15,7 @@ struct ResidenceDetailView: View { @State private var selectedTaskForEdit: TaskDetail? @State private var selectedTaskForComplete: TaskDetail? @State private var hasAppeared = false + @State private var showReportAlert = false var body: some View { ZStack { @@ -105,6 +106,20 @@ struct ResidenceDetailView: View { } ToolbarItemGroup(placement: .navigationBarTrailing) { + // Generate Report button + if viewModel.selectedResidence != nil { + Button(action: { + viewModel.generateTasksReport(residenceId: residenceId) + }) { + if viewModel.isGeneratingReport { + ProgressView() + } else { + Image(systemName: "doc.text") + } + } + .disabled(viewModel.isGeneratingReport) + } + // Manage Users button - only show for primary owners if let residence = viewModel.selectedResidence, residence.isPrimaryOwner { Button(action: { @@ -164,6 +179,16 @@ struct ResidenceDetailView: View { loadResidenceTasks() } } + .onChange(of: viewModel.reportMessage) { message in + if message != nil { + showReportAlert = true + } + } + .alert("Maintenance Report", isPresented: $showReportAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(viewModel.reportMessage ?? "") + } .onAppear { loadResidenceData() } diff --git a/iosApp/iosApp/Residence/ResidenceViewModel.swift b/iosApp/iosApp/Residence/ResidenceViewModel.swift index 6421fcf..2eee737 100644 --- a/iosApp/iosApp/Residence/ResidenceViewModel.swift +++ b/iosApp/iosApp/Residence/ResidenceViewModel.swift @@ -10,6 +10,8 @@ class ResidenceViewModel: ObservableObject { @Published var selectedResidence: Residence? @Published var isLoading: Bool = false @Published var errorMessage: String? + @Published var isGeneratingReport: Bool = false + @Published var reportMessage: String? // MARK: - Private Properties private let residenceApi: ResidenceApi @@ -144,6 +146,31 @@ class ResidenceViewModel: ObservableObject { } } + func generateTasksReport(residenceId: Int32, email: String? = nil) { + guard let token = tokenStorage.getToken() else { + reportMessage = "Not authenticated" + return + } + + isGeneratingReport = true + reportMessage = nil + + residenceApi.generateTasksReport(token: token, residenceId: residenceId, email: email) { result, error in + defer { self.isGeneratingReport = false } + if let successResult = result as? ApiResultSuccess { + if let response = successResult.data { + self.reportMessage = response.message + } else { + self.reportMessage = "Report generated, but no message returned." + } + } else if let errorResult = result as? ApiResultError { + self.reportMessage = errorResult.message + } else if let error = error { + self.reportMessage = error.localizedDescription + } + } + } + func clearError() { errorMessage = nil }