Implement fully dynamic task summary UI from API data

Updated both iOS and Android to build residence task summary UI entirely
from API response data, with no hardcoded categories, icons, colors, or labels.

**Changes:**

**Backend Integration:**
- Updated TaskSummary model to use dynamic categories list instead of static fields
- Added TaskColumnCategory and TaskColumnIcon models for metadata
- Categories now include: name, displayName, icons (ios/android/web), color, count

**Android (ResidencesScreen.kt):**
- Removed hardcoded category extraction (overdue_tasks, current_tasks, in_progress_tasks)
- Now dynamically iterates over first 3 categories from API
- Added getIconForCategory() helper to map icon names to Material Icons
- Added parseHexColor() helper that works in commonMain (no Android-specific code)
- Uses category.displayName, category.icons.android, category.color from API

**iOS (ResidenceCard.swift):**
- Removed hardcoded category extraction and SF Symbol names
- Now dynamically iterates over first 3 categories using ForEach
- Uses category.displayName, category.icons.ios, category.color from API
- Leverages existing Color(hex:) extension for color parsing

**Component Organization:**
- Moved TaskSummaryCard.kt from commonMain to androidMain (uses Android-specific APIs)
- Created TaskSummaryCard.swift for iOS with dynamic category rendering

**Benefits:**
 Backend controls all category metadata (icons, colors, display names)
 Apps automatically reflect backend changes without redeployment
 No platform-specific hardcoded values
 Single source of truth in task/constants.py TASK_COLUMNS

**Files Changed:**
- Residence.kt: Added TaskColumnCategory, TaskColumnIcon models
- ResidencesScreen.kt: Dynamic category rendering with helpers
- ResidenceCard.swift: Dynamic category rendering with ForEach
- TaskSummaryCard.kt: Moved to androidMain
- TaskSummaryCard.swift: New iOS dynamic component

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-15 11:13:37 -06:00
parent 7f6f77d95a
commit bb5664c954
5 changed files with 406 additions and 46 deletions

View File

@@ -0,0 +1,168 @@
//
// TaskSummaryCard.swift
// iosApp
//
// Displays a dynamic task summary with categories from the backend.
// The backend provides icons, colors, and counts, making the UI fully data-driven.
//
import SwiftUI
import ComposeApp
/// Displays a task summary with dynamic categories from the backend
struct TaskSummaryCard: View {
let taskSummary: TaskSummary
var visibleCategories: [String]? = nil
private var filteredCategories: [TaskColumnCategory] {
if let visible = visibleCategories {
return taskSummary.categories.filter { visible.contains($0.name) }
}
return taskSummary.categories
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Tasks")
.font(.headline)
.fontWeight(.bold)
ForEach(filteredCategories, id: \.name) { category in
TaskCategoryRow(category: category)
}
}
.padding(16)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.1), radius: 4, x: 0, y: 2)
}
}
/// Displays a single task category with icon, name, and count
struct TaskCategoryRow: View {
let category: TaskColumnCategory
private var categoryColor: Color {
Color(hex: category.color) ?? .gray
}
var body: some View {
HStack(spacing: 12) {
// Icon with colored background
ZStack {
Circle()
.fill(categoryColor)
.frame(width: 32, height: 32)
Image(systemName: category.icons.ios)
.foregroundColor(.white)
.font(.system(size: 14, weight: .semibold))
}
// Category name
Text(category.displayName)
.font(.body)
.foregroundColor(.primary)
Spacer()
// Count badge
Text("\(category.count)")
.font(.subheadline)
.fontWeight(.bold)
.foregroundColor(.white)
.padding(.horizontal, 12)
.padding(.vertical, 4)
.background(categoryColor)
.cornerRadius(12)
}
.padding(12)
.background(categoryColor.opacity(0.1))
.cornerRadius(8)
}
}
// MARK: - Preview
#if DEBUG
struct TaskSummaryCard_Previews: PreviewProvider {
static var previews: some View {
VStack(spacing: 20) {
// Preview with all categories
TaskSummaryCard(taskSummary: mockTaskSummary)
.padding()
// Preview with filtered categories
TaskSummaryCard(
taskSummary: mockTaskSummary,
visibleCategories: ["overdue_tasks", "current_tasks", "in_progress_tasks"]
)
.padding()
}
.background(Color(.systemGroupedBackground))
}
static var mockTaskSummary: TaskSummary {
TaskSummary(
total: 25,
categories: [
TaskColumnCategory(
name: "overdue_tasks",
displayName: "Overdue",
icons: TaskColumnIcon(
ios: "exclamationmark.triangle",
android: "Warning",
web: "exclamation-triangle"
),
color: "#FF3B30",
count: 3
),
TaskColumnCategory(
name: "current_tasks",
displayName: "Current",
icons: TaskColumnIcon(
ios: "calendar",
android: "CalendarToday",
web: "calendar"
),
color: "#007AFF",
count: 8
),
TaskColumnCategory(
name: "in_progress_tasks",
displayName: "In Progress",
icons: TaskColumnIcon(
ios: "play.circle",
android: "PlayCircle",
web: "play-circle"
),
color: "#FF9500",
count: 2
),
TaskColumnCategory(
name: "backlog_tasks",
displayName: "Backlog",
icons: TaskColumnIcon(
ios: "tray",
android: "Inbox",
web: "inbox"
),
color: "#5856D6",
count: 7
),
TaskColumnCategory(
name: "done_tasks",
displayName: "Done",
icons: TaskColumnIcon(
ios: "checkmark.circle",
android: "CheckCircle",
web: "check-circle"
),
color: "#34C759",
count: 5
)
]
)
}
}
#endif

View File

@@ -71,28 +71,18 @@ struct ResidenceCard: View {
Divider()
// Task Stats
// Fully dynamic task stats from API - show first 3 categories
HStack(spacing: AppSpacing.sm) {
TaskStatChip(
icon: "list.bullet",
value: "\(residence.taskSummary.total)",
label: "Tasks",
color: .blue
)
let displayCategories = Array(residence.taskSummary.categories.prefix(3))
TaskStatChip(
icon: "checkmark.circle.fill",
value: "\(residence.taskSummary.completed)",
label: "Done",
color: .green
)
TaskStatChip(
icon: "clock.fill",
value: "\(residence.taskSummary.pending)",
label: "Pending",
color: .orange
)
ForEach(displayCategories, id: \.name) { category in
TaskStatChip(
icon: category.icons.ios,
value: "\(category.count)",
label: category.displayName,
color: Color(hex: category.color) ?? .gray
)
}
}
}
.padding(AppSpacing.md)
@@ -128,10 +118,36 @@ struct ResidenceCard: View {
isPrimary: true,
taskSummary: TaskSummary(
total: 10,
completed: 3,
pending: 5,
inProgress: 2,
overdue: 0
categories: [
TaskColumnCategory(
name: "overdue_tasks",
displayName: "Overdue",
icons: TaskColumnIcon(ios: "exclamationmark.triangle", android: "Warning", web: "exclamation-triangle"),
color: "#FF3B30",
count: 0
),
TaskColumnCategory(
name: "current_tasks",
displayName: "Current",
icons: TaskColumnIcon(ios: "calendar", android: "CalendarToday", web: "calendar"),
color: "#007AFF",
count: 5
),
TaskColumnCategory(
name: "in_progress_tasks",
displayName: "In Progress",
icons: TaskColumnIcon(ios: "play.circle", android: "PlayCircle", web: "play-circle"),
color: "#FF9500",
count: 2
),
TaskColumnCategory(
name: "done_tasks",
displayName: "Done",
icons: TaskColumnIcon(ios: "checkmark.circle", android: "CheckCircle", web: "check-circle"),
color: "#34C759",
count: 3
)
]
),
tasks: [],
createdAt: "2024-01-01T00:00:00Z",