Compare commits
10 Commits
69ff01073b
...
01f471c53c
| Author | SHA1 | Date | |
|---|---|---|---|
| 01f471c53c | |||
| 0edfa2f7f3 | |||
| c7c2067065 | |||
| 340863d791 | |||
| 3b6293f6e5 | |||
| 4d825f7d91 | |||
| 9dffb32382 | |||
| 968a1db877 | |||
| d01363f236 | |||
| 621ce23fc7 |
@@ -11,6 +11,10 @@
|
||||
D40CCAE22C2DC5A9007C4A9F /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAE12C2DC5A9007C4A9F /* ContentView.swift */; };
|
||||
D40CCAE42C2DC5AA007C4A9F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D40CCAE32C2DC5AA007C4A9F /* Assets.xcassets */; };
|
||||
D40CCAE82C2DC5AA007C4A9F /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D40CCAE72C2DC5AA007C4A9F /* Preview Assets.xcassets */; };
|
||||
D40CCAEF2C2DC5D8007C4A9F /* Subscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */; };
|
||||
D40CCAF32C2EE305007C4A9F /* AddSubscriptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */; };
|
||||
D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D426C5592C2F0F150057455D /* PaymentCalendarView.swift */; };
|
||||
D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4544FEE2C320AF30090E311 /* HomeView.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -20,6 +24,10 @@
|
||||
D40CCAE32C2DC5AA007C4A9F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
D40CCAE52C2DC5AA007C4A9F /* AboTracker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AboTracker.entitlements; sourceTree = "<group>"; };
|
||||
D40CCAE72C2DC5AA007C4A9F /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Subscription.swift; sourceTree = "<group>"; };
|
||||
D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSubscriptionView.swift; sourceTree = "<group>"; };
|
||||
D426C5592C2F0F150057455D /* PaymentCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaymentCalendarView.swift; sourceTree = "<group>"; };
|
||||
D4544FEE2C320AF30090E311 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -53,7 +61,8 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D40CCADF2C2DC5A9007C4A9F /* AboTrackerApp.swift */,
|
||||
D40CCAE12C2DC5A9007C4A9F /* ContentView.swift */,
|
||||
D40CCAEE2C2DC5D8007C4A9F /* Subscription.swift */,
|
||||
D4544FF02C320B920090E311 /* views */,
|
||||
D40CCAE32C2DC5AA007C4A9F /* Assets.xcassets */,
|
||||
D40CCAE52C2DC5AA007C4A9F /* AboTracker.entitlements */,
|
||||
D40CCAE62C2DC5AA007C4A9F /* Preview Content */,
|
||||
@@ -69,6 +78,17 @@
|
||||
path = "Preview Content";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D4544FF02C320B920090E311 /* views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D40CCAE12C2DC5A9007C4A9F /* ContentView.swift */,
|
||||
D4544FEE2C320AF30090E311 /* HomeView.swift */,
|
||||
D40CCAF22C2EE304007C4A9F /* AddSubscriptionView.swift */,
|
||||
D426C5592C2F0F150057455D /* PaymentCalendarView.swift */,
|
||||
);
|
||||
path = views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -139,7 +159,11 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D40CCAEF2C2DC5D8007C4A9F /* Subscription.swift in Sources */,
|
||||
D40CCAF32C2EE305007C4A9F /* AddSubscriptionView.swift in Sources */,
|
||||
D40CCAE22C2DC5A9007C4A9F /* ContentView.swift in Sources */,
|
||||
D4544FEF2C320AF30090E311 /* HomeView.swift in Sources */,
|
||||
D426C55A2C2F0F150057455D /* PaymentCalendarView.swift in Sources */,
|
||||
D40CCAE02C2DC5A9007C4A9F /* AboTrackerApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
//
|
||||
// ContentView.swift
|
||||
// AboTracker
|
||||
//
|
||||
// Created by Keyvan Atashfaraz on 27.06.24.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image(systemName: "globe")
|
||||
.imageScale(.large)
|
||||
.foregroundStyle(.tint)
|
||||
Text("Hello, world!")
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentView()
|
||||
}
|
||||
142
AboTracker/Subscription.swift
Normal file
142
AboTracker/Subscription.swift
Normal file
@@ -0,0 +1,142 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class Subscription: Identifiable {
|
||||
public let id = UUID()
|
||||
var name: String
|
||||
var payments: [Payment]
|
||||
var color: Color
|
||||
|
||||
init(name: String, payments: [Payment], color: Color) {
|
||||
self.name = name
|
||||
self.payments = payments
|
||||
self.color = color
|
||||
}
|
||||
|
||||
func getMonthlyAmount() -> Float {
|
||||
var sum: Float = 0.0
|
||||
for payment in payments {
|
||||
switch payment.intervall {
|
||||
case .weekly:
|
||||
sum += payment.amount / 7 * 30
|
||||
case .monthly:
|
||||
sum += payment.amount
|
||||
case .quarter:
|
||||
sum += payment.amount / 3
|
||||
case .yearly:
|
||||
sum += payment.amount / 12
|
||||
}
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func getRemainingForCurrentMonth() -> Float {
|
||||
var remaining: Float = 0.0
|
||||
let calendar = Calendar.current
|
||||
let today = Date()
|
||||
let range = calendar.range(of: .day, in: .month, for: today)!
|
||||
let daysInMonth = range.count
|
||||
|
||||
for payment in payments {
|
||||
let components = calendar.dateComponents([.day], from: today, to: payment.startDate)
|
||||
let daysRemaining = max(0, (components.day ?? 0) + 1)
|
||||
|
||||
switch payment.intervall {
|
||||
case .weekly:
|
||||
let paymentsRemaining = Float(daysRemaining) / 7.0
|
||||
remaining += payment.amount * paymentsRemaining
|
||||
case .monthly:
|
||||
remaining += (daysRemaining >= daysInMonth) ? 0 : payment.amount
|
||||
case .quarter:
|
||||
let paymentsRemaining = Float(daysRemaining) / 90.0
|
||||
remaining += payment.amount * paymentsRemaining
|
||||
case .yearly:
|
||||
let paymentsRemaining = Float(daysRemaining) / 365.0
|
||||
remaining += payment.amount * paymentsRemaining
|
||||
}
|
||||
}
|
||||
return remaining
|
||||
}
|
||||
|
||||
func getPaymentDaysIn(year: Int, month: Int) -> [Int] {
|
||||
var paymentDays: [Int] = []
|
||||
|
||||
let calendar = Calendar.current
|
||||
let components = DateComponents(year: year, month: month)
|
||||
guard let date = calendar.date(from: components) else {
|
||||
return paymentDays
|
||||
}
|
||||
|
||||
for payment in payments {
|
||||
var currentDate = payment.startDate
|
||||
while calendar.isDate(currentDate, equalTo: date, toGranularity: .month) {
|
||||
let day = calendar.component(.day, from: currentDate)
|
||||
paymentDays.append(day)
|
||||
|
||||
switch payment.intervall {
|
||||
case .weekly:
|
||||
guard let nextDate = calendar.date(byAdding: .day, value: 7, to: currentDate) else { break }
|
||||
currentDate = nextDate
|
||||
case .monthly:
|
||||
guard let nextDate = calendar.date(byAdding: .month, value: 1, to: currentDate) else { break }
|
||||
currentDate = nextDate
|
||||
case .quarter:
|
||||
guard let nextDate = calendar.date(byAdding: .month, value: 3, to: currentDate) else { break }
|
||||
currentDate = nextDate
|
||||
case .yearly:
|
||||
guard let nextDate = calendar.date(byAdding: .year, value: 1, to: currentDate) else { break }
|
||||
currentDate = nextDate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return paymentDays
|
||||
}
|
||||
}
|
||||
|
||||
final class Payment: Identifiable {
|
||||
public let id = UUID()
|
||||
var amount: Float
|
||||
var intervall: PaymentIntervall
|
||||
var startDate: Date
|
||||
|
||||
init(amount: Float, intervall: PaymentIntervall, startDate: Date) {
|
||||
self.amount = amount
|
||||
self.intervall = intervall
|
||||
self.startDate = startDate
|
||||
}
|
||||
}
|
||||
|
||||
enum Currency: CustomStringConvertible {
|
||||
case euro
|
||||
case dollar
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .euro:
|
||||
return "€"
|
||||
case .dollar:
|
||||
return "$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PaymentIntervall: CustomStringConvertible {
|
||||
case weekly
|
||||
case monthly
|
||||
case quarter
|
||||
case yearly
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .weekly:
|
||||
return "week"
|
||||
case .monthly:
|
||||
return "month"
|
||||
case .quarter:
|
||||
return "quarter"
|
||||
case .yearly:
|
||||
return "year"
|
||||
}
|
||||
}
|
||||
}
|
||||
69
AboTracker/views/AddSubscriptionView.swift
Normal file
69
AboTracker/views/AddSubscriptionView.swift
Normal file
@@ -0,0 +1,69 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AddSubscriptionView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
@Binding var subs: [Subscription]
|
||||
|
||||
@State private var name: String = ""
|
||||
@State private var payments: [Payment] = [Payment(amount: 0, intervall: .monthly, startDate: Date())]
|
||||
@State private var color: Color = .blue
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
Form {
|
||||
Section(header: Text("Subscription Name")) {
|
||||
TextField("Name", text: $name)
|
||||
}
|
||||
|
||||
Section(header: Text("Color")) {
|
||||
ColorPicker("Select Color", selection: $color)
|
||||
}
|
||||
|
||||
ForEach($payments) { $payment in
|
||||
Section(header: Text("Payment")) {
|
||||
HStack {
|
||||
TextField("Amount", value: $payment.amount, formatter: NumberFormatter())
|
||||
Spacer()
|
||||
Text("\(Currency.euro.description)")
|
||||
}
|
||||
Picker("Intervall", selection: $payment.intervall) {
|
||||
Text("Weekly").tag(PaymentIntervall.weekly)
|
||||
Text("Monthly").tag(PaymentIntervall.monthly)
|
||||
Text("Quarter").tag(PaymentIntervall.quarter)
|
||||
Text("Yearly").tag(PaymentIntervall.yearly)
|
||||
}
|
||||
.pickerStyle(SegmentedPickerStyle())
|
||||
|
||||
DatePicker("Start Date", selection: $payment.startDate, displayedComponents: .date)
|
||||
}
|
||||
}
|
||||
.onDelete(perform: deletePayment)
|
||||
|
||||
Section {
|
||||
Button("Add Payment") {
|
||||
let newPayment = Payment(amount: 0, intervall: .monthly, startDate: Date())
|
||||
payments.append(newPayment)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Add Subscription") {
|
||||
let newSubscription = Subscription(name: name, payments: payments, color: color)
|
||||
subs.append(newSubscription)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Add Subscription")
|
||||
#if os(iOS)
|
||||
.navigationBarItems(trailing: Button("Cancel") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
})
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
func deletePayment(at offsets: IndexSet) {
|
||||
payments.remove(atOffsets: offsets)
|
||||
}
|
||||
}
|
||||
44
AboTracker/views/ContentView.swift
Normal file
44
AboTracker/views/ContentView.swift
Normal file
@@ -0,0 +1,44 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var subs: [Subscription] = []
|
||||
|
||||
init() {
|
||||
self._subs = State(initialValue: [
|
||||
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: getDate(from: "2023-01-01"))], color: .blue),
|
||||
Subscription(name: "Fitness First", payments: [
|
||||
Payment(amount: 7.9, intervall: .weekly, startDate: getDate(from: "2023-01-23")),
|
||||
Payment(amount: 29, intervall: .quarter, startDate: getDate(from: "2023-04-03"))
|
||||
], color: .red)
|
||||
])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
HomeView(subs: $subs)
|
||||
.tabItem {
|
||||
Image(systemName: "house.fill")
|
||||
Text("Home")
|
||||
}
|
||||
|
||||
PaymentCalendarView(subs: $subs)
|
||||
.tabItem {
|
||||
Image(systemName: "calendar")
|
||||
Text("Calendar")
|
||||
}
|
||||
}
|
||||
.background(Color.gray.opacity(0.2))
|
||||
}
|
||||
|
||||
private func getDate(from dateString: String) -> Date {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyy-MM-dd"
|
||||
return formatter.date(from: dateString) ?? Date()
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
110
AboTracker/views/HomeView.swift
Normal file
110
AboTracker/views/HomeView.swift
Normal file
@@ -0,0 +1,110 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@Binding var subs: [Subscription]
|
||||
@State private var showAddSubscriptionSheet = false
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
VStack {
|
||||
List {
|
||||
ForEach(subs) { sub in
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(sub.name)
|
||||
.font(.headline)
|
||||
ForEach(sub.payments) { payment in
|
||||
HStack {
|
||||
Text("\(payment.amount, specifier: "%.2f")\(Currency.euro.description)")
|
||||
Spacer()
|
||||
Text("/\(payment.intervall.description)")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.listRowBackground(sub.color.opacity(0.2))
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.onDelete(perform: deleteSubscription)
|
||||
|
||||
Section(header: Text("Totals")) {
|
||||
HStack {
|
||||
Text("Monthly Total")
|
||||
Spacer()
|
||||
Text("\(getMonthlyTotal(subs: subs), specifier: "%.2f")\(Currency.euro.description)")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text("Remaining for Current Month")
|
||||
Spacer()
|
||||
Text("\(getRemainingForCurrentMonth(subs: subs), specifier: "%.2f")\(Currency.euro.description)")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Subscriptions")
|
||||
.toolbar {
|
||||
#if os(iOS)
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
EditButton()
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button(action: {
|
||||
showAddSubscriptionSheet.toggle()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button(action: {
|
||||
showAddSubscriptionSheet.toggle()
|
||||
}) {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.sheet(isPresented: $showAddSubscriptionSheet) {
|
||||
AddSubscriptionView(subs: $subs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteSubscription(at offsets: IndexSet) {
|
||||
subs.remove(atOffsets: offsets)
|
||||
}
|
||||
|
||||
func getMonthlyTotal(subs: [Subscription]) -> Float {
|
||||
var monthlyTotal: Float = 0.0
|
||||
for sub in subs {
|
||||
monthlyTotal += sub.getMonthlyAmount()
|
||||
}
|
||||
return monthlyTotal
|
||||
}
|
||||
|
||||
func getRemainingForCurrentMonth(subs: [Subscription]) -> Float {
|
||||
var remainingTotal: Float = 0.0
|
||||
for sub in subs {
|
||||
remainingTotal += sub.getRemainingForCurrentMonth()
|
||||
}
|
||||
return remainingTotal
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HomeView(subs: .constant([
|
||||
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: Date())], color: .blue),
|
||||
Subscription(name: "Fitness First", payments: [
|
||||
Payment(amount: 7.9, intervall: .weekly, startDate: Date()),
|
||||
Payment(amount: 29, intervall: .quarter, startDate: Date())
|
||||
], color: .red)
|
||||
]))
|
||||
}
|
||||
}
|
||||
163
AboTracker/views/PaymentCalendarView.swift
Normal file
163
AboTracker/views/PaymentCalendarView.swift
Normal file
@@ -0,0 +1,163 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PaymentCalendarView: View {
|
||||
@Binding var subs: [Subscription]
|
||||
let calendar = Calendar.current
|
||||
let dateFormatter = DateFormatter()
|
||||
|
||||
@State private var currentDate = Date()
|
||||
private let initialDate = Date()
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
Text("Payment Calendar")
|
||||
.font(.title)
|
||||
.padding()
|
||||
|
||||
HStack {
|
||||
Button(action: {
|
||||
self.currentDate = self.calendar.date(byAdding: .month, value: -1, to: self.currentDate)!
|
||||
}) {
|
||||
Image(systemName: "chevron.left.circle.fill")
|
||||
.font(.title)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.currentDate = self.initialDate
|
||||
}) {
|
||||
Text("Current Month")
|
||||
.font(.headline)
|
||||
}
|
||||
|
||||
Button(action: {
|
||||
self.currentDate = self.calendar.date(byAdding: .month, value: 1, to: self.currentDate)!
|
||||
}) {
|
||||
Image(systemName: "chevron.right.circle.fill")
|
||||
.font(.title)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal)
|
||||
|
||||
calendarView()
|
||||
.gesture(
|
||||
DragGesture()
|
||||
.onEnded { gesture in
|
||||
if gesture.translation.width < 0 {
|
||||
// Swipe left -> move to next month
|
||||
self.currentDate = self.calendar.date(byAdding: .month, value: 1, to: self.currentDate)!
|
||||
} else {
|
||||
// Swipe right -> move to previous month
|
||||
self.currentDate = self.calendar.date(byAdding: .month, value: -1, to: self.currentDate)!
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
func currentMonthYear() -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "MMMM yyyy"
|
||||
return formatter.string(from: currentDate)
|
||||
}
|
||||
|
||||
func calendarView() -> some View {
|
||||
let days = daysInMonth(date: currentDate)
|
||||
let firstDay = firstDayOfMonth(date: currentDate)
|
||||
|
||||
let columns = Array(repeating: GridItem(.flexible()), count: 7)
|
||||
let weeks = (days.count + firstDay) / 7 + ((days.count + firstDay) % 7 == 0 ? 0 : 1)
|
||||
|
||||
return LazyVGrid(columns: columns, spacing: 20) {
|
||||
ForEach(0..<7, id: \.self) { index in
|
||||
Text(daySymbol(index: index))
|
||||
.fontWeight(.bold)
|
||||
}
|
||||
|
||||
ForEach(0..<(weeks * 7), id: \.self) { index in
|
||||
if index < firstDay || index - firstDay >= days.count {
|
||||
Text("")
|
||||
} else {
|
||||
let day = index - firstDay + 1
|
||||
let dayDate = calendar.date(byAdding: .day, value: day - 1, to: startOfMonth(date: currentDate))!
|
||||
let paymentColor = getPaymentColor(for: dayDate)
|
||||
|
||||
Text("\(day)")
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(paymentColor)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getPaymentColor(for date: Date) -> Color {
|
||||
for sub in subs {
|
||||
for payment in sub.payments {
|
||||
if isPaymentDue(payment: payment, for: date) {
|
||||
return sub.color.opacity(0.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Color.clear
|
||||
}
|
||||
|
||||
func isPaymentDue(payment: Payment, for date: Date) -> Bool {
|
||||
switch payment.intervall {
|
||||
case .weekly:
|
||||
let startOfWeek = calendar.date(from: calendar.dateComponents([.yearForWeekOfYear, .weekOfYear], from: date))!
|
||||
let endOfWeek = calendar.date(byAdding: .day, value: 6, to: startOfWeek)!
|
||||
return date >= startOfWeek && date <= endOfWeek && calendar.component(.weekday, from: payment.startDate) == calendar.component(.weekday, from: date)
|
||||
|
||||
case .monthly:
|
||||
return calendar.component(.day, from: payment.startDate) == calendar.component(.day, from: date)
|
||||
|
||||
case .quarter:
|
||||
guard let quarterStartDate = calendar.date(byAdding: .month, value: -3, to: payment.startDate) else {
|
||||
return false
|
||||
}
|
||||
let startOfQuarter = calendar.date(from: calendar.dateComponents([.year, .month], from: quarterStartDate))!
|
||||
let endOfQuarter = calendar.date(byAdding: .month, value: 3, to: startOfQuarter)!
|
||||
return date >= startOfQuarter && date <= endOfQuarter
|
||||
|
||||
case .yearly:
|
||||
return calendar.component(.year, from: payment.startDate) == calendar.component(.year, from: date)
|
||||
}
|
||||
}
|
||||
|
||||
func daysInMonth(date: Date) -> [Date] {
|
||||
guard let range = calendar.range(of: .day, in: .month, for: date) else { return [] }
|
||||
return range.compactMap { day -> Date? in
|
||||
var components = calendar.dateComponents([.year, .month], from: date)
|
||||
components.day = day
|
||||
return calendar.date(from: components)
|
||||
}
|
||||
}
|
||||
|
||||
func firstDayOfMonth(date: Date) -> Int {
|
||||
let components = calendar.dateComponents([.year, .month], from: date)
|
||||
guard let firstDay = calendar.date(from: components) else { return 0 }
|
||||
return calendar.component(.weekday, from: firstDay) - calendar.firstWeekday
|
||||
}
|
||||
|
||||
func daySymbol(index: Int) -> String {
|
||||
dateFormatter.shortWeekdaySymbols[(index + calendar.firstWeekday - 1) % 7]
|
||||
}
|
||||
|
||||
func startOfMonth(date: Date) -> Date {
|
||||
let components = calendar.dateComponents([.year, .month], from: date)
|
||||
return calendar.date(from: components) ?? date
|
||||
}
|
||||
}
|
||||
|
||||
struct PaymentCalendarView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PaymentCalendarView(subs: .constant([
|
||||
Subscription(name: "Test", payments: [Payment(amount: 9.99, intervall: .monthly, startDate: Date())], color: .blue),
|
||||
Subscription(name: "Fitness First", payments: [
|
||||
Payment(amount: 7.9, intervall: .weekly, startDate: Date()),
|
||||
Payment(amount: 29, intervall: .quarter, startDate: Date())
|
||||
], color: .red)
|
||||
]))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user