Replace the separate ChargedDeviceSessionsView with inline session list in the Sessions tab. Sessions are now directly accessible from the tab without an extra navigation step, while maintaining the ability to drill into session details. - Refactored sessionsTab to display summary + inline list - Added sessionListCard for summary + session list layout - Added sessionListItem for individual session display with navigation - Removed sessionHistorySummaryCard and "Manage Sessions" button
@@ -145,8 +145,9 @@ struct ChargedDeviceDetailView: View {
|
||
| 145 | 145 |
activeSessionSummaryCard(activeSession, chargedDevice: chargedDevice) |
| 146 | 146 |
} |
| 147 | 147 |
|
| 148 |
- if !closedSessions(for: chargedDevice).isEmpty {
|
|
| 149 |
- sessionHistorySummaryCard(chargedDevice) |
|
| 148 |
+ let sessions = closedSessions(for: chargedDevice) |
|
| 149 |
+ if !sessions.isEmpty {
|
|
| 150 |
+ sessionListCard(sessions, chargedDevice: chargedDevice) |
|
| 150 | 151 |
} else if chargedDevice.activeSession == nil {
|
| 151 | 152 |
emptyStateCard( |
| 152 | 153 |
title: "No Sessions", |
@@ -615,31 +616,92 @@ struct ChargedDeviceDetailView: View {
|
||
| 615 | 616 |
.meterCard(tint: .blue, fillOpacity: 0.14, strokeOpacity: 0.20, cornerRadius: 18) |
| 616 | 617 |
} |
| 617 | 618 |
|
| 618 |
- private func sessionHistorySummaryCard(_ chargedDevice: ChargedDeviceSummary) -> some View {
|
|
| 619 |
- let sessions = closedSessions(for: chargedDevice) |
|
| 620 |
- let latestSession = sessions.first |
|
| 619 |
+ private func sessionListCard( |
|
| 620 |
+ _ sessions: [ChargeSessionSummary], |
|
| 621 |
+ chargedDevice: ChargedDeviceSummary |
|
| 622 |
+ ) -> some View {
|
|
| 621 | 623 |
let totalEnergyWh = sessions.reduce(0) { $0 + $1.effectiveOrMeasuredEnergyWh }
|
| 624 |
+ let completedCount = sessions.filter { $0.status == .completed }.count
|
|
| 622 | 625 |
|
| 623 |
- return MeterInfoCardView(title: "Session History", tint: .teal) {
|
|
| 624 |
- MeterInfoRowView(label: "Closed Sessions", value: "\(sessions.count)") |
|
| 625 |
- MeterInfoRowView(label: "Total Energy", value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh") |
|
| 626 |
- if let latestSession {
|
|
| 627 |
- MeterInfoRowView(label: "Latest", value: latestSession.startedAt.format()) |
|
| 626 |
+ return VStack(alignment: .leading, spacing: 14) {
|
|
| 627 |
+ MeterInfoCardView(title: "Closed Sessions", tint: .teal) {
|
|
| 628 |
+ MeterInfoRowView(label: "Sessions", value: "\(sessions.count)") |
|
| 629 |
+ MeterInfoRowView(label: "Completed", value: "\(completedCount)") |
|
| 630 |
+ MeterInfoRowView(label: "Total Energy", value: "\(totalEnergyWh.format(decimalDigits: 2)) Wh") |
|
| 628 | 631 |
} |
| 629 | 632 |
|
| 630 |
- NavigationLink( |
|
| 631 |
- destination: ChargedDeviceSessionsView(chargedDeviceID: chargedDevice.id) |
|
| 632 |
- ) {
|
|
| 633 |
- Label("Manage Sessions", systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
|
|
| 634 |
- .font(.subheadline.weight(.semibold)) |
|
| 635 |
- .frame(maxWidth: .infinity) |
|
| 636 |
- .padding(.vertical, 10) |
|
| 637 |
- .meterCard(tint: .teal, fillOpacity: 0.16, strokeOpacity: 0.22, cornerRadius: 14) |
|
| 633 |
+ VStack(spacing: 10) {
|
|
| 634 |
+ ForEach(sessions.sorted { $0.startedAt > $1.startedAt }, id: \.id) { session in
|
|
| 635 |
+ sessionListItem(session, chargedDevice: chargedDevice) |
|
| 636 |
+ } |
|
| 638 | 637 |
} |
| 639 |
- .buttonStyle(.plain) |
|
| 640 | 638 |
} |
| 641 | 639 |
} |
| 642 | 640 |
|
| 641 |
+ private func sessionListItem( |
|
| 642 |
+ _ session: ChargeSessionSummary, |
|
| 643 |
+ chargedDevice: ChargedDeviceSummary |
|
| 644 |
+ ) -> some View {
|
|
| 645 |
+ let sessionTint = statusTint(for: session) |
|
| 646 |
+ |
|
| 647 |
+ return NavigationLink( |
|
| 648 |
+ destination: ChargeSessionDetailView( |
|
| 649 |
+ chargedDeviceID: chargedDevice.id, |
|
| 650 |
+ sessionID: session.id |
|
| 651 |
+ ) |
|
| 652 |
+ ) {
|
|
| 653 |
+ VStack(alignment: .leading, spacing: 10) {
|
|
| 654 |
+ HStack(alignment: .firstTextBaseline, spacing: 10) {
|
|
| 655 |
+ VStack(alignment: .leading, spacing: 2) {
|
|
| 656 |
+ Text(session.startedAt.format()) |
|
| 657 |
+ .font(.subheadline.weight(.semibold)) |
|
| 658 |
+ Text(session.status.title) |
|
| 659 |
+ .font(.caption2) |
|
| 660 |
+ .foregroundColor(sessionTint) |
|
| 661 |
+ } |
|
| 662 |
+ |
|
| 663 |
+ Spacer() |
|
| 664 |
+ |
|
| 665 |
+ VStack(alignment: .trailing, spacing: 2) {
|
|
| 666 |
+ Text("\(session.effectiveOrMeasuredEnergyWh.format(decimalDigits: 2)) Wh")
|
|
| 667 |
+ .font(.subheadline.weight(.semibold)) |
|
| 668 |
+ .foregroundColor(.primary) |
|
| 669 |
+ Text(sessionDurationText(session)) |
|
| 670 |
+ .font(.caption) |
|
| 671 |
+ .foregroundColor(.secondary) |
|
| 672 |
+ } |
|
| 673 |
+ } |
|
| 674 |
+ |
|
| 675 |
+ Divider() |
|
| 676 |
+ |
|
| 677 |
+ HStack(spacing: 8) {
|
|
| 678 |
+ if let batteryDelta = session.batteryDeltaPercent {
|
|
| 679 |
+ Label("\(batteryDelta >= 0 ? "+" : "")\(Int(batteryDelta.rounded()))% charged", systemImage: "battery.100percent")
|
|
| 680 |
+ .font(.caption2) |
|
| 681 |
+ .foregroundColor(.secondary) |
|
| 682 |
+ } |
|
| 683 |
+ |
|
| 684 |
+ if let capacityWh = session.capacityEstimateWh {
|
|
| 685 |
+ Text("est. \(capacityWh.format(decimalDigits: 1)) Wh")
|
|
| 686 |
+ .font(.caption2) |
|
| 687 |
+ .foregroundColor(.secondary) |
|
| 688 |
+ } |
|
| 689 |
+ |
|
| 690 |
+ Spacer() |
|
| 691 |
+ |
|
| 692 |
+ if !session.displayedAggregatedSamples.isEmpty {
|
|
| 693 |
+ Label("\(session.displayedAggregatedSamples.count) points", systemImage: "chart.xyaxis.line")
|
|
| 694 |
+ .font(.caption2) |
|
| 695 |
+ .foregroundColor(.secondary) |
|
| 696 |
+ } |
|
| 697 |
+ } |
|
| 698 |
+ } |
|
| 699 |
+ .padding(12) |
|
| 700 |
+ .meterCard(tint: sessionTint, fillOpacity: 0.08, strokeOpacity: 0.14, cornerRadius: 14) |
|
| 701 |
+ } |
|
| 702 |
+ .buttonStyle(.plain) |
|
| 703 |
+ } |
|
| 704 |
+ |
|
| 643 | 705 |
private func closedSessions(for chargedDevice: ChargedDeviceSummary) -> [ChargeSessionSummary] {
|
| 644 | 706 |
chargedDevice.sessions.filter { !$0.status.isOpen }
|
| 645 | 707 |
} |