Files
honeyDueKMP/iosApp/iosApp/Subviews/Task/TaskCard.swift
Trey t 9305371276 Modernize task and residence card components
Applied modern design system to card components for consistent, sleek appearance.

TaskCard Improvements:
- Modern typography with design system fonts
- Pill-style metadata badges with icons
- Color-coded action buttons (success green, warning orange)
- Improved spacing and visual hierarchy
- Enhanced completion section with icon badge
- Redesigned secondary actions with better contrast

ResidenceCard Improvements:
- Gradient icon background for property type
- Star badge for primary residence with accent color
- Location icons for address fields
- Modernized task stats with color coding
- Consistent shadow and corner radius

Both cards now use:
- AppColors for consistent color palette
- AppTypography for font hierarchy
- AppSpacing for consistent spacing
- AppRadius for uniform corner radii
- AppShadow for depth and elevation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 11:53:39 -06:00

276 lines
12 KiB
Swift

import SwiftUI
import ComposeApp
struct TaskCard: View {
let task: TaskDetail
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: AppSpacing.md) {
// Header
HStack(alignment: .top, spacing: AppSpacing.sm) {
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text(task.title)
.font(AppTypography.titleMedium)
.foregroundColor(AppColors.textPrimary)
.lineLimit(2)
if let status = task.status {
StatusBadge(status: status.name)
}
}
Spacer()
PriorityBadge(priority: task.priority.name)
}
// Description
if let description = task.description_, !description.isEmpty {
Text(description)
.font(AppTypography.bodySmall)
.foregroundColor(AppColors.textSecondary)
.lineLimit(3)
}
// Metadata
HStack(spacing: AppSpacing.md) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "repeat")
.font(.system(size: 12, weight: .medium))
.foregroundColor(AppColors.textTertiary)
Text(task.frequency.displayName)
.font(AppTypography.labelSmall)
.foregroundColor(AppColors.textSecondary)
}
.padding(.horizontal, AppSpacing.sm)
.padding(.vertical, AppSpacing.xxs)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.xs)
Spacer()
if let dueDate = task.dueDate {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "calendar")
.font(.system(size: 12, weight: .medium))
.foregroundColor(AppColors.textTertiary)
Text(formatDate(dueDate))
.font(AppTypography.labelSmall)
.foregroundColor(AppColors.textSecondary)
}
.padding(.horizontal, AppSpacing.sm)
.padding(.vertical, AppSpacing.xxs)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.xs)
}
}
// Completions
if task.completions.count > 0 {
Divider()
.padding(.vertical, AppSpacing.xxs)
VStack(alignment: .leading, spacing: AppSpacing.sm) {
HStack(spacing: AppSpacing.xs) {
ZStack {
Circle()
.fill(AppColors.success.opacity(0.1))
.frame(width: 24, height: 24)
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 14, weight: .semibold))
.foregroundColor(AppColors.success)
}
Text("Completions (\(task.completions.count))")
.font(AppTypography.labelMedium)
.fontWeight(.semibold)
.foregroundColor(AppColors.textPrimary)
}
ForEach(task.completions, id: \.id) { completion in
CompletionCardView(completion: completion)
}
}
}
// Primary Actions
if task.showCompletedButton {
VStack(spacing: AppSpacing.xs) {
if let onMarkInProgress = onMarkInProgress, task.status?.name != "in_progress" {
Button(action: onMarkInProgress) {
HStack(spacing: AppSpacing.xs) {
Image(systemName: "play.circle.fill")
.font(.system(size: 16, weight: .semibold))
Text("In Progress")
.font(AppTypography.labelLarge)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 44)
.foregroundColor(AppColors.warning)
.background(AppColors.warning.opacity(0.1))
.cornerRadius(AppRadius.md)
}
}
if task.showCompletedButton, let onComplete = onComplete {
Button(action: onComplete) {
HStack(spacing: AppSpacing.xs) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 16, weight: .semibold))
Text("Complete")
.font(AppTypography.labelLarge)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 44)
.foregroundColor(.white)
.background(AppColors.success)
.cornerRadius(AppRadius.md)
}
}
}
}
// Secondary Actions
VStack(spacing: AppSpacing.xs) {
HStack(spacing: AppSpacing.xs) {
Button(action: onEdit) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "pencil")
.font(.system(size: 14, weight: .medium))
Text("Edit")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(AppColors.primary)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.sm)
}
if let onCancel = onCancel {
Button(action: onCancel) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "xmark.circle")
.font(.system(size: 14, weight: .medium))
Text("Cancel")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(AppColors.error)
.background(AppColors.error.opacity(0.1))
.cornerRadius(AppRadius.sm)
}
} else if let onUncancel = onUncancel {
Button(action: onUncancel) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "arrow.uturn.backward")
.font(.system(size: 14, weight: .medium))
Text("Restore")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(.white)
.background(AppColors.primary)
.cornerRadius(AppRadius.sm)
}
}
}
if task.archived {
if let onUnarchive = onUnarchive {
Button(action: onUnarchive) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "tray.and.arrow.up")
.font(.system(size: 14, weight: .medium))
Text("Unarchive")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(AppColors.primary)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.sm)
}
}
} else {
if let onArchive = onArchive {
Button(action: onArchive) {
HStack(spacing: AppSpacing.xxs) {
Image(systemName: "archivebox")
.font(.system(size: 14, weight: .medium))
Text("Archive")
.font(AppTypography.labelMedium)
}
.frame(maxWidth: .infinity)
.frame(height: 36)
.foregroundColor(AppColors.textSecondary)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.sm)
}
}
}
}
}
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.lg)
.shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
}
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", lookupName: "", displayName: "30", daySpan: 0, notifyDays: 0),
status: TaskStatus(id: 1, name: "pending", displayName: "", description: ""),
dueDate: "2024-12-15",
estimatedCost: "150.00",
actualCost: nil,
notes: nil,
archived: false,
createdAt: "2024-01-01T00:00:00Z",
updatedAt: "2024-01-01T00:00:00Z",
nextScheduledDate: nil,
showCompletedButton: true,
completions: []
),
onEdit: {},
onCancel: {},
onUncancel: nil,
onMarkInProgress: {},
onComplete: {},
onArchive: {},
onUnarchive: {}
)
}
.padding()
.background(AppColors.background)
}