370 lines
11 KiB
Swift
370 lines
11 KiB
Swift
//
|
|
// DashboardWidget.swift
|
|
// DashboardWidget
|
|
//
|
|
// Created by hillel on 12/06/2023.
|
|
//
|
|
|
|
import WidgetKit
|
|
import SwiftUI
|
|
import Intents
|
|
|
|
struct Provider: IntentTimelineProvider {
|
|
func placeholder(in context: Context) -> SimpleEntry {
|
|
|
|
SimpleEntry(date: Date(),
|
|
configuration: ConfigurationIntent(),
|
|
widgetData: WidgetData(url: "url", companyId: "", companies: [:]),
|
|
field: "Active Invoices",
|
|
value: "$100.00")
|
|
}
|
|
|
|
func getSnapshot(for configuration: ConfigurationIntent,
|
|
in context: Context,
|
|
completion: @escaping (SimpleEntry) -> ()) {
|
|
|
|
let entry = SimpleEntry(date: Date(),
|
|
configuration: configuration,
|
|
widgetData: WidgetData(url: "url", companyId: "", companies: [:]),
|
|
field: "Active Invoices",
|
|
value: "$100.00")
|
|
|
|
completion(entry)
|
|
}
|
|
|
|
func getTimeline(for configuration: ConfigurationIntent,
|
|
in context: Context,
|
|
completion: @escaping (Timeline<Entry>) -> ()) {
|
|
|
|
print("## getTimeline")
|
|
|
|
Task {
|
|
|
|
let sharedDefaults = UserDefaults.init(suiteName: "group.com.invoiceninja.app")
|
|
var widgetData: WidgetData? = nil
|
|
|
|
if sharedDefaults == nil {
|
|
return
|
|
}
|
|
|
|
do {
|
|
let shared = sharedDefaults!.string(forKey: "widget_data")
|
|
if shared == nil {
|
|
return
|
|
}
|
|
|
|
//print("## Shared: \(shared!)")
|
|
|
|
let decoder = JSONDecoder()
|
|
widgetData = try decoder.decode(WidgetData.self, from: shared!.data(using: .utf8)!)
|
|
|
|
if (widgetData?.url == nil) {
|
|
return
|
|
}
|
|
|
|
let url = (widgetData?.url ?? "") + "/charts/totals_v2";
|
|
let companyId = configuration.company?.identifier ?? ""
|
|
let company = widgetData?.companies[companyId]
|
|
var token = company?.token
|
|
|
|
if (token == "" && !(widgetData?.companies.isEmpty)!) {
|
|
print("## WARNING: using first token")
|
|
let company = widgetData?.companies.values.first;
|
|
token = company?.token ?? ""
|
|
}
|
|
|
|
print("## company.name: \(configuration.company?.displayString ?? "")")
|
|
print("## company.id: \(configuration.company?.identifier ?? "")")
|
|
//print("## URL: \(url)")
|
|
|
|
if (token == "") {
|
|
return
|
|
}
|
|
|
|
guard let result = try? await ApiService.post(urlString: url, apiToken: token!) else {
|
|
return
|
|
}
|
|
|
|
let currencyId = configuration.currency?.identifier ?? company?.currencyId
|
|
let currency = company?.currencies[currencyId!]
|
|
|
|
var value = 0.0
|
|
var label = ""
|
|
let data = result[currencyId ?? "1"]
|
|
|
|
if (data != nil) {
|
|
if (configuration.field == Field.active_invoices) {
|
|
if (data?.invoices?.invoicedAmount != nil) {
|
|
value = Double(data?.invoices?.invoicedAmount ?? "")!
|
|
}
|
|
label = "Active Invoices"
|
|
} else if (configuration.field == Field.outstanding_invoices) {
|
|
if (data?.outstanding?.amount != nil) {
|
|
value = Double(data?.outstanding?.amount ?? "")!
|
|
}
|
|
label = "Outstanding Invoices"
|
|
} else if (configuration.field == Field.completed_payments) {
|
|
if (data?.revenue?.paidToDate != nil) {
|
|
value = Double(data?.revenue?.paidToDate ?? "")!
|
|
}
|
|
label = "Completed Payments"
|
|
}
|
|
}
|
|
|
|
let formatter = NumberFormatter()
|
|
formatter.numberStyle = .currency
|
|
formatter.currencyCode = currency?.code ?? "USD"
|
|
|
|
let entry = SimpleEntry(date: Date(),
|
|
configuration: configuration,
|
|
widgetData: widgetData,
|
|
field: label,
|
|
value: formatter.string(from: NSNumber(value: value))!)
|
|
|
|
// Next fetch happens 15 minutes later
|
|
let nextUpdate = Calendar.current.date(
|
|
byAdding: DateComponents(minute: 15),
|
|
to: Date()
|
|
)!
|
|
|
|
let timeline = Timeline(
|
|
entries: [entry],
|
|
policy: .after(nextUpdate)
|
|
)
|
|
|
|
completion(timeline)
|
|
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
struct WidgetData: Decodable, Hashable {
|
|
let url: String
|
|
let companyId: String
|
|
let companies: [String: WidgetCompany]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case url
|
|
case companyId = "company_id"
|
|
case companies
|
|
}
|
|
}
|
|
|
|
struct WidgetCompany: Decodable, Hashable {
|
|
let id: String
|
|
let name: String
|
|
let token: String
|
|
let accentColor: String
|
|
let currencyId: String
|
|
let currencies: [String: WidgetCurrency]
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case name
|
|
case token
|
|
case accentColor = "accent_color"
|
|
case currencyId = "currency_id"
|
|
case currencies
|
|
}
|
|
}
|
|
|
|
struct WidgetCurrency: Decodable, Hashable {
|
|
let id: String
|
|
let name: String
|
|
let code: String
|
|
let exchangeRate: Double
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case name
|
|
case code
|
|
case exchangeRate = "exchange_rate"
|
|
}
|
|
}
|
|
|
|
struct SimpleEntry: TimelineEntry {
|
|
let date: Date
|
|
let configuration: ConfigurationIntent
|
|
let widgetData: WidgetData?
|
|
let field: String
|
|
let value: String
|
|
}
|
|
|
|
struct DashboardWidgetEntryView : View {
|
|
var entry: Provider.Entry
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
//Rectangle().fill(BackgroundStyle())
|
|
Rectangle().fill(Color.blue)
|
|
VStack(alignment: .leading) {
|
|
|
|
HStack {
|
|
VStack {
|
|
Text(entry.field)
|
|
.font(.body)
|
|
.bold()
|
|
.foregroundColor(Color.blue)
|
|
Text(entry.value)
|
|
.font(.title)
|
|
.privacySensitive()
|
|
.foregroundColor(Color.gray)
|
|
.minimumScaleFactor(0.8)
|
|
.padding(.top, 8)
|
|
}
|
|
.padding(.all)
|
|
|
|
}
|
|
.padding(.top, 8.0)
|
|
.background(ContainerRelativeShape().fill(Color(.white)))
|
|
|
|
Spacer()
|
|
|
|
Text(entry.configuration.company?.displayString ?? "")
|
|
.font(.body)
|
|
.bold()
|
|
.foregroundColor(Color.white)
|
|
|
|
Text("Date Range")
|
|
.font(.caption)
|
|
.foregroundColor(Color.white)
|
|
|
|
}
|
|
.padding(.all)
|
|
}
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct DashboardWidget: Widget {
|
|
let kind: String = "DashboardWidget"
|
|
|
|
var body: some WidgetConfiguration {
|
|
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
|
|
DashboardWidgetEntryView(entry: entry)
|
|
}
|
|
.configurationDisplayName("Dashboard")
|
|
.description("View total from dashboard.")
|
|
.supportedFamilies([.systemSmall,])
|
|
}
|
|
}
|
|
|
|
struct DashboardWidget_Previews: PreviewProvider {
|
|
static var previews: some View {
|
|
let entry = SimpleEntry(date: Date(),
|
|
configuration: ConfigurationIntent(),
|
|
widgetData: WidgetData(url: "url", companyId: "", companies: [:]),
|
|
field: "Active Invoices",
|
|
value: "$100.00")
|
|
|
|
DashboardWidgetEntryView(entry: entry)
|
|
.previewContext(WidgetPreviewContext(family: .systemSmall))
|
|
//.environment(\.sizeCategory, .extraLarge)
|
|
//.environment(\.colorScheme, .dark)
|
|
}
|
|
}
|
|
|
|
|
|
struct ApiResult: Codable {
|
|
let invoices: Invoices?
|
|
let revenue: Revenue?
|
|
let outstanding: Outstanding?
|
|
let expenses: Expenses?
|
|
}
|
|
|
|
struct Invoices: Codable {
|
|
let invoicedAmount: String?
|
|
let currencyId: String?
|
|
let code: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case invoicedAmount = "invoiced_amount"
|
|
case currencyId = "currency_id"
|
|
case code
|
|
}
|
|
}
|
|
|
|
struct Revenue: Codable {
|
|
let paidToDate: String?
|
|
let currencyId: String?
|
|
let code: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case paidToDate = "paid_to_date"
|
|
case currencyId = "currency_id"
|
|
case code
|
|
}
|
|
}
|
|
|
|
struct Outstanding: Codable {
|
|
let amount: String?
|
|
let outstandingCount: Int?
|
|
let currencyId: String?
|
|
let code: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case amount
|
|
case outstandingCount = "outstanding_count"
|
|
case currencyId = "currency_id"
|
|
case code
|
|
}
|
|
}
|
|
|
|
struct Expenses: Codable {
|
|
let amount: String?
|
|
let currencyId: String?
|
|
let code: String?
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
case amount
|
|
case currencyId = "currency_id"
|
|
case code
|
|
}
|
|
}
|
|
|
|
|
|
struct ApiService {
|
|
|
|
static func post(urlString: String, apiToken: String) async throws -> [String: ApiResult]? {
|
|
|
|
let url = URL(string: urlString)!
|
|
|
|
var request = URLRequest(url: url)
|
|
request.httpMethod = "POST"
|
|
request.addValue(apiToken, forHTTPHeaderField: "X-Api-Token")
|
|
|
|
let dataDict: [String: String] = [
|
|
"start_date": "2020-12-30",
|
|
"end_date": "2023-12-31",
|
|
]
|
|
|
|
do {
|
|
let jsonData = try JSONSerialization.data(withJSONObject: dataDict, options: [])
|
|
request.httpBody = jsonData
|
|
} catch {
|
|
print("Error: Failed to serialize data - \(error)")
|
|
}
|
|
|
|
do {
|
|
let (data, _) = try await URLSession.shared.data(for: request)
|
|
// process data
|
|
|
|
//print("## Details: \(String(describing: String(data: data, encoding: .utf8)))")
|
|
let result = try JSONDecoder().decode([String: ApiResult].self, from: data)
|
|
|
|
return result
|
|
|
|
} catch {
|
|
print("Error: \(error)")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
|