diff --git a/iosApp/iosApp/Residence/ResidenceViewModel.swift b/iosApp/iosApp/Residence/ResidenceViewModel.swift index 89cd236..76f0ae1 100644 --- a/iosApp/iosApp/Residence/ResidenceViewModel.swift +++ b/iosApp/iosApp/Residence/ResidenceViewModel.swift @@ -136,7 +136,7 @@ class ResidenceViewModel: ObservableObject { } } - func getResidence(id: Int32) { + func getResidence(id: Int32, forceRefresh: Bool = false) { if UITestRuntime.shouldMockAuth { selectedResidence = Self.uiTestMockResidences.first(where: { $0.id == id }) isLoading = false @@ -149,7 +149,7 @@ class ResidenceViewModel: ObservableObject { Task { do { - let result = try await APILayer.shared.getResidence(id: id, forceRefresh: false) + let result = try await APILayer.shared.getResidence(id: id, forceRefresh: forceRefresh) if let success = result as? ApiResultSuccess { self.selectedResidence = success.data diff --git a/iosApp/iosApp/Shared/Components/HoneycombCompletionGrid.swift b/iosApp/iosApp/Shared/Components/HoneycombCompletionGrid.swift index b7e9eb4..e0813c2 100644 --- a/iosApp/iosApp/Shared/Components/HoneycombCompletionGrid.swift +++ b/iosApp/iosApp/Shared/Components/HoneycombCompletionGrid.swift @@ -10,9 +10,10 @@ struct HoneycombSummaryView: View { let summary: CompletionSummary let residenceName: String - private let maxRows = 10 - private let hexSize: CGFloat = 14 private let hexSpacing: CGFloat = 2 + private let colorAlpha: CGFloat = 0.3 + + private let rowCount = 12 var body: some View { VStack(spacing: OrganicSpacing.comfortable) { @@ -59,65 +60,46 @@ struct HoneycombSummaryView: View { // MARK: - Grid private var honeycombGrid: some View { - GeometryReader { geo in - let columns = summary.months.count - guard columns > 0 else { return AnyView(EmptyView()) } + VStack(spacing: hexSpacing) { + // Grid rows (top = row 12, bottom = row 1) + ForEach((0.. 0 { - Text("+\(month.overflow)") - .font(.system(size: 8, weight: .medium)) - .foregroundColor(Color.appTextSecondary) - .frame(width: hexWidth) - } else { - Color.clear - .frame(width: hexWidth, height: 10) - } + .stroke(hexColors[row].opacity(0.7), lineWidth: 1.5) + ) + .aspectRatio(1, contentMode: .fit) + } else { + HexagonShape() + .fill(Color.appTextSecondary.opacity(0.08)) + .aspectRatio(1, contentMode: .fit) } } } - ) - } - .frame(height: gridHeight) - } + } - private var gridHeight: CGFloat { - let hexHeight = hexSize * sqrt(3) - let rowHeight = hexHeight + hexSpacing - return rowHeight * CGFloat(maxRows) + 14 // +14 for overflow text + // Overflow indicators + HStack(spacing: hexSpacing) { + ForEach(Array(summary.months.enumerated()), id: \.offset) { _, month in + if month.overflow > 0 { + Text("+\(month.overflow)") + .font(.system(size: 8, weight: .medium)) + .foregroundColor(Color.appTextSecondary) + .frame(maxWidth: .infinity) + } else { + Color.clear + .frame(maxWidth: .infinity) + .frame(height: 10) + } + } + } + } } // MARK: - Month Labels @@ -156,11 +138,11 @@ struct HoneycombSummaryView: View { for column in fillOrder { if let entry = columnMap[column] { let color = Color(hex: entry.color) ?? Color.green - for _ in 0..= maxRows { break } + if colors.count >= rowCount { break } } return colors @@ -183,24 +165,28 @@ struct HoneycombSummaryView: View { // MARK: - Hexagon Shape -/// A regular hexagon shape (pointy-top orientation). +/// A regular hexagon shape (flat-top orientation). struct HexagonShape: Shape { func path(in rect: CGRect) -> Path { let w = rect.width let h = rect.height let cx = rect.midX let cy = rect.midY + // Use the smaller dimension to keep it regular + let r = min(w, h) / 2 var path = Path() - // Pointy-top hexagon - path.move(to: CGPoint(x: cx, y: cy - h / 2)) - path.addLine(to: CGPoint(x: cx + w / 2, y: cy - h / 4)) - path.addLine(to: CGPoint(x: cx + w / 2, y: cy + h / 4)) - path.addLine(to: CGPoint(x: cx, y: cy + h / 2)) - path.addLine(to: CGPoint(x: cx - w / 2, y: cy + h / 4)) - path.addLine(to: CGPoint(x: cx - w / 2, y: cy - h / 4)) + for i in 0..<6 { + let angle = CGFloat(i) * .pi / 3 - .pi / 6 // flat-top: start at -30° + let x = cx + r * cos(angle) + let y = cy + r * sin(angle) + if i == 0 { + path.move(to: CGPoint(x: x, y: y)) + } else { + path.addLine(to: CGPoint(x: x, y: y)) + } + } path.closeSubpath() return path } } -