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>
276 lines
12 KiB
Swift
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)
|
|
}
|