Fix task stats consistency and improve residence card UI
- Compute task stats locally from kanban data for both summary card and residence cards - Filter out completed_tasks and cancelled_tasks columns from counts - Use startOfDay for accurate date comparisons (overdue, due this week, next 30 days) - Add parseDate helper to DateUtils - Make address tappable to open in Apple Maps - Remove navigation title from residences list - Update CLAUDE.md with Go backend references and DataManager architecture 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -24,8 +24,8 @@ struct ResidencesListView: View {
|
||||
errorMessage: viewModel.errorMessage,
|
||||
content: { residences in
|
||||
ResidencesContent(
|
||||
summary: viewModel.totalSummary ?? TotalSummary(totalResidences: Int32(residences.count), totalTasks: 0, totalPending: 0, totalOverdue: 0, tasksDueNextWeek: 0, tasksDueNextMonth: 0),
|
||||
residences: residences
|
||||
residences: residences,
|
||||
tasksResponse: taskViewModel.tasksResponse
|
||||
)
|
||||
},
|
||||
emptyContent: {
|
||||
@@ -46,7 +46,6 @@ struct ResidencesListView: View {
|
||||
})
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.Residences.title)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
@@ -171,49 +170,132 @@ private struct OrganicToolbarButton: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Residence Task Stats
|
||||
struct ResidenceTaskStats {
|
||||
let totalCount: Int
|
||||
let overdueCount: Int
|
||||
let dueThisWeekCount: Int
|
||||
let dueNext30DaysCount: Int
|
||||
}
|
||||
|
||||
// MARK: - Residences Content View
|
||||
|
||||
private struct ResidencesContent: View {
|
||||
let summary: TotalSummary
|
||||
let residences: [ResidenceResponse]
|
||||
let tasksResponse: TaskColumnsResponse?
|
||||
|
||||
/// Extract active tasks - skip completed_tasks and cancelled_tasks columns
|
||||
private var activeTasks: [TaskResponse] {
|
||||
guard let response = tasksResponse else { return [] }
|
||||
|
||||
var tasks: [TaskResponse] = []
|
||||
for column in response.columns {
|
||||
// Skip completed and cancelled columns (cancelled includes archived)
|
||||
let columnName = column.name.lowercased()
|
||||
if columnName == "completed_tasks" || columnName == "cancelled_tasks" {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add tasks from this column
|
||||
for task in column.tasks {
|
||||
tasks.append(task)
|
||||
}
|
||||
}
|
||||
return tasks
|
||||
}
|
||||
|
||||
/// Compute total summary from task data using date logic
|
||||
private var computedSummary: TotalSummary {
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let in7Days = calendar.date(byAdding: .day, value: 7, to: today) ?? today
|
||||
let in30Days = calendar.date(byAdding: .day, value: 30, to: today) ?? today
|
||||
|
||||
var overdueCount: Int32 = 0
|
||||
var dueThisWeekCount: Int32 = 0
|
||||
var dueNext30DaysCount: Int32 = 0
|
||||
|
||||
for task in activeTasks {
|
||||
guard let dueDateStr = task.effectiveDueDate,
|
||||
let dueDate = DateUtils.parseDate(dueDateStr) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let taskDate = calendar.startOfDay(for: dueDate)
|
||||
|
||||
if taskDate < today {
|
||||
overdueCount += 1
|
||||
} else if taskDate <= in7Days {
|
||||
dueThisWeekCount += 1
|
||||
} else if taskDate <= in30Days {
|
||||
dueNext30DaysCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return TotalSummary(
|
||||
totalResidences: Int32(residences.count),
|
||||
totalTasks: Int32(activeTasks.count),
|
||||
totalPending: 0,
|
||||
totalOverdue: overdueCount,
|
||||
tasksDueNextWeek: dueThisWeekCount,
|
||||
tasksDueNextMonth: dueNext30DaysCount
|
||||
)
|
||||
}
|
||||
|
||||
/// Get task stats for a specific residence
|
||||
private func taskStats(for residenceId: Int32) -> ResidenceTaskStats {
|
||||
let residenceTasks = activeTasks.filter { $0.residenceId == residenceId }
|
||||
let calendar = Calendar.current
|
||||
let today = calendar.startOfDay(for: Date())
|
||||
let in7Days = calendar.date(byAdding: .day, value: 7, to: today) ?? today
|
||||
let in30Days = calendar.date(byAdding: .day, value: 30, to: today) ?? today
|
||||
|
||||
var overdueCount = 0
|
||||
var dueThisWeekCount = 0
|
||||
var dueNext30DaysCount = 0
|
||||
|
||||
for task in residenceTasks {
|
||||
guard let dueDateStr = task.effectiveDueDate,
|
||||
let dueDate = DateUtils.parseDate(dueDateStr) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let taskDate = calendar.startOfDay(for: dueDate)
|
||||
|
||||
if taskDate < today {
|
||||
overdueCount += 1
|
||||
} else if taskDate <= in7Days {
|
||||
dueThisWeekCount += 1
|
||||
} else if taskDate <= in30Days {
|
||||
dueNext30DaysCount += 1
|
||||
}
|
||||
}
|
||||
|
||||
return ResidenceTaskStats(
|
||||
totalCount: residenceTasks.count,
|
||||
overdueCount: overdueCount,
|
||||
dueThisWeekCount: dueThisWeekCount,
|
||||
dueNext30DaysCount: dueNext30DaysCount
|
||||
)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
// Summary Card with enhanced styling
|
||||
SummaryCard(summary: summary)
|
||||
SummaryCard(summary: computedSummary)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Properties Section Header
|
||||
HStack(alignment: .center) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(L10n.Residences.yourProperties)
|
||||
.font(.system(size: 20, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text("\(residences.count) \(residences.count == 1 ? L10n.Residences.property : L10n.Residences.properties)")
|
||||
.font(.system(size: 13, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
// Decorative leaf
|
||||
Image(systemName: "leaf.fill")
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
.foregroundColor(Color.appPrimary.opacity(0.3))
|
||||
.rotationEffect(.degrees(-15))
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
|
||||
// Residences List with staggered animation
|
||||
LazyVStack(spacing: 16) {
|
||||
ForEach(Array(residences.enumerated()), id: \.element.id) { index, residence in
|
||||
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
|
||||
ResidenceCard(residence: residence)
|
||||
.padding(.horizontal, 16)
|
||||
ResidenceCard(
|
||||
residence: residence,
|
||||
taskStats: taskStats(for: residence.id)
|
||||
)
|
||||
.padding(.horizontal, 16)
|
||||
}
|
||||
.buttonStyle(OrganicCardButtonStyle())
|
||||
.transition(.asymmetric(
|
||||
|
||||
Reference in New Issue
Block a user