231 lines
8.0 KiB
Swift
231 lines
8.0 KiB
Swift
// ── Reschedule Sheet ──────────────────────────────────────────
|
|
// NL-style reschedule options: "I slept in", "Push everything", "Skip next"
|
|
|
|
import SwiftUI
|
|
|
|
struct RescheduleSheet: View {
|
|
@EnvironmentObject var store: TimerStore
|
|
@Environment(\.dismiss) private var dismiss
|
|
@State private var customMinutes: Double = 30
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ZStack {
|
|
CMColors.bg.ignoresSafeArea()
|
|
|
|
ScrollView {
|
|
VStack(spacing: CMSpacing.xl) {
|
|
// Smart suggestions (if any)
|
|
if !store.rescheduleSuggestions.isEmpty {
|
|
smartSuggestionsSection
|
|
}
|
|
|
|
// Quick actions
|
|
quickActionsSection
|
|
|
|
// Custom shift
|
|
customShiftSection
|
|
|
|
// Skip next
|
|
skipSection
|
|
}
|
|
.padding(.horizontal, CMSpacing.lg)
|
|
.padding(.bottom, CMSpacing.xxl)
|
|
}
|
|
}
|
|
.navigationTitle("Reschedule")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbarBackground(CMColors.surface, for: .navigationBar)
|
|
.toolbarColorScheme(.dark, for: .navigationBar)
|
|
.toolbar {
|
|
ToolbarItem(placement: .cancellationAction) {
|
|
Button("Cancel") { dismiss() }
|
|
.foregroundStyle(CMColors.textSecondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Smart Suggestions
|
|
|
|
private var smartSuggestionsSection: some View {
|
|
VStack(alignment: .leading, spacing: CMSpacing.md) {
|
|
HStack(spacing: CMSpacing.sm) {
|
|
Image(systemName: "sparkles")
|
|
.foregroundStyle(CMColors.accent)
|
|
Text("SUGGESTED")
|
|
.font(CMFonts.body(size: 11, weight: .bold))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
.tracking(1.5)
|
|
}
|
|
|
|
ForEach(store.rescheduleSuggestions) { suggestion in
|
|
RescheduleOptionButton(
|
|
title: suggestion.title,
|
|
subtitle: suggestion.subtitle,
|
|
icon: suggestion.icon,
|
|
color: CMColors.accent
|
|
) {
|
|
applyAndDismiss(suggestion.action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Quick Actions
|
|
|
|
private var quickActionsSection: some View {
|
|
VStack(alignment: .leading, spacing: CMSpacing.md) {
|
|
Text("QUICK ACTIONS")
|
|
.font(CMFonts.body(size: 11, weight: .bold))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
.tracking(1.5)
|
|
|
|
ForEach(RescheduleSuggestion.quickActions) { action in
|
|
RescheduleOptionButton(
|
|
title: action.title,
|
|
subtitle: action.subtitle,
|
|
icon: action.icon,
|
|
color: CMColors.text
|
|
) {
|
|
applyAndDismiss(action.action)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Custom Shift
|
|
|
|
private var customShiftSection: some View {
|
|
VStack(alignment: .leading, spacing: CMSpacing.md) {
|
|
Text("CUSTOM")
|
|
.font(CMFonts.body(size: 11, weight: .bold))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
.tracking(1.5)
|
|
|
|
VStack(spacing: CMSpacing.md) {
|
|
HStack {
|
|
Text("Shift by")
|
|
.font(CMFonts.body(size: 14))
|
|
.foregroundStyle(CMColors.textSecondary)
|
|
Spacer()
|
|
Text("\(Int(customMinutes)) min")
|
|
.font(CMFonts.mono(size: 18, weight: .bold))
|
|
.foregroundStyle(CMColors.accent)
|
|
}
|
|
|
|
Slider(value: $customMinutes, in: 5...120, step: 5)
|
|
.tint(CMColors.accent)
|
|
|
|
HStack(spacing: CMSpacing.md) {
|
|
Button {
|
|
applyAndDismiss(.pushAll(interval: TimeInterval(customMinutes * 60)))
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "arrow.right")
|
|
Text("Push Later")
|
|
}
|
|
.font(CMFonts.body(size: 14, weight: .semibold))
|
|
.foregroundStyle(.white)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CMSpacing.md)
|
|
.background(CMColors.accent)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.sm))
|
|
}
|
|
|
|
Button {
|
|
applyAndDismiss(.pushAll(interval: TimeInterval(-customMinutes * 60)))
|
|
} label: {
|
|
HStack {
|
|
Image(systemName: "arrow.left")
|
|
Text("Pull Earlier")
|
|
}
|
|
.font(CMFonts.body(size: 14, weight: .semibold))
|
|
.foregroundStyle(CMColors.text)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, CMSpacing.md)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.sm))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CMRadius.sm)
|
|
.stroke(CMColors.border, lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.padding(CMSpacing.lg)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.md))
|
|
}
|
|
}
|
|
|
|
// MARK: - Skip
|
|
|
|
private var skipSection: some View {
|
|
VStack(alignment: .leading, spacing: CMSpacing.md) {
|
|
if let next = store.nextFiringTimer {
|
|
RescheduleOptionButton(
|
|
title: "Skip \"\(next.label)\"",
|
|
subtitle: "Dismiss your next timer and adjust remaining",
|
|
icon: "forward.fill",
|
|
color: CMColors.important
|
|
) {
|
|
applyAndDismiss(.skip(timerId: next.id))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func applyAndDismiss(_ action: RescheduleAction) {
|
|
HapticEngine.tap()
|
|
store.applyReschedule(action)
|
|
dismiss()
|
|
}
|
|
}
|
|
|
|
// MARK: - Option Button
|
|
|
|
struct RescheduleOptionButton: View {
|
|
let title: String
|
|
let subtitle: String
|
|
let icon: String
|
|
let color: Color
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
HStack(spacing: CMSpacing.md) {
|
|
Image(systemName: icon)
|
|
.font(.title3)
|
|
.foregroundStyle(color)
|
|
.frame(width: 32, height: 32)
|
|
|
|
VStack(alignment: .leading, spacing: CMSpacing.xxs) {
|
|
Text(title)
|
|
.font(CMFonts.body(size: 15, weight: .semibold))
|
|
.foregroundStyle(CMColors.text)
|
|
Text(subtitle)
|
|
.font(CMFonts.body(size: 12))
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "chevron.right")
|
|
.font(.caption)
|
|
.foregroundStyle(CMColors.textMuted)
|
|
}
|
|
.padding(CMSpacing.md)
|
|
.background(CMColors.surface)
|
|
.clipShape(RoundedRectangle(cornerRadius: CMRadius.sm))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: CMRadius.sm)
|
|
.stroke(CMColors.border, lineWidth: 1)
|
|
)
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|