Add Warm Organic design system to iOS app
- Add OrganicDesign.swift with reusable components: - WarmGradientBackground, OrganicBlobShape, GrainTexture - OrganicDivider, OrganicCardBackground, NaturalShadow modifier - OrganicSpacing constants (cozy, comfortable, spacious, airy) - Update high-priority screens with organic styling: - LoginView: hero glow, organic card background, rounded fonts - ResidenceDetailView, ResidencesListView: warm backgrounds - ResidenceCard, SummaryCard, PropertyHeaderCard: organic cards - TaskCard: metadata pills, secondary buttons, card background - TaskFormView: organic loading overlay, templates button - CompletionHistorySheet: organic loading/error/empty states - ProfileView, NotificationPreferencesView, ThemeSelectionView - Update task badges with icons and capsule styling: - PriorityBadge: priority-specific icons - StatusBadge: status-specific icons - Fix TaskCard isOverdue error using DateUtils.isOverdue() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,15 +10,20 @@ struct CompletionHistorySheet: View {
|
||||
|
||||
var body: some View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if viewModel.isLoadingCompletions {
|
||||
loadingView
|
||||
} else if let error = viewModel.completionsError {
|
||||
errorView(error)
|
||||
} else if viewModel.completions.isEmpty {
|
||||
emptyView
|
||||
} else {
|
||||
completionsList
|
||||
ZStack {
|
||||
WarmGradientBackground()
|
||||
.ignoresSafeArea()
|
||||
|
||||
Group {
|
||||
if viewModel.isLoadingCompletions {
|
||||
loadingView
|
||||
} else if let error = viewModel.completionsError {
|
||||
errorView(error)
|
||||
} else if viewModel.completions.isEmpty {
|
||||
emptyView
|
||||
} else {
|
||||
completionsList
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle(L10n.Tasks.completionHistory)
|
||||
@@ -28,9 +33,9 @@ struct CompletionHistorySheet: View {
|
||||
Button(L10n.Common.done) {
|
||||
isPresented = false
|
||||
}
|
||||
.font(.system(size: 16, weight: .semibold, design: .rounded))
|
||||
}
|
||||
}
|
||||
.background(Color.appBackgroundPrimary)
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.loadCompletions(taskId: taskId)
|
||||
@@ -43,56 +48,80 @@ struct CompletionHistorySheet: View {
|
||||
// MARK: - Subviews
|
||||
|
||||
private var loadingView: some View {
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: Color.appPrimary))
|
||||
.scaleEffect(1.5)
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 64, height: 64)
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: Color.appPrimary))
|
||||
.scaleEffect(1.2)
|
||||
}
|
||||
Text(L10n.Tasks.loadingCompletions)
|
||||
.font(.subheadline)
|
||||
.font(.system(size: 15, weight: .medium, design: .rounded))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private func errorView(_ error: String) -> some View {
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appError)
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appError.opacity(0.1))
|
||||
.frame(width: 80, height: 80)
|
||||
Image(systemName: "exclamationmark.triangle.fill")
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundColor(Color.appError)
|
||||
}
|
||||
|
||||
Text(L10n.Tasks.failedToLoad)
|
||||
.font(.headline)
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text(error)
|
||||
.font(.subheadline)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.multilineTextAlignment(.center)
|
||||
.padding(.horizontal)
|
||||
.padding(.horizontal, OrganicSpacing.spacious)
|
||||
|
||||
Button(action: {
|
||||
viewModel.loadCompletions(taskId: taskId)
|
||||
}) {
|
||||
Label(L10n.Common.retry, systemImage: "arrow.clockwise")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "arrow.clockwise")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
Text(L10n.Common.retry)
|
||||
.font(.system(size: 15, weight: .semibold, design: .rounded))
|
||||
}
|
||||
.foregroundColor(Color.appTextOnPrimary)
|
||||
.padding(.horizontal, OrganicSpacing.comfortable)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.appPrimary)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.padding(.top, AppSpacing.sm)
|
||||
.padding(.top, 8)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
|
||||
private var emptyView: some View {
|
||||
VStack(spacing: AppSpacing.md) {
|
||||
Image(systemName: "checkmark.circle")
|
||||
.font(.system(size: 48))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.5))
|
||||
VStack(spacing: OrganicSpacing.comfortable) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appTextSecondary.opacity(0.08))
|
||||
.frame(width: 80, height: 80)
|
||||
Image(systemName: "checkmark.circle")
|
||||
.font(.system(size: 36, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary.opacity(0.5))
|
||||
}
|
||||
|
||||
Text(L10n.Tasks.noCompletionsYet)
|
||||
.font(.headline)
|
||||
.font(.system(size: 18, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
Text(L10n.Tasks.notCompleted)
|
||||
.font(.subheadline)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
@@ -100,30 +129,46 @@ struct CompletionHistorySheet: View {
|
||||
|
||||
private var completionsList: some View {
|
||||
ScrollView {
|
||||
VStack(spacing: AppSpacing.sm) {
|
||||
VStack(spacing: OrganicSpacing.cozy) {
|
||||
// Task title header
|
||||
HStack {
|
||||
Image(systemName: "doc.text")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.12))
|
||||
.frame(width: 36, height: 36)
|
||||
Image(systemName: "doc.text")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
|
||||
Text(taskTitle)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: 15, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
.lineLimit(2)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("\(viewModel.completions.count) \(viewModel.completions.count == 1 ? L10n.Tasks.completion : L10n.Tasks.completions)")
|
||||
.font(.caption)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color.appTextSecondary.opacity(0.1))
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.padding(OrganicSpacing.cozy)
|
||||
.background(Color.appBackgroundSecondary)
|
||||
.cornerRadius(AppRadius.md)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous))
|
||||
.naturalShadow(.subtle)
|
||||
|
||||
// Completions list
|
||||
ForEach(viewModel.completions, id: \.id) { completion in
|
||||
CompletionHistoryCard(completion: completion)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(OrganicSpacing.cozy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -134,20 +179,20 @@ struct CompletionHistoryCard: View {
|
||||
@State private var showPhotoSheet = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: AppSpacing.sm) {
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
// Header with date and completed by
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(DateUtils.formatDateTimeWithTime(completion.completionDate))
|
||||
.font(.headline)
|
||||
.font(.system(size: 16, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
if let completedBy = completion.completedByName, !completedBy.isEmpty {
|
||||
HStack(spacing: 4) {
|
||||
HStack(spacing: 5) {
|
||||
Image(systemName: "person.fill")
|
||||
.font(.caption2)
|
||||
.font(.system(size: 10, weight: .medium))
|
||||
Text("\(L10n.Tasks.completedByName) \(completedBy)")
|
||||
.font(.caption)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
}
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
@@ -157,39 +202,48 @@ struct CompletionHistoryCard: View {
|
||||
|
||||
// Rating badge
|
||||
if let rating = completion.rating {
|
||||
HStack(spacing: 2) {
|
||||
HStack(spacing: 4) {
|
||||
Image(systemName: "star.fill")
|
||||
.font(.caption)
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
Text("\(rating)")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.bold)
|
||||
.font(.system(size: 13, weight: .bold, design: .rounded))
|
||||
}
|
||||
.foregroundColor(Color.appAccent)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.horizontal, 12)
|
||||
.padding(.vertical, 6)
|
||||
.background(Color.appAccent.opacity(0.1))
|
||||
.cornerRadius(AppRadius.sm)
|
||||
.background(
|
||||
Capsule()
|
||||
.fill(Color.appAccent.opacity(0.12))
|
||||
.overlay(
|
||||
Capsule()
|
||||
.stroke(Color.appAccent.opacity(0.2), lineWidth: 1)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Divider()
|
||||
OrganicDivider()
|
||||
|
||||
// Contractor info
|
||||
if let contractor = completion.contractorDetails {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "wrench.and.screwdriver.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "wrench.and.screwdriver.fill")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(contractor.name)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.medium)
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
|
||||
if let company = contractor.company {
|
||||
Text(company)
|
||||
.font(.caption)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
}
|
||||
}
|
||||
@@ -198,28 +252,33 @@ struct CompletionHistoryCard: View {
|
||||
|
||||
// Cost
|
||||
if let cost = completion.actualCost {
|
||||
HStack(spacing: AppSpacing.sm) {
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.frame(width: 24)
|
||||
HStack(spacing: 12) {
|
||||
ZStack {
|
||||
Circle()
|
||||
.fill(Color.appPrimary.opacity(0.1))
|
||||
.frame(width: 32, height: 32)
|
||||
Image(systemName: "dollarsign.circle.fill")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
|
||||
Text("$\(cost)")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: 15, weight: .bold, design: .rounded))
|
||||
.foregroundColor(Color.appPrimary)
|
||||
}
|
||||
}
|
||||
|
||||
// Notes
|
||||
if !completion.notes.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(L10n.Tasks.notes)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: 11, weight: .semibold, design: .rounded))
|
||||
.foregroundColor(Color.appTextSecondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
Text(completion.notes)
|
||||
.font(.subheadline)
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(Color.appTextPrimary)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
@@ -230,26 +289,27 @@ struct CompletionHistoryCard: View {
|
||||
Button(action: {
|
||||
showPhotoSheet = true
|
||||
}) {
|
||||
HStack {
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "photo.on.rectangle.angled")
|
||||
.font(.subheadline)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
Text("\(L10n.Tasks.viewPhotos) (\(completion.images.count))")
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
.font(.system(size: 14, weight: .semibold, design: .rounded))
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color.appPrimary.opacity(0.1))
|
||||
.padding(.vertical, 14)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14, style: .continuous)
|
||||
.fill(Color.appPrimary.opacity(0.12))
|
||||
)
|
||||
.foregroundColor(Color.appPrimary)
|
||||
.cornerRadius(AppRadius.sm)
|
||||
}
|
||||
.padding(.top, 4)
|
||||
.padding(.top, 6)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.padding(OrganicSpacing.cozy)
|
||||
.background(Color.appBackgroundSecondary)
|
||||
.cornerRadius(AppRadius.lg)
|
||||
.shadow(color: Color.black.opacity(0.05), radius: 2, x: 0, y: 1)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 24, style: .continuous))
|
||||
.naturalShadow(.medium)
|
||||
.sheet(isPresented: $showPhotoSheet) {
|
||||
PhotoViewerSheet(images: completion.images)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user