Add PDF maintenance report generation feature for Android and iOS
- Add generateTasksReport API endpoint in ResidenceApi - Implement report generation button in Android residence detail screen - Add report generation state management in shared ResidenceViewModel - Add report generation button to iOS residence detail view toolbar - Implement iOS-specific report generation logic in ResidenceViewModel - Display loading spinner and success/error alerts for report generation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -214,8 +214,37 @@ class ResidenceApi(private val client: HttpClient = ApiClient.httpClient) {
|
|||||||
ApiResult.Error(e.message ?: "Unknown error occurred")
|
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
|
||||||
|
)
|
||||||
|
|
||||||
@kotlinx.serialization.Serializable
|
@kotlinx.serialization.Serializable
|
||||||
data class PaginatedResponse<T>(
|
data class PaginatedResponse<T>(
|
||||||
val count: Int,
|
val count: Int,
|
||||||
|
|||||||
@@ -46,11 +46,14 @@ fun ResidenceDetailScreen(
|
|||||||
val taskAddNewTaskState by taskViewModel.taskAddNewCustomTaskState.collectAsState()
|
val taskAddNewTaskState by taskViewModel.taskAddNewCustomTaskState.collectAsState()
|
||||||
val cancelTaskState by residenceViewModel.cancelTaskState.collectAsState()
|
val cancelTaskState by residenceViewModel.cancelTaskState.collectAsState()
|
||||||
val uncancelTaskState by residenceViewModel.uncancelTaskState.collectAsState()
|
val uncancelTaskState by residenceViewModel.uncancelTaskState.collectAsState()
|
||||||
|
val generateReportState by residenceViewModel.generateReportState.collectAsState()
|
||||||
|
|
||||||
var showCompleteDialog by remember { mutableStateOf(false) }
|
var showCompleteDialog by remember { mutableStateOf(false) }
|
||||||
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
var selectedTask by remember { mutableStateOf<TaskDetail?>(null) }
|
||||||
var showNewTaskDialog by remember { mutableStateOf(false) }
|
var showNewTaskDialog by remember { mutableStateOf(false) }
|
||||||
var showManageUsersDialog by remember { mutableStateOf(false) }
|
var showManageUsersDialog by remember { mutableStateOf(false) }
|
||||||
|
var showReportSnackbar by remember { mutableStateOf(false) }
|
||||||
|
var reportMessage by remember { mutableStateOf("") }
|
||||||
|
|
||||||
LaunchedEffect(residenceId) {
|
LaunchedEffect(residenceId) {
|
||||||
residenceViewModel.getResidence(residenceId) { result ->
|
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) {
|
if (showCompleteDialog && selectedTask != null) {
|
||||||
CompleteTaskDialog(
|
CompleteTaskDialog(
|
||||||
taskId = selectedTask!!.id,
|
taskId = selectedTask!!.id,
|
||||||
@@ -155,7 +176,17 @@ fun ResidenceDetailScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
LaunchedEffect(showReportSnackbar) {
|
||||||
|
if (showReportSnackbar) {
|
||||||
|
snackbarHostState.showSnackbar(reportMessage)
|
||||||
|
showReportSnackbar = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("Property Details", fontWeight = FontWeight.Bold) },
|
title = { Text("Property Details", fontWeight = FontWeight.Bold) },
|
||||||
@@ -169,6 +200,23 @@ fun ResidenceDetailScreen(
|
|||||||
if (residenceState is ApiResult.Success) {
|
if (residenceState is ApiResult.Success) {
|
||||||
val residence = (residenceState as ApiResult.Success<Residence>).data
|
val residence = (residenceState as ApiResult.Success<Residence>).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
|
// Manage Users button - only show for primary owners
|
||||||
if (residence.isPrimaryOwner) {
|
if (residence.isPrimaryOwner) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
private val _updateTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.CustomTask>>(ApiResult.Idle)
|
private val _updateTaskState = MutableStateFlow<ApiResult<com.mycrib.shared.models.CustomTask>>(ApiResult.Idle)
|
||||||
val updateTaskState: StateFlow<ApiResult<com.mycrib.shared.models.CustomTask>> = _updateTaskState
|
val updateTaskState: StateFlow<ApiResult<com.mycrib.shared.models.CustomTask>> = _updateTaskState
|
||||||
|
|
||||||
|
private val _generateReportState = MutableStateFlow<ApiResult<com.mycrib.shared.network.GenerateReportResponse>>(ApiResult.Idle)
|
||||||
|
val generateReportState: StateFlow<ApiResult<com.mycrib.shared.network.GenerateReportResponse>> = _generateReportState
|
||||||
|
|
||||||
fun loadResidences() {
|
fun loadResidences() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_residencesState.value = ApiResult.Loading
|
_residencesState.value = ApiResult.Loading
|
||||||
@@ -189,4 +192,20 @@ class ResidenceViewModel : ViewModel() {
|
|||||||
fun resetUpdateTaskState() {
|
fun resetUpdateTaskState() {
|
||||||
_updateTaskState.value = ApiResult.Idle
|
_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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ struct ResidenceDetailView: View {
|
|||||||
@State private var selectedTaskForEdit: TaskDetail?
|
@State private var selectedTaskForEdit: TaskDetail?
|
||||||
@State private var selectedTaskForComplete: TaskDetail?
|
@State private var selectedTaskForComplete: TaskDetail?
|
||||||
@State private var hasAppeared = false
|
@State private var hasAppeared = false
|
||||||
|
@State private var showReportAlert = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -105,6 +106,20 @@ struct ResidenceDetailView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
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
|
// Manage Users button - only show for primary owners
|
||||||
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
|
if let residence = viewModel.selectedResidence, residence.isPrimaryOwner {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -164,6 +179,16 @@ struct ResidenceDetailView: View {
|
|||||||
loadResidenceTasks()
|
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 {
|
.onAppear {
|
||||||
loadResidenceData()
|
loadResidenceData()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ class ResidenceViewModel: ObservableObject {
|
|||||||
@Published var selectedResidence: Residence?
|
@Published var selectedResidence: Residence?
|
||||||
@Published var isLoading: Bool = false
|
@Published var isLoading: Bool = false
|
||||||
@Published var errorMessage: String?
|
@Published var errorMessage: String?
|
||||||
|
@Published var isGeneratingReport: Bool = false
|
||||||
|
@Published var reportMessage: String?
|
||||||
|
|
||||||
// MARK: - Private Properties
|
// MARK: - Private Properties
|
||||||
private let residenceApi: ResidenceApi
|
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<GenerateReportResponse> {
|
||||||
|
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() {
|
func clearError() {
|
||||||
errorMessage = nil
|
errorMessage = nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user