wip
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user