This commit is contained in:
Trey t
2025-11-05 15:15:59 -06:00
parent 5deac95818
commit 1d48a9bff1
13 changed files with 1360 additions and 871 deletions

View File

@@ -4,11 +4,15 @@ import ComposeApp
struct ResidenceDetailView: View {
let residenceId: Int32
@StateObject private var viewModel = ResidenceViewModel()
@State private var residenceWithTasks: ResidenceWithTasks?
@StateObject private var taskViewModel = TaskViewModel()
@State private var tasksResponse: TasksByResidenceResponse?
@State private var isLoadingTasks = false
@State private var tasksError: String?
@State private var showAddTask = false
@State private var showEditResidence = false
@State private var showEditTask = false
@State private var selectedTaskForEdit: TaskDetail?
@State private var showCancelledTasks = false
var body: some View {
ZStack {
@@ -30,9 +34,26 @@ struct ResidenceDetailView: View {
.padding(.top)
// Tasks Section
if let residenceWithTasks = residenceWithTasks {
TasksSection(residenceWithTasks: residenceWithTasks)
.padding(.horizontal)
if let tasksResponse = tasksResponse {
TasksSection(
tasksResponse: tasksResponse,
showCancelledTasks: $showCancelledTasks,
onEditTask: { task in
selectedTaskForEdit = task
showEditTask = true
},
onCancelTask: { task in
taskViewModel.cancelTask(id: task.id) { _ in
loadResidenceTasks()
}
},
onUncancelTask: { task in
taskViewModel.uncancelTask(id: task.id) { _ in
loadResidenceTasks()
}
}
)
.padding(.horizontal)
} else if isLoadingTasks {
ProgressView("Loading tasks...")
} else if let tasksError = tasksError {
@@ -74,18 +95,26 @@ struct ResidenceDetailView: View {
EditResidenceView(residence: residence, isPresented: $showEditResidence)
}
}
.sheet(isPresented: $showEditTask) {
if let task = selectedTaskForEdit {
EditTaskView(task: task, isPresented: $showEditTask)
}
}
.onChange(of: showAddTask) { isShowing in
if !isShowing {
// Refresh tasks when sheet is dismissed
loadResidenceWithTasks()
loadResidenceTasks()
}
}
.onChange(of: showEditResidence) { isShowing in
if !isShowing {
// Refresh residence data when edit sheet is dismissed
loadResidenceData()
}
}
.onChange(of: showEditTask) { isShowing in
if !isShowing {
loadResidenceTasks()
}
}
.onAppear {
loadResidenceData()
}
@@ -93,21 +122,19 @@ struct ResidenceDetailView: View {
private func loadResidenceData() {
viewModel.getResidence(id: residenceId)
loadResidenceWithTasks()
loadResidenceTasks()
}
private func loadResidenceWithTasks() {
private func loadResidenceTasks() {
guard let token = TokenStorage().getToken() else { return }
isLoadingTasks = true
tasksError = nil
let residenceApi = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
residenceApi.getMyResidences(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<MyResidencesResponse> {
if let residence = successResult.data?.residences.first(where: { $0.id == residenceId }) {
self.residenceWithTasks = residence
}
let taskApi = TaskApi(client: ApiClient_iosKt.createHttpClient())
taskApi.getTasksByResidence(token: token, residenceId: residenceId) { result, error in
if let successResult = result as? ApiResultSuccess<TasksByResidenceResponse> {
self.tasksResponse = successResult.data
self.isLoadingTasks = false
} else if let errorResult = result as? ApiResultError {
self.tasksError = errorResult.message
@@ -175,14 +202,6 @@ struct PropertyHeaderCard: View {
}
}
}
// if !residence.description.isEmpty {
// Divider()
//
// Text(residence.)
// .font(.body)
// .foregroundColor(.secondary)
// }
}
.padding(20)
.background(Color.blue.opacity(0.1))
@@ -213,7 +232,11 @@ struct PropertyDetailItem: View {
}
struct TasksSection: View {
let residenceWithTasks: ResidenceWithTasks
let tasksResponse: TasksByResidenceResponse
@Binding var showCancelledTasks: Bool
let onEditTask: (TaskDetail) -> Void
let onCancelTask: (TaskDetail) -> Void
let onUncancelTask: (TaskDetail) -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -226,17 +249,53 @@ struct TasksSection: View {
// Task Summary Pills
HStack(spacing: 8) {
TaskPill(count: residenceWithTasks.taskSummary.total, label: "Total", color: .blue)
TaskPill(count: residenceWithTasks.taskSummary.pending, label: "Pending", color: .orange)
TaskPill(count: residenceWithTasks.taskSummary.completed, label: "Done", color: .green)
TaskPill(count: tasksResponse.summary.total, label: "Total", color: .blue)
TaskPill(count: tasksResponse.summary.pending, label: "Pending", color: .orange)
TaskPill(count: tasksResponse.summary.completed, label: "Done", color: .green)
}
}
if residenceWithTasks.tasks.isEmpty {
// Active Tasks
if tasksResponse.tasks.isEmpty && tasksResponse.cancelledTasks.isEmpty {
EmptyTasksView()
} else {
ForEach(residenceWithTasks.tasks, id: \.id) { task in
TaskCard(task: task)
ForEach(tasksResponse.tasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: { onCancelTask(task) },
onUncancel: nil
)
}
// Cancelled Tasks Section
if !tasksResponse.cancelledTasks.isEmpty {
VStack(alignment: .leading, spacing: 12) {
HStack {
Label("Cancelled Tasks (\(tasksResponse.cancelledTasks.count))", systemImage: "xmark.circle")
.font(.headline)
.foregroundColor(.red)
Spacer()
Button(showCancelledTasks ? "Hide" : "Show") {
showCancelledTasks.toggle()
}
.font(.subheadline)
}
.padding(.top, 8)
if showCancelledTasks {
ForEach(tasksResponse.cancelledTasks, id: \.id) { task in
TaskCard(
task: task,
onEdit: { onEditTask(task) },
onCancel: nil,
onUncancel: { onUncancelTask(task) }
)
}
}
}
}
}
}
@@ -267,6 +326,9 @@ struct TaskPill: View {
struct TaskCard: View {
let task: TaskDetail
let onEdit: () -> Void
let onCancel: (() -> Void)?
let onUncancel: (() -> Void)?
var body: some View {
VStack(alignment: .leading, spacing: 12) {
@@ -283,7 +345,7 @@ struct TaskCard: View {
Spacer()
PriorityBadge(priority: task.priority.name)
PriorityBadge(priority: task.priority.name)
}
if let description = task.description_, !description.isEmpty {
@@ -294,17 +356,15 @@ struct TaskCard: View {
}
HStack {
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Label(task.frequency.displayName, systemImage: "repeat")
.font(.caption)
.foregroundColor(.secondary)
Spacer()
Label(formatDate(task.dueDate), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
Label(formatDate(task.dueDate), systemImage: "calendar")
.font(.caption)
.foregroundColor(.secondary)
}
// Completion count
@@ -318,69 +378,49 @@ struct TaskCard: View {
.font(.caption)
.foregroundColor(.secondary)
}
ForEach(task.completions, id: \.id) { completion in
Spacer().frame(height: 12)
// Card equivalent
VStack(alignment: .leading, spacing: 8) {
// Top row: date + rating badge
HStack {
Text(completion.completionDate.components(separatedBy: "T").first ?? "")
.font(.body.weight(.bold))
.foregroundColor(.accentColor)
Spacer()
if let rating = completion.rating {
Text("\(rating)")
.font(.caption.weight(.bold))
.padding(.horizontal, 8)
.padding(.vertical, 4)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color(.tertiarySystemFill))
)
}
}
// Completed by
if let name = completion.completedByName {
Text("By: \(name)")
.font(.subheadline.weight(.medium))
.padding(.top, 8)
}
// Cost
if let cost = completion.actualCost {
Text("Cost: $\(cost)")
.font(.subheadline.weight(.medium))
.foregroundColor(.teal) // tertiary equivalent
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(16)
.background(
RoundedRectangle(cornerRadius: 12)
.fill(Color.secondary.opacity(0.15)) // surfaceVariant equivalent
)
}
}
if task.showCompletedButton {
Button(action: {}) {
HStack {
Image(systemName: "checkmark.circle.fill") // SF Symbol
Image(systemName: "checkmark.circle.fill")
.resizable()
.frame(width: 20, height: 20)
Spacer().frame(width: 8)
Text("Complete Task")
.font(.title3.weight(.semibold)) // Material titleSmall + SemiBold
.font(.title3.weight(.semibold))
}
.frame(maxWidth: .infinity, alignment: .center)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.cornerRadius(12)
}
// Action Buttons
HStack(spacing: 8) {
Button(action: onEdit) {
Label("Edit", systemImage: "pencil")
.font(.subheadline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
if let onCancel = onCancel {
Button(action: onCancel) {
Label("Cancel", systemImage: "xmark.circle")
.font(.subheadline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.bordered)
.tint(.red)
} else if let onUncancel = onUncancel {
Button(action: onUncancel) {
Label("Restore", systemImage: "arrow.uturn.backward")
.font(.subheadline)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.tint(.blue)
}
.buttonStyle(.borderedProminent) // gives filled look
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
.padding(16)