Refactor iOS and Android views into separate files
Organized view components by extracting composables and views into separate files following single responsibility principle. iOS Changes: - Split MainTabView → extracted ProfileTabView - Split CompleteTaskView → extracted ImageThumbnailView, CameraPickerView - Split ManageUsersView → extracted ShareCodeCard, UserListItem - Consolidated task action buttons into single TaskActionButtons.swift file - Split HomeScreenView → extracted OverviewCard, StatView, HomeNavigationCard - Split AllTasksView → extracted DynamicTaskColumnView, DynamicTaskCard - Split ContentView → extracted ComposeView, CustomView Android Changes: - Split ResetPasswordScreen → extracted RequirementItem component - Split TasksScreen → extracted TaskPill component - Created TaskDisplayUtils for shared helper functions (getIconFromName, hexToColor) All extracted components properly organized in: - iOS: Subviews/Common, Subviews/Task, Subviews/Residence, Profile - Android: ui/components/auth, ui/components/task, ui/utils 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
package com.mycrib.android.ui.components.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Circle
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun RequirementItem(text: String, satisfied: Boolean) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
|
||||
contentDescription = null,
|
||||
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.mycrib.android.ui.components.task
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun TaskPill(
|
||||
count: Int,
|
||||
label: String,
|
||||
color: Color
|
||||
) {
|
||||
Surface(
|
||||
color = color.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.mycrib.android.ui.components.auth.AuthHeader
|
||||
import com.mycrib.android.ui.components.auth.RequirementItem
|
||||
import com.mycrib.android.ui.components.common.ErrorCard
|
||||
import com.mycrib.android.viewmodel.PasswordResetViewModel
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
@@ -254,23 +255,3 @@ fun ResetPasswordScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RequirementItem(text: String, satisfied: Boolean) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
if (satisfied) Icons.Default.CheckCircle else Icons.Default.Circle,
|
||||
contentDescription = null,
|
||||
tint = if (satisfied) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = if (satisfied) MaterialTheme.colorScheme.onSecondaryContainer else MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.mycrib.android.ui.components.CompleteTaskDialog
|
||||
import com.mycrib.android.ui.components.task.TaskCard
|
||||
import com.mycrib.android.ui.components.task.TaskPill
|
||||
import com.mycrib.android.ui.utils.getIconFromName
|
||||
import com.mycrib.android.ui.utils.hexToColor
|
||||
import com.mycrib.android.viewmodel.TaskCompletionViewModel
|
||||
import com.mycrib.android.viewmodel.TaskViewModel
|
||||
import com.mycrib.shared.network.ApiResult
|
||||
@@ -271,108 +274,3 @@ fun TasksScreen(
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TaskPill(
|
||||
count: Int,
|
||||
label: String,
|
||||
color: androidx.compose.ui.graphics.Color
|
||||
) {
|
||||
Surface(
|
||||
color = color.copy(alpha = 0.1f),
|
||||
shape = MaterialTheme.shapes.small
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = count.toString(),
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = color
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = color
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getColumnColor(columnName: String): androidx.compose.ui.graphics.Color {
|
||||
return when (columnName) {
|
||||
"upcoming_tasks" -> MaterialTheme.colorScheme.primary
|
||||
"in_progress_tasks" -> MaterialTheme.colorScheme.tertiary
|
||||
"done_tasks" -> MaterialTheme.colorScheme.secondary
|
||||
"archived_tasks" -> MaterialTheme.colorScheme.outline
|
||||
else -> MaterialTheme.colorScheme.primary // Default color for unknown columns
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getColumnIcon(columnName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
||||
return when (columnName) {
|
||||
"upcoming_tasks" -> Icons.Default.CalendarToday
|
||||
"in_progress_tasks" -> Icons.Default.PlayArrow
|
||||
"done_tasks" -> Icons.Default.CheckCircle
|
||||
"archived_tasks" -> Icons.Default.Archive
|
||||
else -> Icons.Default.List // Default icon for unknown columns
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert icon name string to ImageVector
|
||||
*/
|
||||
private fun getIconFromName(iconName: String): androidx.compose.ui.graphics.vector.ImageVector {
|
||||
return when (iconName) {
|
||||
"CalendarToday" -> Icons.Default.CalendarToday
|
||||
"PlayCircle" -> Icons.Default.PlayCircle
|
||||
"PlayArrow" -> Icons.Default.PlayArrow
|
||||
"CheckCircle" -> Icons.Default.CheckCircle
|
||||
"Archive" -> Icons.Default.Archive
|
||||
"List" -> Icons.Default.List
|
||||
"Unarchive" -> Icons.Default.Unarchive
|
||||
else -> Icons.Default.List // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert hex color string to Color
|
||||
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
||||
* Platform-independent implementation
|
||||
*/
|
||||
private fun hexToColor(hex: String): androidx.compose.ui.graphics.Color {
|
||||
val cleanHex = hex.removePrefix("#")
|
||||
return try {
|
||||
when (cleanHex.length) {
|
||||
3 -> {
|
||||
// RGB format - expand to RRGGBB
|
||||
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
||||
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
||||
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||
}
|
||||
6 -> {
|
||||
// RRGGBB format
|
||||
val r = cleanHex.substring(0, 2).toInt(16)
|
||||
val g = cleanHex.substring(2, 4).toInt(16)
|
||||
val b = cleanHex.substring(4, 6).toInt(16)
|
||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||
}
|
||||
8 -> {
|
||||
// AARRGGBB format
|
||||
val a = cleanHex.substring(0, 2).toInt(16)
|
||||
val r = cleanHex.substring(2, 4).toInt(16)
|
||||
val g = cleanHex.substring(4, 6).toInt(16)
|
||||
val b = cleanHex.substring(6, 8).toInt(16)
|
||||
androidx.compose.ui.graphics.Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
||||
}
|
||||
else -> androidx.compose.ui.graphics.Color.Gray // Default fallback
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
androidx.compose.ui.graphics.Color.Gray // Fallback on parse error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.mycrib.android.ui.utils
|
||||
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
|
||||
/**
|
||||
* Helper function to convert icon name string to ImageVector
|
||||
*/
|
||||
fun getIconFromName(iconName: String): ImageVector {
|
||||
return when (iconName) {
|
||||
"CalendarToday" -> Icons.Default.CalendarToday
|
||||
"PlayCircle" -> Icons.Default.PlayCircle
|
||||
"PlayArrow" -> Icons.Default.PlayArrow
|
||||
"CheckCircle" -> Icons.Default.CheckCircle
|
||||
"Archive" -> Icons.Default.Archive
|
||||
"List" -> Icons.Default.List
|
||||
"Unarchive" -> Icons.Default.Unarchive
|
||||
else -> Icons.Default.List // Default fallback
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to convert hex color string to Color
|
||||
* Supports formats: #RGB, #RRGGBB, #AARRGGBB
|
||||
* Platform-independent implementation
|
||||
*/
|
||||
fun hexToColor(hex: String): Color {
|
||||
val cleanHex = hex.removePrefix("#")
|
||||
return try {
|
||||
when (cleanHex.length) {
|
||||
3 -> {
|
||||
// RGB format - expand to RRGGBB
|
||||
val r = cleanHex[0].toString().repeat(2).toInt(16)
|
||||
val g = cleanHex[1].toString().repeat(2).toInt(16)
|
||||
val b = cleanHex[2].toString().repeat(2).toInt(16)
|
||||
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||
}
|
||||
6 -> {
|
||||
// RRGGBB format
|
||||
val r = cleanHex.substring(0, 2).toInt(16)
|
||||
val g = cleanHex.substring(2, 4).toInt(16)
|
||||
val b = cleanHex.substring(4, 6).toInt(16)
|
||||
Color(red = r / 255f, green = g / 255f, blue = b / 255f)
|
||||
}
|
||||
8 -> {
|
||||
// AARRGGBB format
|
||||
val a = cleanHex.substring(0, 2).toInt(16)
|
||||
val r = cleanHex.substring(2, 4).toInt(16)
|
||||
val g = cleanHex.substring(4, 6).toInt(16)
|
||||
val b = cleanHex.substring(6, 8).toInt(16)
|
||||
Color(red = r / 255f, green = g / 255f, blue = b / 255f, alpha = a / 255f)
|
||||
}
|
||||
else -> Color.Gray // Default fallback
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Color.Gray // Fallback on parse error
|
||||
}
|
||||
}
|
||||
@@ -1,46 +1,9 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
MainViewControllerKt.MainViewController()
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||
|
||||
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
CustomView()
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
||||
struct CustomView: View {
|
||||
var body: some View {
|
||||
Text("Custom view")
|
||||
.task {
|
||||
await ViewModel().somethingRandom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewModel {
|
||||
func somethingRandom() async {
|
||||
TokenStorage().initialize(manager: TokenManager.init())
|
||||
// TokenStorage.initialize(TokenManager.getInstance())
|
||||
|
||||
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
|
||||
|
||||
api.deleteResidence(token: "token", id: 32) { result, error in
|
||||
if let error = error {
|
||||
print("Interop error: \(error)")
|
||||
return
|
||||
}
|
||||
guard let result = result else { return }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,104 +62,6 @@ struct HomeScreenView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct OverviewCard: View {
|
||||
let summary: OverallSummary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: "chart.bar.fill")
|
||||
.font(.title3)
|
||||
Text("Overview")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 40) {
|
||||
StatView(
|
||||
icon: "house.fill",
|
||||
value: "\(summary.totalResidences)",
|
||||
label: "Properties"
|
||||
)
|
||||
|
||||
StatView(
|
||||
icon: "list.bullet",
|
||||
value: "\(summary.totalTasks)",
|
||||
label: "Total Tasks"
|
||||
)
|
||||
|
||||
StatView(
|
||||
icon: "clock.fill",
|
||||
value: "\(summary.totalPending)",
|
||||
label: "Pending"
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(16)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
struct StatView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let label: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text(value)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeNavigationCard: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(.blue)
|
||||
.frame(width: 60)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
HomeScreenView()
|
||||
}
|
||||
|
||||
@@ -33,76 +33,6 @@ struct MainTabView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProfileTabView: View {
|
||||
@EnvironmentObject var loginViewModel: LoginViewModel
|
||||
@State private var showingProfileEdit = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("User Profile")
|
||||
.font(.headline)
|
||||
|
||||
Text("Manage your account")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section("Account") {
|
||||
Button(action: {
|
||||
showingProfileEdit = true
|
||||
}) {
|
||||
Label("Edit Profile", systemImage: "person.crop.circle")
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
NavigationLink(destination: Text("Notifications")) {
|
||||
Label("Notifications", systemImage: "bell")
|
||||
}
|
||||
|
||||
NavigationLink(destination: Text("Privacy")) {
|
||||
Label("Privacy", systemImage: "lock.shield")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: {
|
||||
loginViewModel.logout()
|
||||
}) {
|
||||
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("MyCrib")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Version 1.0.0")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Profile")
|
||||
.sheet(isPresented: $showingProfileEdit) {
|
||||
ProfileView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
MainTabView()
|
||||
}
|
||||
|
||||
71
iosApp/iosApp/Profile/ProfileTabView.swift
Normal file
71
iosApp/iosApp/Profile/ProfileTabView.swift
Normal file
@@ -0,0 +1,71 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ProfileTabView: View {
|
||||
@EnvironmentObject var loginViewModel: LoginViewModel
|
||||
@State private var showingProfileEdit = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
HStack {
|
||||
Image(systemName: "person.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("User Profile")
|
||||
.font(.headline)
|
||||
|
||||
Text("Manage your account")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
|
||||
Section("Account") {
|
||||
Button(action: {
|
||||
showingProfileEdit = true
|
||||
}) {
|
||||
Label("Edit Profile", systemImage: "person.crop.circle")
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
|
||||
NavigationLink(destination: Text("Notifications")) {
|
||||
Label("Notifications", systemImage: "bell")
|
||||
}
|
||||
|
||||
NavigationLink(destination: Text("Privacy")) {
|
||||
Label("Privacy", systemImage: "lock.shield")
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(action: {
|
||||
loginViewModel.logout()
|
||||
}) {
|
||||
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("MyCrib")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text("Version 1.0.0")
|
||||
.font(.caption2)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Profile")
|
||||
.sheet(isPresented: $showingProfileEdit) {
|
||||
ProfileView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,118 +155,6 @@ struct ManageUsersView: View {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Share Code Card
|
||||
struct ShareCodeCard: View {
|
||||
let shareCode: ResidenceShareCode?
|
||||
let residenceName: String
|
||||
let isGeneratingCode: Bool
|
||||
let onGenerateCode: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Share Code")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let shareCode = shareCode {
|
||||
Text(shareCode.code)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
} else {
|
||||
Text("No active code")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: onGenerateCode) {
|
||||
HStack {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
Text(shareCode != nil ? "New Code" : "Generate")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isGeneratingCode)
|
||||
}
|
||||
|
||||
if shareCode != nil {
|
||||
Text("Share this code with others to give them access to \(residenceName)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User List Item
|
||||
struct UserListItem: View {
|
||||
let user: ResidenceUser
|
||||
let isOwner: Bool
|
||||
let isPrimaryOwner: Bool
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(user.username)
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
|
||||
if isOwner {
|
||||
Text("Owner")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.blue)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
|
||||
if !user.email.isEmpty {
|
||||
Text(user.email)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
let fullName = [user.firstName, user.lastName]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: " ")
|
||||
|
||||
if !fullName.isEmpty {
|
||||
Text(fullName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isPrimaryOwner && !isOwner {
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true)
|
||||
}
|
||||
|
||||
38
iosApp/iosApp/Subviews/Common/CameraPickerView.swift
Normal file
38
iosApp/iosApp/Subviews/Common/CameraPickerView.swift
Normal file
@@ -0,0 +1,38 @@
|
||||
import SwiftUI
|
||||
|
||||
struct CameraPickerView: UIViewControllerRepresentable {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
let onImageCaptured: (UIImage) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = .camera
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
let parent: CameraPickerView
|
||||
|
||||
init(_ parent: CameraPickerView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let image = info[.originalImage] as? UIImage {
|
||||
parent.onImageCaptured(image)
|
||||
}
|
||||
parent.dismiss()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
parent.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
11
iosApp/iosApp/Subviews/Common/ComposeView.swift
Normal file
11
iosApp/iosApp/Subviews/Common/ComposeView.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct ComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
MainViewControllerKt.MainViewController()
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
|
||||
}
|
||||
28
iosApp/iosApp/Subviews/Common/CustomView.swift
Normal file
28
iosApp/iosApp/Subviews/Common/CustomView.swift
Normal file
@@ -0,0 +1,28 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct CustomView: View {
|
||||
var body: some View {
|
||||
Text("Custom view")
|
||||
.task {
|
||||
await ViewModel().somethingRandom()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ViewModel {
|
||||
func somethingRandom() async {
|
||||
TokenStorage().initialize(manager: TokenManager.init())
|
||||
// TokenStorage.initialize(TokenManager.getInstance())
|
||||
|
||||
let api = ResidenceApi(client: ApiClient_iosKt.createHttpClient())
|
||||
|
||||
api.deleteResidence(token: "token", id: 32) { result, error in
|
||||
if let error = error {
|
||||
print("Interop error: \(error)")
|
||||
return
|
||||
}
|
||||
guard let result = result else { return }
|
||||
}
|
||||
}
|
||||
}
|
||||
36
iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift
Normal file
36
iosApp/iosApp/Subviews/Common/HomeNavigationCard.swift
Normal file
@@ -0,0 +1,36 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeNavigationCard: View {
|
||||
let icon: String
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 16) {
|
||||
Image(systemName: icon)
|
||||
.font(.system(size: 36))
|
||||
.foregroundColor(.blue)
|
||||
.frame(width: 60)
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(title)
|
||||
.font(.title3)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Text(subtitle)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
|
||||
}
|
||||
}
|
||||
32
iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift
Normal file
32
iosApp/iosApp/Subviews/Common/ImageThumbnailView.swift
Normal file
@@ -0,0 +1,32 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ImageThumbnailView: View {
|
||||
let image: UIImage
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.strokeBorder(.quaternary, lineWidth: 1)
|
||||
}
|
||||
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.white)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(.black.opacity(0.6))
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
}
|
||||
}
|
||||
43
iosApp/iosApp/Subviews/Common/OverviewCard.swift
Normal file
43
iosApp/iosApp/Subviews/Common/OverviewCard.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
struct OverviewCard: View {
|
||||
let summary: OverallSummary
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 16) {
|
||||
HStack {
|
||||
Image(systemName: "chart.bar.fill")
|
||||
.font(.title3)
|
||||
Text("Overview")
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
Spacer()
|
||||
}
|
||||
|
||||
HStack(spacing: 40) {
|
||||
StatView(
|
||||
icon: "house.fill",
|
||||
value: "\(summary.totalResidences)",
|
||||
label: "Properties"
|
||||
)
|
||||
|
||||
StatView(
|
||||
icon: "list.bullet",
|
||||
value: "\(summary.totalTasks)",
|
||||
label: "Total Tasks"
|
||||
)
|
||||
|
||||
StatView(
|
||||
icon: "clock.fill",
|
||||
value: "\(summary.totalPending)",
|
||||
label: "Pending"
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(16)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
23
iosApp/iosApp/Subviews/Common/StatView.swift
Normal file
23
iosApp/iosApp/Subviews/Common/StatView.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
import SwiftUI
|
||||
|
||||
struct StatView: View {
|
||||
let icon: String
|
||||
let value: String
|
||||
let label: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: icon)
|
||||
.font(.title2)
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text(value)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text(label)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
53
iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift
Normal file
53
iosApp/iosApp/Subviews/Residence/ShareCodeCard.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
// MARK: - Share Code Card
|
||||
struct ShareCodeCard: View {
|
||||
let shareCode: ResidenceShareCode?
|
||||
let residenceName: String
|
||||
let isGeneratingCode: Bool
|
||||
let onGenerateCode: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text("Share Code")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
if let shareCode = shareCode {
|
||||
Text(shareCode.code)
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
} else {
|
||||
Text("No active code")
|
||||
.font(.body)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(action: onGenerateCode) {
|
||||
HStack {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
Text(shareCode != nil ? "New Code" : "Generate")
|
||||
}
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.disabled(isGeneratingCode)
|
||||
}
|
||||
|
||||
if shareCode != nil {
|
||||
Text("Share this code with others to give them access to \(residenceName)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(12)
|
||||
}
|
||||
}
|
||||
63
iosApp/iosApp/Subviews/Residence/UserListItem.swift
Normal file
63
iosApp/iosApp/Subviews/Residence/UserListItem.swift
Normal file
@@ -0,0 +1,63 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
// MARK: - User List Item
|
||||
struct UserListItem: View {
|
||||
let user: ResidenceUser
|
||||
let isOwner: Bool
|
||||
let isPrimaryOwner: Bool
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
HStack(alignment: .top, spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(user.username)
|
||||
.font(.body)
|
||||
.fontWeight(.medium)
|
||||
|
||||
if isOwner {
|
||||
Text("Owner")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.blue)
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 2)
|
||||
.background(Color.blue.opacity(0.1))
|
||||
.cornerRadius(4)
|
||||
}
|
||||
}
|
||||
|
||||
if !user.email.isEmpty {
|
||||
Text(user.email)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
let fullName = [user.firstName, user.lastName]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: " ")
|
||||
|
||||
if !fullName.isEmpty {
|
||||
Text(fullName)
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if isPrimaryOwner && !isOwner {
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemBackground))
|
||||
.cornerRadius(12)
|
||||
.padding(.horizontal)
|
||||
}
|
||||
}
|
||||
159
iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift
Normal file
159
iosApp/iosApp/Subviews/Task/DynamicTaskCard.swift
Normal file
@@ -0,0 +1,159 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
/// Task card that dynamically renders buttons based on the column's button types
|
||||
struct DynamicTaskCard: View {
|
||||
let task: TaskDetail
|
||||
let buttonTypes: [String]
|
||||
let onEdit: () -> Void
|
||||
let onCancel: () -> Void
|
||||
let onUncancel: () -> Void
|
||||
let onMarkInProgress: () -> Void
|
||||
let onComplete: () -> Void
|
||||
let onArchive: () -> Void
|
||||
let onUnarchive: () -> 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()
|
||||
|
||||
if let due_date = task.dueDate {
|
||||
Label(formatDate(due_date), systemImage: "calendar")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if task.completions.count > 0 {
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Completions (\(task.completions.count))")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render buttons based on buttonTypes array
|
||||
VStack(spacing: 8) {
|
||||
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
||||
renderButton(for: buttonType)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func renderButton(for buttonType: String) -> some View {
|
||||
switch buttonType {
|
||||
case "mark_in_progress":
|
||||
MarkInProgressButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onMarkInProgress,
|
||||
onError: { error in
|
||||
print("Error marking in progress: \(error)")
|
||||
}
|
||||
)
|
||||
case "complete":
|
||||
CompleteTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onComplete,
|
||||
onError: { error in
|
||||
print("Error completing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "edit":
|
||||
EditTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onEdit,
|
||||
onError: { error in
|
||||
print("Error editing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "cancel":
|
||||
CancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onCancel,
|
||||
onError: { error in
|
||||
print("Error cancelling task: \(error)")
|
||||
}
|
||||
)
|
||||
case "uncancel":
|
||||
UncancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUncancel,
|
||||
onError: { error in
|
||||
print("Error restoring task: \(error)")
|
||||
}
|
||||
)
|
||||
case "archive":
|
||||
ArchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onArchive,
|
||||
onError: { error in
|
||||
print("Error archiving task: \(error)")
|
||||
}
|
||||
)
|
||||
case "unarchive":
|
||||
UnarchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUnarchive,
|
||||
onError: { error in
|
||||
print("Error unarchiving task: \(error)")
|
||||
}
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
81
iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift
Normal file
81
iosApp/iosApp/Subviews/Task/DynamicTaskColumnView.swift
Normal file
@@ -0,0 +1,81 @@
|
||||
import SwiftUI
|
||||
import ComposeApp
|
||||
|
||||
/// Dynamic task column view that adapts based on the column configuration
|
||||
struct DynamicTaskColumnView: View {
|
||||
let column: TaskColumn
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
// Get icon from API response, with fallback
|
||||
private var columnIcon: String {
|
||||
column.icons["ios"] ?? "list.bullet"
|
||||
}
|
||||
|
||||
private var columnColor: Color {
|
||||
Color(hex: column.color) ?? .primary
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
// Header
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: columnIcon)
|
||||
.font(.headline)
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Text(column.displayName)
|
||||
.font(.headline)
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(column.count)")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(columnColor)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
if column.tasks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: columnIcon)
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(columnColor.opacity(0.3))
|
||||
|
||||
Text("No tasks")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
ForEach(column.tasks, id: \.id) { task in
|
||||
DynamicTaskCard(
|
||||
task: task,
|
||||
buttonTypes: column.buttonTypes,
|
||||
onEdit: { onEditTask(task) },
|
||||
onCancel: { onCancelTask(task.id) },
|
||||
onUncancel: { onUncancelTask(task.id) },
|
||||
onMarkInProgress: { onMarkInProgress(task.id) },
|
||||
onComplete: { onCompleteTask(task) },
|
||||
onArchive: { onArchiveTask(task.id) },
|
||||
onUnarchive: { onUnarchiveTask(task.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,242 +198,6 @@ struct AllTasksView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Dynamic task column view that adapts based on the column configuration
|
||||
struct DynamicTaskColumnView: View {
|
||||
let column: TaskColumn
|
||||
let onEditTask: (TaskDetail) -> Void
|
||||
let onCancelTask: (Int32) -> Void
|
||||
let onUncancelTask: (Int32) -> Void
|
||||
let onMarkInProgress: (Int32) -> Void
|
||||
let onCompleteTask: (TaskDetail) -> Void
|
||||
let onArchiveTask: (Int32) -> Void
|
||||
let onUnarchiveTask: (Int32) -> Void
|
||||
|
||||
// Get icon from API response, with fallback
|
||||
private var columnIcon: String {
|
||||
column.icons["ios"] ?? "list.bullet"
|
||||
}
|
||||
|
||||
private var columnColor: Color {
|
||||
Color(hex: column.color) ?? .primary
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
ScrollView {
|
||||
VStack(spacing: 16) {
|
||||
// Header
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: columnIcon)
|
||||
.font(.headline)
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Text(column.displayName)
|
||||
.font(.headline)
|
||||
.foregroundColor(columnColor)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(column.count)")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(columnColor)
|
||||
.cornerRadius(12)
|
||||
}
|
||||
|
||||
if column.tasks.isEmpty {
|
||||
VStack(spacing: 8) {
|
||||
Image(systemName: columnIcon)
|
||||
.font(.system(size: 40))
|
||||
.foregroundColor(columnColor.opacity(0.3))
|
||||
|
||||
Text("No tasks")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.top, 40)
|
||||
} else {
|
||||
ForEach(column.tasks, id: \.id) { task in
|
||||
DynamicTaskCard(
|
||||
task: task,
|
||||
buttonTypes: column.buttonTypes,
|
||||
onEdit: { onEditTask(task) },
|
||||
onCancel: { onCancelTask(task.id) },
|
||||
onUncancel: { onUncancelTask(task.id) },
|
||||
onMarkInProgress: { onMarkInProgress(task.id) },
|
||||
onComplete: { onCompleteTask(task) },
|
||||
onArchive: { onArchiveTask(task.id) },
|
||||
onUnarchive: { onUnarchiveTask(task.id) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Task card that dynamically renders buttons based on the column's button types
|
||||
struct DynamicTaskCard: View {
|
||||
let task: TaskDetail
|
||||
let buttonTypes: [String]
|
||||
let onEdit: () -> Void
|
||||
let onCancel: () -> Void
|
||||
let onUncancel: () -> Void
|
||||
let onMarkInProgress: () -> Void
|
||||
let onComplete: () -> Void
|
||||
let onArchive: () -> Void
|
||||
let onUnarchive: () -> 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()
|
||||
|
||||
if let due_date = task.dueDate {
|
||||
Label(formatDate(due_date), systemImage: "calendar")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
if task.completions.count > 0 {
|
||||
Divider()
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.foregroundColor(.green)
|
||||
Text("Completions (\(task.completions.count))")
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
|
||||
ForEach(task.completions, id: \.id) { completion in
|
||||
CompletionCardView(completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render buttons based on buttonTypes array
|
||||
VStack(spacing: 8) {
|
||||
ForEach(Array(buttonTypes.enumerated()), id: \.offset) { index, buttonType in
|
||||
renderButton(for: buttonType)
|
||||
}
|
||||
}
|
||||
}
|
||||
.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
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func renderButton(for buttonType: String) -> some View {
|
||||
switch buttonType {
|
||||
case "mark_in_progress":
|
||||
MarkInProgressButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onMarkInProgress,
|
||||
onError: { error in
|
||||
print("Error marking in progress: \(error)")
|
||||
}
|
||||
)
|
||||
case "complete":
|
||||
CompleteTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onComplete,
|
||||
onError: { error in
|
||||
print("Error completing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "edit":
|
||||
EditTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onEdit,
|
||||
onError: { error in
|
||||
print("Error editing task: \(error)")
|
||||
}
|
||||
)
|
||||
case "cancel":
|
||||
CancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onCancel,
|
||||
onError: { error in
|
||||
print("Error cancelling task: \(error)")
|
||||
}
|
||||
)
|
||||
case "uncancel":
|
||||
UncancelTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUncancel,
|
||||
onError: { error in
|
||||
print("Error restoring task: \(error)")
|
||||
}
|
||||
)
|
||||
case "archive":
|
||||
ArchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onArchive,
|
||||
onError: { error in
|
||||
print("Error archiving task: \(error)")
|
||||
}
|
||||
)
|
||||
case "unarchive":
|
||||
UnarchiveTaskButton(
|
||||
taskId: task.id,
|
||||
onCompletion: onUnarchive,
|
||||
onError: { error in
|
||||
print("Error unarchiving task: \(error)")
|
||||
}
|
||||
)
|
||||
default:
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to apply corner radius to specific corners
|
||||
extension View {
|
||||
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
|
||||
|
||||
@@ -310,72 +310,3 @@ extension KotlinByteArray {
|
||||
}
|
||||
}
|
||||
|
||||
// Image Thumbnail View Component
|
||||
struct ImageThumbnailView: View {
|
||||
let image: UIImage
|
||||
let onRemove: () -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.strokeBorder(.quaternary, lineWidth: 1)
|
||||
}
|
||||
|
||||
Button(action: onRemove) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.white)
|
||||
.background {
|
||||
Circle()
|
||||
.fill(.black.opacity(0.6))
|
||||
.padding(4)
|
||||
}
|
||||
}
|
||||
.offset(x: 8, y: -8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Camera Picker View Component
|
||||
struct CameraPickerView: UIViewControllerRepresentable {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
let onImageCaptured: (UIImage) -> Void
|
||||
|
||||
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||
let picker = UIImagePickerController()
|
||||
picker.sourceType = .camera
|
||||
picker.delegate = context.coordinator
|
||||
return picker
|
||||
}
|
||||
|
||||
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||
let parent: CameraPickerView
|
||||
|
||||
init(_ parent: CameraPickerView) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
|
||||
if let image = info[.originalImage] as? UIImage {
|
||||
parent.onImageCaptured(image)
|
||||
}
|
||||
parent.dismiss()
|
||||
}
|
||||
|
||||
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||
parent.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user