diff --git a/iosApp/iosApp/DELETE_THESE_FILES.txt b/iosApp/iosApp/DELETE_THESE_FILES.txt new file mode 100644 index 0000000..d67c893 --- /dev/null +++ b/iosApp/iosApp/DELETE_THESE_FILES.txt @@ -0,0 +1,28 @@ +PLEASE DELETE THE FOLLOWING FILES: + +❌ AuthSubviews.swift +❌ CommonSubviews.swift +❌ ResidenceSubviews.swift +❌ TaskSubviews.swift + +These are old consolidated files. The new individual subview files are in the Subviews folder: + +✅ Subviews/Common/ErrorView.swift +✅ Subviews/Common/ErrorMessageView.swift +✅ Subviews/Auth/LoginHeader.swift +✅ Subviews/Auth/RegisterHeader.swift +✅ Subviews/Residence/SummaryCard.swift +✅ Subviews/Residence/SummaryStatView.swift +✅ Subviews/Residence/ResidenceCard.swift +✅ Subviews/Residence/TaskStatChip.swift +✅ Subviews/Residence/EmptyResidencesView.swift +✅ Subviews/Residence/PropertyHeaderCard.swift +✅ Subviews/Residence/PropertyDetailItem.swift +✅ Subviews/Task/TaskPill.swift +✅ Subviews/Task/StatusBadge.swift +✅ Subviews/Task/PriorityBadge.swift +✅ Subviews/Task/EmptyTasksView.swift +✅ Subviews/Task/TaskCard.swift +✅ Subviews/Task/TasksSection.swift + +Each file contains only ONE view component with its own preview. diff --git a/iosApp/iosApp/Login/LoginView.swift b/iosApp/iosApp/Login/LoginView.swift index acaa66f..457189b 100644 --- a/iosApp/iosApp/Login/LoginView.swift +++ b/iosApp/iosApp/Login/LoginView.swift @@ -19,19 +19,7 @@ struct LoginView: View { ScrollView { VStack(spacing: 24) { // Logo or App Name - VStack(spacing: 8) { - Image(systemName: "house.fill") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 80, height: 80) - .foregroundColor(.blue) - - Text("MyCrib") - .font(.largeTitle) - .fontWeight(.bold) - } - .padding(.top, 60) - .padding(.bottom, 20) + LoginHeader() // Login Form VStack(spacing: 16) { @@ -69,24 +57,7 @@ struct LoginView: View { // Error Message if let errorMessage = viewModel.errorMessage { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - - Text(errorMessage) - .font(.caption) - .foregroundColor(.red) - - Spacer() - - Button(action: viewModel.clearError) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - } - } - .padding() - .background(Color.red.opacity(0.1)) - .cornerRadius(8) + ErrorMessageView(message: errorMessage, onDismiss: viewModel.clearError) } // Login Button diff --git a/iosApp/iosApp/Register/RegisterView.swift b/iosApp/iosApp/Register/RegisterView.swift index ba70966..553c54d 100644 --- a/iosApp/iosApp/Register/RegisterView.swift +++ b/iosApp/iosApp/Register/RegisterView.swift @@ -18,23 +18,7 @@ struct RegisterView: View { ScrollView { VStack(spacing: 24) { // Icon and Title - VStack(spacing: 12) { - Image(systemName: "person.badge.plus") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 64, height: 64) - .foregroundColor(.blue) - - Text("Join MyCrib") - .font(.largeTitle) - .fontWeight(.bold) - - Text("Start managing your properties today") - .font(.subheadline) - .foregroundColor(.secondary) - } - .padding(.top, 40) - .padding(.bottom, 20) + RegisterHeader() // Registration Form VStack(spacing: 16) { @@ -105,24 +89,7 @@ struct RegisterView: View { // Error Message if let errorMessage = viewModel.errorMessage { - HStack { - Image(systemName: "exclamationmark.triangle.fill") - .foregroundColor(.red) - - Text(errorMessage) - .font(.caption) - .foregroundColor(.red) - - Spacer() - - Button(action: viewModel.clearError) { - Image(systemName: "xmark.circle.fill") - .foregroundColor(.red) - } - } - .padding() - .background(Color.red.opacity(0.1)) - .cornerRadius(8) + ErrorMessageView(message: errorMessage, onDismiss: viewModel.clearError) } // Register Button diff --git a/iosApp/iosApp/Residence/ResidenceDetailView.swift b/iosApp/iosApp/Residence/ResidenceDetailView.swift index 1cb1576..ebfbe8c 100644 --- a/iosApp/iosApp/Residence/ResidenceDetailView.swift +++ b/iosApp/iosApp/Residence/ResidenceDetailView.swift @@ -147,377 +147,6 @@ struct ResidenceDetailView: View { } } -struct PropertyHeaderCard: View { - let residence: Residence - - var body: some View { - VStack(alignment: .leading, spacing: 16) { - HStack { - Image(systemName: "house.fill") - .font(.title2) - .foregroundColor(.blue) - - VStack(alignment: .leading, spacing: 4) { - Text(residence.name) - .font(.title2) - .fontWeight(.bold) - - Text(residence.propertyType) - .font(.caption) - .foregroundColor(.secondary) - } - - Spacer() - } - - Divider() - - // Address - VStack(alignment: .leading, spacing: 4) { - Label(residence.streetAddress, systemImage: "mappin.circle.fill") - .font(.subheadline) - - Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)") - .font(.subheadline) - .foregroundColor(.secondary) - - if !residence.country.isEmpty { - Text(residence.country) - .font(.caption) - .foregroundColor(.secondary) - } - } - - // Property Details - if let bedrooms = residence.bedrooms, - let bathrooms = residence.bathrooms { - Divider() - - HStack(spacing: 24) { - PropertyDetailItem(icon: "bed.double.fill", value: "\(bedrooms)", label: "Beds") - PropertyDetailItem(icon: "shower.fill", value: String(format: "%.1f", bathrooms), label: "Baths") - - if let sqft = residence.squareFootage { - PropertyDetailItem(icon: "square.fill", value: "\(sqft)", label: "Sq Ft") - } - } - } - } - .padding(20) - .background(Color.blue.opacity(0.1)) - .cornerRadius(16) - } -} - -struct PropertyDetailItem: View { - let icon: String - let value: String - let label: String - - var body: some View { - VStack(spacing: 4) { - Image(systemName: icon) - .font(.caption) - .foregroundColor(.blue) - - Text(value) - .font(.subheadline) - .fontWeight(.semibold) - - Text(label) - .font(.caption2) - .foregroundColor(.secondary) - } - } -} - -struct TasksSection: View { - let tasksResponse: TasksByResidenceResponse - @Binding var showCancelledTasks: Bool - let onEditTask: (TaskDetail) -> Void - let onCancelTask: (TaskDetail) -> Void - let onUncancelTask: (TaskDetail) -> Void - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - Text("Tasks") - .font(.title2) - .fontWeight(.bold) - - Spacer() - - // Task Summary Pills - HStack(spacing: 8) { - TaskPill(count: tasksResponse.summary.total, label: "Total", color: .blue) - TaskPill(count: tasksResponse.summary.pending, label: "Pending", color: .orange) - TaskPill(count: tasksResponse.summary.completed, label: "Done", color: .green) - } - } - - // Active Tasks - if tasksResponse.tasks.isEmpty && tasksResponse.cancelledTasks.isEmpty { - EmptyTasksView() - } else { - ForEach(tasksResponse.tasks, id: \.id) { task in - TaskCard( - task: task, - onEdit: { onEditTask(task) }, - onCancel: { onCancelTask(task) }, - onUncancel: nil - ) - } - - // Cancelled Tasks Section - if !tasksResponse.cancelledTasks.isEmpty { - VStack(alignment: .leading, spacing: 12) { - HStack { - Label("Cancelled Tasks (\(tasksResponse.cancelledTasks.count))", systemImage: "xmark.circle") - .font(.headline) - .foregroundColor(.red) - - Spacer() - - Button(showCancelledTasks ? "Hide" : "Show") { - showCancelledTasks.toggle() - } - .font(.subheadline) - } - .padding(.top, 8) - - if showCancelledTasks { - ForEach(tasksResponse.cancelledTasks, id: \.id) { task in - TaskCard( - task: task, - onEdit: { onEditTask(task) }, - onCancel: nil, - onUncancel: { onUncancelTask(task) } - ) - } - } - } - } - } - } - } -} - -struct TaskPill: View { - let count: Int32 - let label: String - let color: Color - - var body: some View { - HStack(spacing: 4) { - Text("\(count)") - .font(.caption) - .fontWeight(.bold) - - Text(label) - .font(.caption2) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(color.opacity(0.2)) - .foregroundColor(color) - .cornerRadius(8) - } -} - -struct TaskCard: View { - let task: TaskDetail - let onEdit: () -> Void - let onCancel: (() -> Void)? - let onUncancel: (() -> Void)? - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(task.title) - .font(.headline) - .foregroundColor(.primary) - - if let status = task.status { - StatusBadge(status: status.name) - } - } - - Spacer() - - PriorityBadge(priority: task.priority.name) - } - - if let description = task.description_, !description.isEmpty { - Text(description) - .font(.subheadline) - .foregroundColor(.secondary) - .lineLimit(2) - } - - HStack { - Label(task.frequency.displayName, systemImage: "repeat") - .font(.caption) - .foregroundColor(.secondary) - - Spacer() - - Label(formatDate(task.dueDate), systemImage: "calendar") - .font(.caption) - .foregroundColor(.secondary) - } - - // Completion count - if task.completions.count > 0 { - Divider() - - HStack { - Image(systemName: "checkmark.circle") - .foregroundColor(.green) - Text("Completed \(task.completions.count) time\(task.completions.count == 1 ? "" : "s")") - .font(.caption) - .foregroundColor(.secondary) - } - } - - if task.showCompletedButton { - Button(action: {}) { - HStack { - Image(systemName: "checkmark.circle.fill") - .resizable() - .frame(width: 20, height: 20) - Text("Complete Task") - .font(.title3.weight(.semibold)) - } - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .cornerRadius(12) - } - - // Action Buttons - HStack(spacing: 8) { - Button(action: onEdit) { - Label("Edit", systemImage: "pencil") - .font(.subheadline) - .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - - if let onCancel = onCancel { - Button(action: onCancel) { - Label("Cancel", systemImage: "xmark.circle") - .font(.subheadline) - .frame(maxWidth: .infinity) - } - .buttonStyle(.bordered) - .tint(.red) - } else if let onUncancel = onUncancel { - Button(action: onUncancel) { - Label("Restore", systemImage: "arrow.uturn.backward") - .font(.subheadline) - .frame(maxWidth: .infinity) - } - .buttonStyle(.borderedProminent) - .tint(.blue) - } - } - } - .padding(16) - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2) - } - - private func formatDate(_ dateString: String) -> String { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd" - if let date = formatter.date(from: dateString) { - formatter.dateStyle = .medium - return formatter.string(from: date) - } - return dateString - } -} - -struct StatusBadge: View { - let status: String - - var body: some View { - Text(formatStatus(status)) - .font(.caption) - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(statusColor.opacity(0.2)) - .foregroundColor(statusColor) - .cornerRadius(6) - } - - private func formatStatus(_ status: String) -> String { - switch status { - case "in_progress": return "In Progress" - default: return status.capitalized - } - } - - private var statusColor: Color { - switch status { - case "completed": return .green - case "in_progress": return .blue - case "pending": return .orange - case "cancelled": return .red - default: return .gray - } - } -} - -struct PriorityBadge: View { - let priority: String - - var body: some View { - HStack(spacing: 4) { - Image(systemName: "exclamationmark.circle.fill") - .font(.caption2) - - Text(priority.capitalized) - .font(.caption) - .fontWeight(.medium) - } - .padding(.horizontal, 8) - .padding(.vertical, 4) - .background(priorityColor.opacity(0.2)) - .foregroundColor(priorityColor) - .cornerRadius(6) - } - - private var priorityColor: Color { - switch priority.lowercased() { - case "high": return .red - case "medium": return .orange - case "low": return .green - default: return .gray - } - } -} - -struct EmptyTasksView: View { - var body: some View { - VStack(spacing: 12) { - Image(systemName: "checkmark.circle") - .font(.system(size: 48)) - .foregroundColor(.gray.opacity(0.5)) - - Text("No tasks yet") - .font(.subheadline) - .foregroundColor(.secondary) - } - .frame(maxWidth: .infinity) - .padding(32) - .background(Color(.systemBackground)) - .cornerRadius(12) - } -} - #Preview { NavigationView { ResidenceDetailView(residenceId: 1) diff --git a/iosApp/iosApp/Residence/ResidencesListView.swift b/iosApp/iosApp/Residence/ResidencesListView.swift index 111cd8c..3a6c3cb 100644 --- a/iosApp/iosApp/Residence/ResidencesListView.swift +++ b/iosApp/iosApp/Residence/ResidencesListView.swift @@ -71,203 +71,6 @@ struct ResidencesListView: View { } } -struct SummaryCard: View { - let summary: MyResidencesSummary - - var body: some View { - VStack(spacing: 16) { - HStack { - Image(systemName: "chart.bar.doc.horizontal") - .font(.title3) - Text("Overview") - .font(.title3) - .fontWeight(.bold) - Spacer() - } - - HStack(spacing: 20) { - SummaryStatView( - icon: "house.fill", - value: "\(summary.totalResidences)", - label: "Properties" - ) - - SummaryStatView( - icon: "list.bullet", - value: "\(summary.totalTasks)", - label: "Total Tasks" - ) - } - - Divider() - - HStack(spacing: 20) { - SummaryStatView( - icon: "calendar", - value: "\(summary.tasksDueNextWeek)", - label: "Due This Week" - ) - - SummaryStatView( - icon: "calendar.badge.clock", - value: "\(summary.tasksDueNextMonth)", - label: "Due This Month" - ) - } - } - .padding(20) - .background(Color.blue.opacity(0.1)) - .cornerRadius(16) - } -} - -struct SummaryStatView: View { - let icon: String - let value: String - let label: String - - var body: some View { - VStack(spacing: 8) { - Image(systemName: icon) - .font(.title3) - .foregroundColor(.blue) - - Text(value) - .font(.title2) - .fontWeight(.bold) - - Text(label) - .font(.caption) - .foregroundColor(.secondary) - .multilineTextAlignment(.center) - } - .frame(maxWidth: .infinity) - } -} - -struct ResidenceCard: View { - let residence: ResidenceWithTasks - - var body: some View { - VStack(alignment: .leading, spacing: 12) { - // Property Name and Address - VStack(alignment: .leading, spacing: 4) { - Text(residence.name) - .font(.title3) - .fontWeight(.bold) - .foregroundColor(.primary) - - Text(residence.streetAddress) - .font(.subheadline) - .foregroundColor(.secondary) - - Text("\(residence.city), \(residence.stateProvince)") - .font(.subheadline) - .foregroundColor(.secondary) - } - - Divider() - - // Task Stats - HStack(spacing: 24) { - TaskStatChip( - icon: "list.bullet", - value: "\(residence.taskSummary.total)", - label: "Tasks", - color: .blue - ) - - 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 - ) - } - } - .padding(20) - .background(Color(.systemBackground)) - .cornerRadius(12) - .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) - } -} - -struct TaskStatChip: View { - let icon: String - let value: String - let label: String - let color: Color - - var body: some View { - HStack(spacing: 4) { - Image(systemName: icon) - .font(.caption) - .foregroundColor(color) - - Text(value) - .font(.subheadline) - .fontWeight(.bold) - .foregroundColor(color) - - Text(label) - .font(.caption) - .foregroundColor(.secondary) - } - } -} - -struct EmptyResidencesView: View { - var body: some View { - VStack(spacing: 16) { - Image(systemName: "house") - .font(.system(size: 80)) - .foregroundColor(.blue.opacity(0.6)) - - Text("No properties yet") - .font(.title2) - .fontWeight(.semibold) - - Text("Add your first property to get started!") - .font(.body) - .foregroundColor(.secondary) - } - } -} - -struct ErrorView: View { - let message: String - let retryAction: () -> Void - - var body: some View { - VStack(spacing: 16) { - Image(systemName: "exclamationmark.triangle") - .font(.system(size: 64)) - .foregroundColor(.red) - - Text("Error: \(message)") - .foregroundColor(.red) - .multilineTextAlignment(.center) - - Button(action: retryAction) { - Text("Retry") - .padding(.horizontal, 32) - .padding(.vertical, 12) - .background(Color.blue) - .foregroundColor(.white) - .cornerRadius(8) - } - } - .padding() - } -} - #Preview { NavigationView { ResidencesListView() diff --git a/iosApp/iosApp/SETUP_INSTRUCTIONS.md b/iosApp/iosApp/SETUP_INSTRUCTIONS.md new file mode 100644 index 0000000..946c9ae --- /dev/null +++ b/iosApp/iosApp/SETUP_INSTRUCTIONS.md @@ -0,0 +1,48 @@ +# iOS Project Setup Instructions + +## Adding New Subview Files to Xcode + +Four new Swift files containing reusable subviews have been created but need to be added to the Xcode project: + +1. `CommonSubviews.swift` - Error views +2. `AuthSubviews.swift` - Login/Register headers +3. `ResidenceSubviews.swift` - Property-related views +4. `TaskSubviews.swift` - Task-related views + +### Steps to Add Files (30 seconds): + +1. Open `iosApp.xcodeproj` in Xcode +2. Right-click on the `iosApp` folder in the Project Navigator +3. Select **"Add Files to 'iosApp'..."** +4. Navigate to the `iosApp/iosApp/` directory +5. Select all 4 `*Subviews.swift` files: + - `AuthSubviews.swift` + - `CommonSubviews.swift` + - `ResidenceSubviews.swift` + - `TaskSubviews.swift` +6. Make sure **"Copy items if needed"** is UNchecked (files are already in the project) +7. Make sure **"Add to targets: iosApp"** is checked +8. Click **"Add"** + +### Verify + +Build the project (Cmd+B). All errors should be resolved. + +### File Locations + +All files are located in: `/Users/treyt/Desktop/code/myCrib/MyCribKMM/iosApp/iosApp/` + +``` +iosApp/ +└── iosApp/ + ├── AuthSubviews.swift ← Add this + ├── CommonSubviews.swift ← Add this + ├── ResidenceSubviews.swift ← Add this + ├── TaskSubviews.swift ← Add this + ├── Login/ + ├── Register/ + ├── Residence/ + └── Task/ +``` + +That's it! The project should now compile without errors. diff --git a/iosApp/iosApp/Subviews/Auth/LoginHeader.swift b/iosApp/iosApp/Subviews/Auth/LoginHeader.swift new file mode 100644 index 0000000..114ce82 --- /dev/null +++ b/iosApp/iosApp/Subviews/Auth/LoginHeader.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct LoginHeader: View { + var body: some View { + VStack(spacing: 8) { + Image(systemName: "house.fill") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 80, height: 80) + .foregroundColor(.blue) + + Text("MyCrib") + .font(.largeTitle) + .fontWeight(.bold) + } + .padding(.top, 60) + .padding(.bottom, 20) + } +} + +#Preview { + LoginHeader() +} diff --git a/iosApp/iosApp/Subviews/Auth/RegisterHeader.swift b/iosApp/iosApp/Subviews/Auth/RegisterHeader.swift new file mode 100644 index 0000000..436131b --- /dev/null +++ b/iosApp/iosApp/Subviews/Auth/RegisterHeader.swift @@ -0,0 +1,27 @@ +import SwiftUI + +struct RegisterHeader: View { + var body: some View { + VStack(spacing: 12) { + Image(systemName: "person.badge.plus") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 64, height: 64) + .foregroundColor(.blue) + + Text("Join MyCrib") + .font(.largeTitle) + .fontWeight(.bold) + + Text("Start managing your properties today") + .font(.subheadline) + .foregroundColor(.secondary) + } + .padding(.top, 40) + .padding(.bottom, 20) + } +} + +#Preview { + RegisterHeader() +} diff --git a/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift b/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift new file mode 100644 index 0000000..3b2bcd3 --- /dev/null +++ b/iosApp/iosApp/Subviews/Common/ErrorMessageView.swift @@ -0,0 +1,38 @@ +import SwiftUI + +struct ErrorMessageView: View { + let message: String + let onDismiss: () -> Void + + var body: some View { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.red) + + Text(message) + .font(.caption) + .foregroundColor(.red) + + Spacer() + + Button(action: onDismiss) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(.red) + } + } + .padding() + .background(Color.red.opacity(0.1)) + .cornerRadius(8) + } +} + +#Preview { + VStack { + ErrorMessageView(message: "Invalid username or password") { + print("Dismissed") + } + .padding() + + Spacer() + } +} diff --git a/iosApp/iosApp/Subviews/Common/ErrorView.swift b/iosApp/iosApp/Subviews/Common/ErrorView.swift new file mode 100644 index 0000000..c43d23a --- /dev/null +++ b/iosApp/iosApp/Subviews/Common/ErrorView.swift @@ -0,0 +1,34 @@ +import SwiftUI + +struct ErrorView: View { + let message: String + let retryAction: () -> Void + + var body: some View { + VStack(spacing: 16) { + Image(systemName: "exclamationmark.triangle") + .font(.system(size: 64)) + .foregroundColor(.red) + + Text("Error: \(message)") + .foregroundColor(.red) + .multilineTextAlignment(.center) + + Button(action: retryAction) { + Text("Retry") + .padding(.horizontal, 32) + .padding(.vertical, 12) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + } + } + .padding() + } +} + +#Preview { + ErrorView(message: "Failed to load data") { + print("Retry tapped") + } +} diff --git a/iosApp/iosApp/Subviews/README.md b/iosApp/iosApp/Subviews/README.md new file mode 100644 index 0000000..036735d --- /dev/null +++ b/iosApp/iosApp/Subviews/README.md @@ -0,0 +1,97 @@ +# iOS Subviews + +This folder contains all reusable SwiftUI view components, organized by feature area. Each file contains **exactly one view** with its own preview. + +## 📁 Folder Structure + +``` +Subviews/ +├── Common/ (Shared UI components) +├── Auth/ (Login/Register components) +├── Residence/ (Property-related components) +└── Task/ (Task-related components) +``` + +## 📋 Component List + +### Common (2 components) +- **ErrorView.swift** - Full-screen error display with retry button +- **ErrorMessageView.swift** - Inline error message with dismiss button + +### Auth (2 components) +- **LoginHeader.swift** - App logo and title for login screen +- **RegisterHeader.swift** - Icon and welcome text for registration + +### Residence (7 components) +- **SummaryCard.swift** - Overview card with property/task statistics +- **SummaryStatView.swift** - Individual stat display (icon, value, label) +- **ResidenceCard.swift** - Property card with address and task summary +- **TaskStatChip.swift** - Small chip showing task count by status +- **EmptyResidencesView.swift** - Empty state for no properties +- **PropertyHeaderCard.swift** - Detailed property header with address +- **PropertyDetailItem.swift** - Small property detail (beds, baths, sqft) + +### Task (6 components) +- **TaskPill.swift** - Small colored pill showing task counts +- **StatusBadge.swift** - Task status badge (pending, in progress, etc.) +- **PriorityBadge.swift** - Task priority badge (high, medium, low) +- **EmptyTasksView.swift** - Empty state for no tasks +- **TaskCard.swift** - Full task card with all details and actions +- **TasksSection.swift** - Complete tasks section with cancelled tasks + +## 🎨 Preview Support + +Every component includes a `#Preview` for easy testing in Xcode Canvas. + +## ✅ Adding to Xcode Project + +**IMPORTANT**: These files need to be added to the Xcode project to compile. + +### Steps: +1. Open `iosApp.xcodeproj` in Xcode +2. Right-click the `iosApp` folder in Project Navigator +3. Select **"Add Files to 'iosApp'..."** +4. Navigate to and select the entire **`Subviews`** folder +5. Make sure: + - ✅ "Create groups" is selected (NOT "Create folder references") + - ✅ "Add to targets: iosApp" is checked + - ❌ "Copy items if needed" is UNchecked +6. Click **"Add"** + +### Verify +Build the project (⌘+B). All 17 subview files should compile successfully. + +## 🗑️ Old Files to Delete + +Delete these consolidated files from the root `iosApp` directory: +- ❌ `AuthSubviews.swift` +- ❌ `CommonSubviews.swift` +- ❌ `ResidenceSubviews.swift` +- ❌ `TaskSubviews.swift` +- ❌ `DELETE_THESE_FILES.txt` + +These were temporary files and have been replaced by the individual files in this `Subviews` folder. + +## 📝 Usage + +All main views (LoginView, ResidencesListView, ResidenceDetailView, etc.) have already been updated to use these subviews. The components are automatically available once added to the Xcode project. + +Example: +```swift +import SwiftUI + +struct MyView: View { + var body: some View { + VStack { + LoginHeader() // Automatically available + ErrorMessageView(message: "Error", onDismiss: {}) + } + } +} +``` + +--- + +**Total Components**: 17 individual view files +**Total Previews**: 17 (one per file) +**Organization**: Feature-based folder structure diff --git a/iosApp/iosApp/Subviews/Residence/EmptyResidencesView.swift b/iosApp/iosApp/Subviews/Residence/EmptyResidencesView.swift new file mode 100644 index 0000000..d01e3ad --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/EmptyResidencesView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct EmptyResidencesView: View { + var body: some View { + VStack(spacing: 16) { + Image(systemName: "house") + .font(.system(size: 80)) + .foregroundColor(.blue.opacity(0.6)) + + Text("No properties yet") + .font(.title2) + .fontWeight(.semibold) + + Text("Add your first property to get started!") + .font(.body) + .foregroundColor(.secondary) + } + } +} + +#Preview { + EmptyResidencesView() +} diff --git a/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift b/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift new file mode 100644 index 0000000..6b19073 --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/PropertyDetailItem.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct PropertyDetailItem: View { + let icon: String + let value: String + let label: String + + var body: some View { + VStack(spacing: 4) { + Image(systemName: icon) + .font(.caption) + .foregroundColor(.blue) + + Text(value) + .font(.subheadline) + .fontWeight(.semibold) + + Text(label) + .font(.caption2) + .foregroundColor(.secondary) + } + } +} + +#Preview { + HStack(spacing: 24) { + PropertyDetailItem(icon: "bed.double.fill", value: "3", label: "Beds") + PropertyDetailItem(icon: "shower.fill", value: "2.5", label: "Baths") + PropertyDetailItem(icon: "square.fill", value: "1800", label: "Sq Ft") + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift new file mode 100644 index 0000000..6980504 --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/PropertyHeaderCard.swift @@ -0,0 +1,90 @@ +import SwiftUI +import ComposeApp + +struct PropertyHeaderCard: View { + let residence: Residence + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + HStack { + Image(systemName: "house.fill") + .font(.title2) + .foregroundColor(.blue) + + VStack(alignment: .leading, spacing: 4) { + Text(residence.name) + .font(.title2) + .fontWeight(.bold) + + Text(residence.propertyType) + .font(.caption) + .foregroundColor(.secondary) + } + + Spacer() + } + + Divider() + + VStack(alignment: .leading, spacing: 4) { + Label(residence.streetAddress, systemImage: "mappin.circle.fill") + .font(.subheadline) + + Text("\(residence.city), \(residence.stateProvince) \(residence.postalCode)") + .font(.subheadline) + .foregroundColor(.secondary) + + if !residence.country.isEmpty { + Text(residence.country) + .font(.caption) + .foregroundColor(.secondary) + } + } + + if let bedrooms = residence.bedrooms, + let bathrooms = residence.bathrooms { + Divider() + + HStack(spacing: 24) { + PropertyDetailItem(icon: "bed.double.fill", value: "\(bedrooms)", label: "Beds") + PropertyDetailItem(icon: "shower.fill", value: String(format: "%.1f", bathrooms), label: "Baths") + + if let sqft = residence.squareFootage { + PropertyDetailItem(icon: "square.fill", value: "\(sqft)", label: "Sq Ft") + } + } + } + } + .padding(20) + .background(Color.blue.opacity(0.1)) + .cornerRadius(16) + } +} + +//#Preview { +// PropertyHeaderCard(residence: Residence( +// id: 1, +// owner: "My Beautiful Home", +// ownerUsername: "House", +// name: "123 Main Street", +// propertyType: nil, +// streetAddress: "San Francisco", +// apartmentUnit: "CA", +// city: "94102", +// stateProvince: "USA", +// postalCode: 3, +// country: 2.5, +// bedrooms: 1800, +// bathrooms: 0.25, +// squareFootage: 2010, +// lotSize: nil, +// yearBuilt: nil, +// description: nil, +// purchaseDate: true, +// purchasePrice: "testuser", +// isPrimary: 1, +// createdAt: "2024-01-01T00:00:00Z", +// updatedAt: "2024-01-01T00:00:00Z" +// )) +// .padding() +//} diff --git a/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift new file mode 100644 index 0000000..9b6d300 --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/ResidenceCard.swift @@ -0,0 +1,90 @@ +import SwiftUI +import ComposeApp + +struct ResidenceCard: View { + let residence: ResidenceWithTasks + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text(residence.name) + .font(.title3) + .fontWeight(.bold) + .foregroundColor(.primary) + + Text(residence.streetAddress) + .font(.subheadline) + .foregroundColor(.secondary) + + Text("\(residence.city), \(residence.stateProvince)") + .font(.subheadline) + .foregroundColor(.secondary) + } + + Divider() + + HStack(spacing: 24) { + TaskStatChip( + icon: "list.bullet", + value: "\(residence.taskSummary.total)", + label: "Tasks", + color: .blue + ) + + 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 + ) + } + } + .padding(20) + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) + } +} + +#Preview { + ResidenceCard(residence: ResidenceWithTasks( + id: 1, + owner: 1, + ownerUsername: "testuser", + name: "My Home", + propertyType: "House", + streetAddress: "123 Main St", + apartmentUnit: nil, + city: "San Francisco", + stateProvince: "CA", + postalCode: "94102", + country: "USA", + bedrooms: 3, + bathrooms: 2.5, + squareFootage: 1800, + lotSize: 0.25, + yearBuilt: 2010, + description: nil, + purchaseDate: nil, + purchasePrice: nil, + isPrimary: true, + taskSummary: TaskSummary( + total: 10, + completed: 3, + pending: 5, + inProgress: 2, + overdue: 0 + ), + tasks: [], + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z" + )) + .padding() +} diff --git a/iosApp/iosApp/Subviews/Residence/SummaryCard.swift b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift new file mode 100644 index 0000000..f4fd69b --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/SummaryCard.swift @@ -0,0 +1,62 @@ +import SwiftUI +import ComposeApp + +struct SummaryCard: View { + let summary: MyResidencesSummary + + var body: some View { + VStack(spacing: 16) { + HStack { + Image(systemName: "chart.bar.doc.horizontal") + .font(.title3) + Text("Overview") + .font(.title3) + .fontWeight(.bold) + Spacer() + } + + HStack(spacing: 20) { + SummaryStatView( + icon: "house.fill", + value: "\(summary.totalResidences)", + label: "Properties" + ) + + SummaryStatView( + icon: "list.bullet", + value: "\(summary.totalTasks)", + label: "Total Tasks" + ) + } + + Divider() + + HStack(spacing: 20) { + SummaryStatView( + icon: "calendar", + value: "\(summary.tasksDueNextWeek)", + label: "Due This Week" + ) + + SummaryStatView( + icon: "calendar.badge.clock", + value: "\(summary.tasksDueNextMonth)", + label: "Due This Month" + ) + } + } + .padding(20) + .background(Color.blue.opacity(0.1)) + .cornerRadius(16) + } +} + +#Preview { + SummaryCard(summary: MyResidencesSummary( + totalResidences: 3, + totalTasks: 12, + tasksDueNextWeek: 4, + tasksDueNextMonth: 8 + )) + .padding() +} diff --git a/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift b/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift new file mode 100644 index 0000000..eb4e808 --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/SummaryStatView.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct SummaryStatView: View { + let icon: String + let value: String + let label: String + + var body: some View { + VStack(spacing: 8) { + Image(systemName: icon) + .font(.title3) + .foregroundColor(.blue) + + Text(value) + .font(.title2) + .fontWeight(.bold) + + Text(label) + .font(.caption) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .frame(maxWidth: .infinity) + } +} + +#Preview { + HStack(spacing: 20) { + SummaryStatView( + icon: "house.fill", + value: "3", + label: "Properties" + ) + + SummaryStatView( + icon: "list.bullet", + value: "12", + label: "Total Tasks" + ) + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift b/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift new file mode 100644 index 0000000..c2701ef --- /dev/null +++ b/iosApp/iosApp/Subviews/Residence/TaskStatChip.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct TaskStatChip: View { + let icon: String + let value: String + let label: String + let color: Color + + var body: some View { + HStack(spacing: 4) { + Image(systemName: icon) + .font(.caption) + .foregroundColor(color) + + Text(value) + .font(.subheadline) + .fontWeight(.bold) + .foregroundColor(color) + + Text(label) + .font(.caption) + .foregroundColor(.secondary) + } + } +} + +#Preview { + VStack(spacing: 16) { + TaskStatChip( + icon: "list.bullet", + value: "10", + label: "Tasks", + color: .blue + ) + + TaskStatChip( + icon: "checkmark.circle.fill", + value: "3", + label: "Done", + color: .green + ) + + TaskStatChip( + icon: "clock.fill", + value: "5", + label: "Pending", + color: .orange + ) + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Task/EmptyTasksView.swift b/iosApp/iosApp/Subviews/Task/EmptyTasksView.swift new file mode 100644 index 0000000..a9fddd3 --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/EmptyTasksView.swift @@ -0,0 +1,24 @@ +import SwiftUI + +struct EmptyTasksView: View { + var body: some View { + VStack(spacing: 12) { + Image(systemName: "checkmark.circle") + .font(.system(size: 48)) + .foregroundColor(.gray.opacity(0.5)) + + Text("No tasks yet") + .font(.subheadline) + .foregroundColor(.secondary) + } + .frame(maxWidth: .infinity) + .padding(32) + .background(Color(.systemBackground)) + .cornerRadius(12) + } +} + +#Preview { + EmptyTasksView() + .padding() +} diff --git a/iosApp/iosApp/Subviews/Task/PriorityBadge.swift b/iosApp/iosApp/Subviews/Task/PriorityBadge.swift new file mode 100644 index 0000000..8e647a9 --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/PriorityBadge.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct PriorityBadge: View { + let priority: String + + var body: some View { + HStack(spacing: 4) { + Image(systemName: "exclamationmark.circle.fill") + .font(.caption2) + + Text(priority.capitalized) + .font(.caption) + .fontWeight(.medium) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(priorityColor.opacity(0.2)) + .foregroundColor(priorityColor) + .cornerRadius(6) + } + + private var priorityColor: Color { + switch priority.lowercased() { + case "high": return .red + case "medium": return .orange + case "low": return .green + default: return .gray + } + } +} + +#Preview { + VStack(spacing: 12) { + PriorityBadge(priority: "high") + PriorityBadge(priority: "medium") + PriorityBadge(priority: "low") + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Task/StatusBadge.swift b/iosApp/iosApp/Subviews/Task/StatusBadge.swift new file mode 100644 index 0000000..801fa72 --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/StatusBadge.swift @@ -0,0 +1,42 @@ +import SwiftUI + +struct StatusBadge: View { + let status: String + + var body: some View { + Text(formatStatus(status)) + .font(.caption) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(statusColor.opacity(0.2)) + .foregroundColor(statusColor) + .cornerRadius(6) + } + + private func formatStatus(_ status: String) -> String { + switch status { + case "in_progress": return "In Progress" + default: return status.capitalized + } + } + + private var statusColor: Color { + switch status { + case "completed": return .green + case "in_progress": return .blue + case "pending": return .orange + case "cancelled": return .red + default: return .gray + } + } +} + +#Preview { + VStack(spacing: 12) { + StatusBadge(status: "pending") + StatusBadge(status: "in_progress") + StatusBadge(status: "completed") + StatusBadge(status: "cancelled") + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Task/TaskCard.swift b/iosApp/iosApp/Subviews/Task/TaskCard.swift new file mode 100644 index 0000000..010660a --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/TaskCard.swift @@ -0,0 +1,147 @@ +import SwiftUI +import ComposeApp + +struct TaskCard: View { + let task: TaskDetail + let onEdit: () -> Void + let onCancel: (() -> Void)? + let onUncancel: (() -> Void)? + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + VStack(alignment: .leading, spacing: 4) { + Text(task.title) + .font(.headline) + .foregroundColor(.primary) + + if let status = task.status { + StatusBadge(status: status.name) + } + } + + Spacer() + + PriorityBadge(priority: task.priority.name) + } + + if let description = task.description_, !description.isEmpty { + Text(description) + .font(.subheadline) + .foregroundColor(.secondary) + .lineLimit(2) + } + + HStack { + Label(task.frequency.displayName, systemImage: "repeat") + .font(.caption) + .foregroundColor(.secondary) + + Spacer() + + Label(formatDate(task.dueDate), systemImage: "calendar") + .font(.caption) + .foregroundColor(.secondary) + } + + if task.completions.count > 0 { + Divider() + + HStack { + Image(systemName: "checkmark.circle") + .foregroundColor(.green) + Text("Completed \(task.completions.count) time\(task.completions.count == 1 ? "" : "s")") + .font(.caption) + .foregroundColor(.secondary) + } + } + + if task.showCompletedButton { + Button(action: {}) { + HStack { + Image(systemName: "checkmark.circle.fill") + .resizable() + .frame(width: 20, height: 20) + Text("Complete Task") + .font(.title3.weight(.semibold)) + } + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .cornerRadius(12) + } + + HStack(spacing: 8) { + Button(action: onEdit) { + Label("Edit", systemImage: "pencil") + .font(.subheadline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + + if let onCancel = onCancel { + Button(action: onCancel) { + Label("Cancel", systemImage: "xmark.circle") + .font(.subheadline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.bordered) + .tint(.red) + } else if let onUncancel = onUncancel { + Button(action: onUncancel) { + Label("Restore", systemImage: "arrow.uturn.backward") + .font(.subheadline) + .frame(maxWidth: .infinity) + } + .buttonStyle(.borderedProminent) + .tint(.blue) + } + } + } + .padding(16) + .background(Color(.systemBackground)) + .cornerRadius(12) + .shadow(color: Color.black.opacity(0.05), radius: 3, x: 0, y: 2) + } + + private func formatDate(_ dateString: String) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd" + if let date = formatter.date(from: dateString) { + formatter.dateStyle = .medium + return formatter.string(from: date) + } + return dateString + } +} + +#Preview { + VStack(spacing: 16) { + TaskCard( + task: TaskDetail( + id: 1, + residence: 1, + title: "Clean Gutters", + description: "Remove all debris from gutters", + category: TaskCategory(id: 1, name: "maintenance", description: ""), + priority: TaskPriority(id: 2, name: "medium", displayName: "", description: ""), + frequency: TaskFrequency(id: 1, name: "monthly", displayName: "30"), + status: TaskStatus(id: 1, name: "pending", displayName: "", description: ""), + dueDate: "2024-12-15", + estimatedCost: "150.00", + actualCost: nil, + notes: nil, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + nextScheduledDate: nil, + showCompletedButton: true, + completions: [] + ), + onEdit: {}, + onCancel: {}, + onUncancel: nil + ) + } + .padding() + .background(Color(.systemGroupedBackground)) +} diff --git a/iosApp/iosApp/Subviews/Task/TaskPill.swift b/iosApp/iosApp/Subviews/Task/TaskPill.swift new file mode 100644 index 0000000..7980fe6 --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/TaskPill.swift @@ -0,0 +1,32 @@ +import SwiftUI + +struct TaskPill: View { + let count: Int32 + let label: String + let color: Color + + var body: some View { + HStack(spacing: 4) { + Text("\(count)") + .font(.caption) + .fontWeight(.bold) + + Text(label) + .font(.caption2) + } + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(color.opacity(0.2)) + .foregroundColor(color) + .cornerRadius(8) + } +} + +#Preview { + HStack(spacing: 8) { + TaskPill(count: 12, label: "Total", color: .blue) + TaskPill(count: 5, label: "Pending", color: .orange) + TaskPill(count: 3, label: "Done", color: .green) + } + .padding() +} diff --git a/iosApp/iosApp/Subviews/Task/TasksSection.swift b/iosApp/iosApp/Subviews/Task/TasksSection.swift new file mode 100644 index 0000000..b2a62fc --- /dev/null +++ b/iosApp/iosApp/Subviews/Task/TasksSection.swift @@ -0,0 +1,113 @@ +import SwiftUI +import ComposeApp + +struct TasksSection: View { + let tasksResponse: TasksByResidenceResponse + @Binding var showCancelledTasks: Bool + let onEditTask: (TaskDetail) -> Void + let onCancelTask: (TaskDetail) -> Void + let onUncancelTask: (TaskDetail) -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Text("Tasks") + .font(.title2) + .fontWeight(.bold) + + Spacer() + + HStack(spacing: 8) { + TaskPill(count: tasksResponse.summary.total, label: "Total", color: .blue) + TaskPill(count: tasksResponse.summary.pending, label: "Pending", color: .orange) + TaskPill(count: tasksResponse.summary.completed, label: "Done", color: .green) + } + } + + if tasksResponse.tasks.isEmpty && tasksResponse.cancelledTasks.isEmpty { + EmptyTasksView() + } else { + ForEach(tasksResponse.tasks, id: \.id) { task in + TaskCard( + task: task, + onEdit: { onEditTask(task) }, + onCancel: { onCancelTask(task) }, + onUncancel: nil + ) + } + + if !tasksResponse.cancelledTasks.isEmpty { + VStack(alignment: .leading, spacing: 12) { + HStack { + Label("Cancelled Tasks (\(tasksResponse.cancelledTasks.count))", systemImage: "xmark.circle") + .font(.headline) + .foregroundColor(.red) + + Spacer() + + Button(showCancelledTasks ? "Hide" : "Show") { + showCancelledTasks.toggle() + } + .font(.subheadline) + } + .padding(.top, 8) + + if showCancelledTasks { + ForEach(tasksResponse.cancelledTasks, id: \.id) { task in + TaskCard( + task: task, + onEdit: { onEditTask(task) }, + onCancel: nil, + onUncancel: { onUncancelTask(task) } + ) + } + } + } + } + } + } + } +} + +#Preview { + TasksSection( + tasksResponse: TasksByResidenceResponse( + residenceId: "1", + summary: TaskSummary( + total: 3, + completed: 1, + pending: 2, + inProgress: 0, + overdue: 1 + ), + tasks: [ + TaskDetail( + id: 1, + residence: 1, + title: "Clean Gutters", + description: "Remove all debris", + category: TaskCategory(id: 1, name: "maintenance", description: "General upkeep tasks"), + priority: TaskPriority(id: 2, name: "medium", displayName: "Medium", description: "Standard priority"), + frequency: TaskFrequency(id: 1, name: "monthly", displayName: "Monthly"), + status: TaskStatus(id: 1, name: "pending", displayName: "Pending", description: "Awaiting completion"), + dueDate: "2024-12-15", + estimatedCost: "150.00", + actualCost: nil, + notes: nil, + createdAt: "2024-01-01T00:00:00Z", + updatedAt: "2024-01-01T00:00:00Z", + nextScheduledDate: nil, + showCompletedButton: true, + completions: [] + ) + ], + cancelledTasks: [] + ), + showCancelledTasks: .constant(true), + onEditTask: { _ in }, + onCancelTask: { _ in }, + onUncancelTask: { _ in } + ) + .padding() +} +