- Remove TaskStatus model and status_id foreign key references - Add in_progress boolean field to task models and forms - Update TaskApi to use dedicated POST endpoints for task actions: - POST /tasks/:id/cancel/ instead of PATCH with is_cancelled - POST /tasks/:id/uncancel/ - POST /tasks/:id/archive/ - POST /tasks/:id/unarchive/ - Fix iOS TaskViewModel to use error-first pattern for Kotlin-Swift generic type bridging issues - Update iOS callback signatures to pass full TaskResponse instead of just taskId to avoid stale closure lookups - Add in_progress localization strings - Update widget preview data to use inProgress boolean 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
601 lines
26 KiB
Swift
601 lines
26 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
/// Screen 6: First task prompt with suggested templates - Content only (no navigation bar)
|
|
struct OnboardingFirstTaskContent: View {
|
|
var residenceName: String
|
|
var onTaskAdded: () -> Void
|
|
|
|
@StateObject private var viewModel = TaskViewModel()
|
|
@ObservedObject private var dataManager = DataManagerObservable.shared
|
|
@ObservedObject private var onboardingState = OnboardingState.shared
|
|
@State private var selectedTasks: Set<UUID> = []
|
|
@State private var isCreatingTasks = false
|
|
@State private var showCustomTaskSheet = false
|
|
@State private var expandedCategory: String? = nil
|
|
|
|
/// Maximum tasks allowed for free tier (matches API TierLimits)
|
|
private let maxTasksAllowed = 5
|
|
|
|
private let taskCategories: [OnboardingTaskCategory] = [
|
|
OnboardingTaskCategory(
|
|
name: "HVAC & Climate",
|
|
icon: "thermometer.medium",
|
|
color: Color.appPrimary,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "fanblades.fill", title: "Change HVAC Filter", category: "hvac", frequency: "monthly", color: Color.appPrimary),
|
|
OnboardingTaskTemplate(icon: "air.conditioner.horizontal.fill", title: "Schedule AC Tune-Up", category: "hvac", frequency: "yearly", color: Color.appPrimary),
|
|
OnboardingTaskTemplate(icon: "flame.fill", title: "Inspect Furnace", category: "hvac", frequency: "yearly", color: Color(hex: "#FF6B35") ?? .orange),
|
|
OnboardingTaskTemplate(icon: "wind", title: "Clean Air Ducts", category: "hvac", frequency: "yearly", color: Color.appSecondary)
|
|
]
|
|
),
|
|
OnboardingTaskCategory(
|
|
name: "Safety & Security",
|
|
icon: "shield.checkered",
|
|
color: Color.appError,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "smoke.fill", title: "Test Smoke Detectors", category: "safety", frequency: "monthly", color: Color.appError),
|
|
OnboardingTaskTemplate(icon: "dot.radiowaves.left.and.right", title: "Check CO Detectors", category: "safety", frequency: "monthly", color: Color.appError),
|
|
OnboardingTaskTemplate(icon: "flame.fill", title: "Inspect Fire Extinguisher", category: "safety", frequency: "yearly", color: Color(hex: "#FF6B35") ?? .orange),
|
|
OnboardingTaskTemplate(icon: "lock.fill", title: "Test Garage Door Safety", category: "safety", frequency: "monthly", color: Color.appSecondary)
|
|
]
|
|
),
|
|
OnboardingTaskCategory(
|
|
name: "Plumbing",
|
|
icon: "drop.fill",
|
|
color: Color.appSecondary,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "drop.fill", title: "Check for Leaks", category: "plumbing", frequency: "monthly", color: Color.appSecondary),
|
|
OnboardingTaskTemplate(icon: "bolt.horizontal.fill", title: "Flush Water Heater", category: "plumbing", frequency: "yearly", color: Color(hex: "#FF9500") ?? .orange),
|
|
OnboardingTaskTemplate(icon: "wrench.and.screwdriver.fill", title: "Clean Faucet Aerators", category: "plumbing", frequency: "quarterly", color: Color.appPrimary),
|
|
OnboardingTaskTemplate(icon: "arrow.down.circle.fill", title: "Snake Drains", category: "plumbing", frequency: "quarterly", color: Color.appTextSecondary)
|
|
]
|
|
),
|
|
OnboardingTaskCategory(
|
|
name: "Outdoor & Lawn",
|
|
icon: "leaf.fill",
|
|
color: Color(hex: "#34C759") ?? .green,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "leaf.fill", title: "Lawn Care", category: "landscaping", frequency: "weekly", color: Color(hex: "#34C759") ?? .green),
|
|
OnboardingTaskTemplate(icon: "cloud.rain.fill", title: "Clean Gutters", category: "exterior", frequency: "semiannually", color: Color.appSecondary),
|
|
OnboardingTaskTemplate(icon: "sun.max.fill", title: "Check Sprinkler System", category: "landscaping", frequency: "monthly", color: Color(hex: "#FF9500") ?? .orange),
|
|
OnboardingTaskTemplate(icon: "scissors", title: "Trim Trees & Shrubs", category: "landscaping", frequency: "quarterly", color: Color(hex: "#34C759") ?? .green)
|
|
]
|
|
),
|
|
OnboardingTaskCategory(
|
|
name: "Appliances",
|
|
icon: "refrigerator.fill",
|
|
color: Color.appAccent,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "refrigerator.fill", title: "Clean Refrigerator Coils", category: "appliances", frequency: "semiannually", color: Color.appAccent),
|
|
OnboardingTaskTemplate(icon: "washer.fill", title: "Clean Washing Machine", category: "appliances", frequency: "monthly", color: Color.appSecondary),
|
|
OnboardingTaskTemplate(icon: "dishwasher.fill", title: "Clean Dishwasher Filter", category: "appliances", frequency: "monthly", color: Color.appPrimary),
|
|
OnboardingTaskTemplate(icon: "oven.fill", title: "Deep Clean Oven", category: "appliances", frequency: "quarterly", color: Color(hex: "#FF6B35") ?? .orange)
|
|
]
|
|
),
|
|
OnboardingTaskCategory(
|
|
name: "General Home",
|
|
icon: "house.fill",
|
|
color: Color(hex: "#AF52DE") ?? .purple,
|
|
tasks: [
|
|
OnboardingTaskTemplate(icon: "paintbrush.fill", title: "Touch Up Paint", category: "interior", frequency: "yearly", color: Color(hex: "#AF52DE") ?? .purple),
|
|
OnboardingTaskTemplate(icon: "lightbulb.fill", title: "Replace Light Bulbs", category: "electrical", frequency: "monthly", color: Color.appAccent),
|
|
OnboardingTaskTemplate(icon: "door.left.hand.closed", title: "Lubricate Door Hinges", category: "interior", frequency: "yearly", color: Color.appTextSecondary),
|
|
OnboardingTaskTemplate(icon: "window.vertical.closed", title: "Clean Window Tracks", category: "interior", frequency: "semiannually", color: Color.appPrimary)
|
|
]
|
|
)
|
|
]
|
|
|
|
private var allTasks: [OnboardingTaskTemplate] {
|
|
taskCategories.flatMap { $0.tasks }
|
|
}
|
|
|
|
private var selectedCount: Int {
|
|
selectedTasks.count
|
|
}
|
|
|
|
private var isAtMaxSelection: Bool {
|
|
selectedTasks.count >= maxTasksAllowed
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
ScrollView {
|
|
VStack(spacing: AppSpacing.xl) {
|
|
// Header with celebration
|
|
VStack(spacing: AppSpacing.md) {
|
|
ZStack {
|
|
// Celebration circles
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [Color.appPrimary.opacity(0.2), Color.clear],
|
|
center: .center,
|
|
startRadius: 30,
|
|
endRadius: 80
|
|
)
|
|
)
|
|
.frame(width: 140, height: 140)
|
|
.offset(x: -15, y: -15)
|
|
|
|
Circle()
|
|
.fill(
|
|
RadialGradient(
|
|
colors: [Color.appAccent.opacity(0.2), Color.clear],
|
|
center: .center,
|
|
startRadius: 30,
|
|
endRadius: 80
|
|
)
|
|
)
|
|
.frame(width: 140, height: 140)
|
|
.offset(x: 15, y: 15)
|
|
|
|
// Party icon
|
|
ZStack {
|
|
Circle()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary, Color.appSecondary],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(width: 80, height: 80)
|
|
|
|
Image(systemName: "party.popper.fill")
|
|
.font(.system(size: 36))
|
|
.foregroundColor(.white)
|
|
}
|
|
.shadow(color: Color.appPrimary.opacity(0.4), radius: 15, y: 8)
|
|
}
|
|
|
|
Text("You're all set up!")
|
|
.font(.title)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
|
|
Text("Let's get you started with some tasks.\nThe more you pick, the more we'll help you remember!")
|
|
.font(.subheadline)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
.multilineTextAlignment(.center)
|
|
.lineSpacing(4)
|
|
}
|
|
.padding(.top, AppSpacing.lg)
|
|
|
|
// Selection counter chip
|
|
HStack(spacing: AppSpacing.sm) {
|
|
Image(systemName: isAtMaxSelection ? "checkmark.seal.fill" : "checkmark.circle.fill")
|
|
.foregroundColor(isAtMaxSelection ? Color.appAccent : Color.appPrimary)
|
|
|
|
Text("\(selectedCount)/\(maxTasksAllowed) tasks selected")
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(isAtMaxSelection ? Color.appAccent : Color.appPrimary)
|
|
}
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
.padding(.vertical, AppSpacing.sm)
|
|
.background((isAtMaxSelection ? Color.appAccent : Color.appPrimary).opacity(0.1))
|
|
.cornerRadius(AppRadius.xl)
|
|
.animation(.spring(response: 0.3), value: selectedCount)
|
|
|
|
// Task categories
|
|
VStack(spacing: AppSpacing.md) {
|
|
ForEach(taskCategories) { category in
|
|
TaskCategorySection(
|
|
category: category,
|
|
selectedTasks: $selectedTasks,
|
|
isExpanded: expandedCategory == category.name,
|
|
isAtMaxSelection: isAtMaxSelection,
|
|
onToggleExpand: {
|
|
withAnimation(.spring(response: 0.3)) {
|
|
if expandedCategory == category.name {
|
|
expandedCategory = nil
|
|
} else {
|
|
expandedCategory = category.name
|
|
}
|
|
}
|
|
}
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
|
|
// Quick add all popular
|
|
Button(action: selectPopularTasks) {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
Image(systemName: "sparkles")
|
|
.font(.headline)
|
|
|
|
Text("Add Most Popular")
|
|
.font(.headline)
|
|
.fontWeight(.medium)
|
|
}
|
|
.foregroundStyle(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary, Color.appAccent],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.background(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary.opacity(0.1), Color.appAccent.opacity(0.1)],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
)
|
|
)
|
|
.cornerRadius(AppRadius.lg)
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: AppRadius.lg)
|
|
.stroke(
|
|
LinearGradient(
|
|
colors: [Color.appPrimary.opacity(0.3), Color.appAccent.opacity(0.3)],
|
|
startPoint: .leading,
|
|
endPoint: .trailing
|
|
),
|
|
lineWidth: 1.5
|
|
)
|
|
)
|
|
}
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
}
|
|
.padding(.bottom, 140) // Space for button
|
|
}
|
|
|
|
// Bottom action area
|
|
VStack(spacing: AppSpacing.md) {
|
|
Button(action: addSelectedTasks) {
|
|
HStack(spacing: AppSpacing.sm) {
|
|
if isCreatingTasks {
|
|
ProgressView()
|
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
|
} else {
|
|
Text(selectedCount > 0 ? "Add \(selectedCount) Task\(selectedCount == 1 ? "" : "s") & Continue" : "Skip for Now")
|
|
.font(.headline)
|
|
.fontWeight(.bold)
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.frame(height: 56)
|
|
.foregroundColor(Color.appTextOnPrimary)
|
|
.background(
|
|
selectedCount > 0
|
|
? AnyShapeStyle(LinearGradient(colors: [Color.appPrimary, Color.appSecondary], startPoint: .leading, endPoint: .trailing))
|
|
: AnyShapeStyle(Color.appTextSecondary.opacity(0.5))
|
|
)
|
|
.cornerRadius(AppRadius.lg)
|
|
.shadow(color: selectedCount > 0 ? Color.appPrimary.opacity(0.4) : .clear, radius: 15, y: 8)
|
|
}
|
|
.disabled(isCreatingTasks)
|
|
.animation(.easeInOut(duration: 0.2), value: selectedCount)
|
|
}
|
|
.padding(.horizontal, AppSpacing.xl)
|
|
.padding(.bottom, AppSpacing.xxxl)
|
|
.background(
|
|
LinearGradient(
|
|
colors: [Color.appBackgroundPrimary.opacity(0), Color.appBackgroundPrimary],
|
|
startPoint: .top,
|
|
endPoint: .center
|
|
)
|
|
.frame(height: 60)
|
|
.offset(y: -60)
|
|
, alignment: .top
|
|
)
|
|
}
|
|
.background(Color.appBackgroundPrimary)
|
|
.onAppear {
|
|
// Expand first category by default
|
|
expandedCategory = taskCategories.first?.name
|
|
}
|
|
}
|
|
|
|
private func selectPopularTasks() {
|
|
// Select top popular tasks (up to max allowed)
|
|
let popularTaskTitles = [
|
|
"Change HVAC Filter",
|
|
"Test Smoke Detectors",
|
|
"Check for Leaks",
|
|
"Clean Gutters",
|
|
"Clean Refrigerator Coils"
|
|
]
|
|
|
|
withAnimation(.spring(response: 0.3)) {
|
|
for task in allTasks where popularTaskTitles.contains(task.title) {
|
|
if selectedTasks.count < maxTasksAllowed {
|
|
selectedTasks.insert(task.id)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func addSelectedTasks() {
|
|
// If no tasks selected, just skip
|
|
if selectedTasks.isEmpty {
|
|
onTaskAdded()
|
|
return
|
|
}
|
|
|
|
// Get the residence ID from OnboardingState (set during residence creation)
|
|
guard let residenceId = onboardingState.createdResidenceId else {
|
|
print("🏠 ONBOARDING: No residence ID found in OnboardingState, skipping task creation")
|
|
onTaskAdded()
|
|
return
|
|
}
|
|
|
|
isCreatingTasks = true
|
|
|
|
let selectedTemplates = allTasks.filter { selectedTasks.contains($0.id) }
|
|
var completedCount = 0
|
|
let totalCount = selectedTemplates.count
|
|
|
|
// Format today's date as YYYY-MM-DD for the API
|
|
let dateFormatter = DateFormatter()
|
|
dateFormatter.dateFormat = "yyyy-MM-dd"
|
|
let todayString = dateFormatter.string(from: Date())
|
|
|
|
print("🏠 ONBOARDING: Creating \(totalCount) tasks for residence \(residenceId)")
|
|
|
|
for template in selectedTemplates {
|
|
// Look up category ID from DataManager
|
|
let categoryId: Int32? = {
|
|
let categoryName = template.category.lowercased()
|
|
return dataManager.taskCategories.first { $0.name.lowercased() == categoryName }?.id
|
|
}()
|
|
|
|
// Look up frequency ID from DataManager
|
|
let frequencyId: Int32? = {
|
|
let frequencyName = template.frequency.lowercased()
|
|
return dataManager.taskFrequencies.first { $0.name.lowercased() == frequencyName }?.id
|
|
}()
|
|
|
|
print("🏠 ONBOARDING: Creating task '\(template.title)' - categoryId=\(String(describing: categoryId)), frequencyId=\(String(describing: frequencyId))")
|
|
|
|
let request = TaskCreateRequest(
|
|
residenceId: residenceId,
|
|
title: template.title,
|
|
description: nil,
|
|
categoryId: categoryId.map { KotlinInt(int: $0) },
|
|
priorityId: nil,
|
|
inProgress: false,
|
|
frequencyId: frequencyId.map { KotlinInt(int: $0) },
|
|
assignedToId: nil,
|
|
dueDate: todayString,
|
|
estimatedCost: nil,
|
|
contractorId: nil
|
|
)
|
|
|
|
viewModel.createTask(request: request) { success in
|
|
completedCount += 1
|
|
print("🏠 ONBOARDING: Task '\(template.title)' creation: \(success ? "SUCCESS" : "FAILED") (\(completedCount)/\(totalCount))")
|
|
|
|
if completedCount == totalCount {
|
|
self.isCreatingTasks = false
|
|
self.onTaskAdded()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Onboarding Task Category Model
|
|
|
|
struct OnboardingTaskCategory: Identifiable {
|
|
let id = UUID()
|
|
let name: String
|
|
let icon: String
|
|
let color: Color
|
|
let tasks: [OnboardingTaskTemplate]
|
|
}
|
|
|
|
// MARK: - Task Category Section
|
|
|
|
struct TaskCategorySection: View {
|
|
let category: OnboardingTaskCategory
|
|
@Binding var selectedTasks: Set<UUID>
|
|
let isExpanded: Bool
|
|
let isAtMaxSelection: Bool
|
|
var onToggleExpand: () -> Void
|
|
|
|
private var selectedInCategory: Int {
|
|
category.tasks.filter { selectedTasks.contains($0.id) }.count
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Category header
|
|
Button(action: onToggleExpand) {
|
|
HStack(spacing: AppSpacing.md) {
|
|
// Category icon
|
|
ZStack {
|
|
Circle()
|
|
.fill(
|
|
LinearGradient(
|
|
colors: [category.color, category.color.opacity(0.7)],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
)
|
|
.frame(width: 44, height: 44)
|
|
|
|
Image(systemName: category.icon)
|
|
.font(.title3)
|
|
.foregroundColor(.white)
|
|
}
|
|
|
|
// Category name
|
|
Text(category.name)
|
|
.font(.headline)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(Color.appTextPrimary)
|
|
|
|
Spacer()
|
|
|
|
// Selection badge
|
|
if selectedInCategory > 0 {
|
|
Text("\(selectedInCategory)")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
.frame(width: 24, height: 24)
|
|
.background(category.color)
|
|
.clipShape(Circle())
|
|
}
|
|
|
|
// Chevron
|
|
Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
.padding(AppSpacing.md)
|
|
.background(Color.appBackgroundSecondary)
|
|
.cornerRadius(isExpanded ? AppRadius.lg : AppRadius.lg, corners: isExpanded ? [.topLeft, .topRight] : .allCorners)
|
|
}
|
|
.buttonStyle(.plain)
|
|
|
|
// Expanded tasks
|
|
if isExpanded {
|
|
VStack(spacing: 0) {
|
|
ForEach(category.tasks) { task in
|
|
let taskIsSelected = selectedTasks.contains(task.id)
|
|
OnboardingTaskTemplateRow(
|
|
template: task,
|
|
isSelected: taskIsSelected,
|
|
isDisabled: isAtMaxSelection && !taskIsSelected,
|
|
onTap: {
|
|
withAnimation(.spring(response: 0.2)) {
|
|
if taskIsSelected {
|
|
selectedTasks.remove(task.id)
|
|
} else if !isAtMaxSelection {
|
|
selectedTasks.insert(task.id)
|
|
}
|
|
}
|
|
}
|
|
)
|
|
|
|
if task.id != category.tasks.last?.id {
|
|
Divider()
|
|
.padding(.leading, 60)
|
|
}
|
|
}
|
|
}
|
|
.background(Color.appBackgroundSecondary.opacity(0.5))
|
|
.cornerRadius(AppRadius.lg, corners: [.bottomLeft, .bottomRight])
|
|
}
|
|
}
|
|
.shadow(color: Color.black.opacity(0.05), radius: 8, y: 4)
|
|
}
|
|
}
|
|
|
|
// MARK: - Task Template Row
|
|
|
|
struct OnboardingTaskTemplateRow: View {
|
|
let template: OnboardingTaskTemplate
|
|
let isSelected: Bool
|
|
let isDisabled: Bool
|
|
var onTap: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: onTap) {
|
|
HStack(spacing: AppSpacing.md) {
|
|
// Checkbox
|
|
ZStack {
|
|
Circle()
|
|
.stroke(isSelected ? template.color : Color.appTextSecondary.opacity(isDisabled ? 0.15 : 0.3), lineWidth: 2)
|
|
.frame(width: 28, height: 28)
|
|
|
|
if isSelected {
|
|
Circle()
|
|
.fill(template.color)
|
|
.frame(width: 28, height: 28)
|
|
|
|
Image(systemName: "checkmark")
|
|
.font(.caption)
|
|
.fontWeight(.bold)
|
|
.foregroundColor(.white)
|
|
}
|
|
}
|
|
|
|
// Task info
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(template.title)
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(isDisabled ? Color.appTextSecondary.opacity(0.5) : Color.appTextPrimary)
|
|
|
|
Text(template.frequency.capitalized)
|
|
.font(.caption)
|
|
.foregroundColor(Color.appTextSecondary.opacity(isDisabled ? 0.5 : 1))
|
|
}
|
|
|
|
Spacer()
|
|
|
|
// Task icon
|
|
Image(systemName: template.icon)
|
|
.font(.title3)
|
|
.foregroundColor(template.color.opacity(isDisabled ? 0.3 : 0.6))
|
|
}
|
|
.padding(.horizontal, AppSpacing.md)
|
|
.padding(.vertical, AppSpacing.sm)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.disabled(isDisabled)
|
|
}
|
|
}
|
|
|
|
// MARK: - Onboarding Task Template Model
|
|
|
|
struct OnboardingTaskTemplate: Identifiable {
|
|
let id = UUID()
|
|
let icon: String
|
|
let title: String
|
|
let category: String
|
|
let frequency: String
|
|
let color: Color
|
|
}
|
|
|
|
// MARK: - Legacy wrapper with navigation bar (for backwards compatibility)
|
|
|
|
struct OnboardingFirstTaskView: View {
|
|
var residenceName: String
|
|
var onTaskAdded: () -> Void
|
|
var onSkip: () -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Navigation bar
|
|
HStack {
|
|
Spacer()
|
|
|
|
Button(action: onSkip) {
|
|
Text("Skip")
|
|
.font(.subheadline)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
}
|
|
.padding(.horizontal, AppSpacing.lg)
|
|
.padding(.vertical, AppSpacing.md)
|
|
|
|
OnboardingFirstTaskContent(
|
|
residenceName: residenceName,
|
|
onTaskAdded: onTaskAdded
|
|
)
|
|
}
|
|
.background(Color.appBackgroundPrimary)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
OnboardingFirstTaskContent(
|
|
residenceName: "My Home",
|
|
onTaskAdded: {}
|
|
)
|
|
}
|