Tech_Tracker_iOS/Tech Tracker/ContentView.swift

490 lines
18 KiB
Swift
Raw Permalink Normal View History

2024-05-22 16:06:53 -05:00
//
// ContentView.swift
// Tech Tracker
//
// Created by Gabriel Brown on 4/6/24.
//
import SwiftUI
import Foundation
import Combine
// Struct for Technician API
struct Technician: Identifiable, Codable {
let name: String
let status: String
let updatedAt: Date
2024-05-22 16:06:53 -05:00
var id: String { name } // Computed property for Identifiable conformance
enum CodingKeys: String, CodingKey {
case name, status, updatedAt
2024-05-22 16:06:53 -05:00
}
}
// Struct for Update API
struct TechnicianUpdate: Codable {
let name: String
let status: String
}
// Struct for History API
struct HistoryResponse: Decodable {
let data: [TechnicianHistory]
let meta: MetaData
}
// Struct for the technician portion of the history API
struct TechnicianHistory: Identifiable, Codable {
let name: String
let status: String
let updatedAt: Date
2024-05-22 16:06:53 -05:00
var id: UUID { UUID() }
enum CodingKeys: String, CodingKey {
case name, status, updatedAt
2024-05-22 16:06:53 -05:00
}
}
// Struct for Metadata from History API
struct MetaData: Decodable {
let current_page: Int
let per_page: Int
let total_pages: Int
let total_count: Int
}
// The Sheet View for updating technician status
struct StatusUpdateView: View {
@Binding var isPresented: Bool
var technicianName: (String)
var technicianStatus: (String)
var updateAction: (String) -> Void
@State private var newStatus: String = ""
var body: some View {
VStack(spacing: 15) {
VStack(alignment: .leading, spacing: 5) {
Text(technicianName).bold()
.font(.largeTitle)
Text(technicianStatus).lineLimit(2).font(.title)
}
TextField("New Status", text: $newStatus)
.padding() // Adds padding inside the TextField, making it taller
.textFieldStyle(RoundedBorderTextFieldStyle())
.font(.title3) // Increases the font size of the text field content
.foregroundColor(.white)
.padding(.horizontal, 20)
Button(action: {
updateAction(newStatus)
isPresented = false
}) {
Text("Update")
.font(.headline) // Make the button text larger
.padding() // Add padding around the button text to make the button larger
.frame(minWidth: 0, maxWidth: .infinity) // Makes the button expand to full width
2024-07-19 08:11:18 -05:00
.background(Color.accentColor) // Sets the button background color to blue
2024-05-22 16:06:53 -05:00
.foregroundColor(.white) // Sets the button text color to white
.cornerRadius(10) // Rounds the corners of the button
}
.padding(.top, 10) // Adds some space above the button
.padding(.horizontal, 30)
Spacer().frame(width: 0, height: 5)
HStack(spacing: 5) {
Button(action: {
updateAction("In the Office")
isPresented = false
}) {
Text("In the Office")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer().frame(width: 10)
Button(action: {
updateAction("At desk")
isPresented = false
}) {
Text("At desk")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
}.padding(.horizontal, 25)
HStack(spacing: 5) {
Button(action: {
updateAction("At lunch")
isPresented = false
}) {
Text("At lunch")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer().frame(width: 10)
Button(action: {
updateAction("At Hardy")
isPresented = false
}) {
Text("At Hardy")
.font(.headline)
.padding()
.frame(maxWidth: /*@START_MENU_TOKEN@*/.infinity/*@END_MENU_TOKEN@*/)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
}.padding(.horizontal, 25)
HStack(spacing: 5) {
Button(action: {
updateAction("At Police Department")
isPresented = false
}) {
Text("At Police Dpt")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer().frame(width: 10)
Button(action: {
updateAction("At City Hall")
isPresented = false
}) {
Text("At City Hall")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
}.padding(.horizontal, 25)
HStack(spacing: 5) {
Button(action: {
updateAction("In a meeting")
isPresented = false
}) {
Text("In a meeting")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer().frame(width: 10)
Button(action: {
updateAction("Out today")
isPresented = false
}) {
Text("Out today")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
}.padding(.horizontal, 25)
HStack(spacing: 5) {
Button(action: {
updateAction("Running late")
isPresented = false
}) {
Text("Running late")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
Spacer().frame(width: 10)
Button(action: {
updateAction("End of Shift")
isPresented = false
}) {
Text("End of Shift")
.font(.headline)
.padding()
.frame(maxWidth: .infinity)
2024-07-19 08:11:18 -05:00
.background(Color.accentColor)
2024-05-22 16:06:53 -05:00
.foregroundColor(.white)
.cornerRadius(10)
}
}.padding(.horizontal, 25)
}
.padding()
.padding(.top, 150)
}
}
// Single class for interpretting all API's. Has fetch functions for all 3.
class TechnicianViewModel: ObservableObject {
// Variables for containing our data from our APIs
@Published var technicians: [Technician] = []
@Published var technicianHistories: [TechnicianHistory] = []
@Published var currentPage = 1
var totalPageCount = 1
2024-07-19 08:11:18 -05:00
let apiKey = ProcessInfo.processInfo.environment["API_KEY"] ?? ""
2024-05-22 16:06:53 -05:00
// Fetch technicians function for Technicians API
func fetchTechnicians() {
2024-07-19 08:11:18 -05:00
let urlString = "https://techtracker.gibbyb.com/api/get_technicians?apikey=" + apiKey
2024-05-22 16:06:53 -05:00
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let data = data else {
DispatchQueue.main.async {
print("Networking error: \(error?.localizedDescription ?? "Unknown error")")
}
return
}
let decoder = JSONDecoder()
// Had to make my own formatter because I couldn't get the JSON decoding to work right
// This may have to do with the added ms
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(formatter)
// Decode the JSON response.
do {
let decodedResponse = try decoder.decode([Technician].self, from: data)
DispatchQueue.main.async {
self?.technicians = decodedResponse
}
} catch {
DispatchQueue.main.async {
print("Decoding error: \(error)")
}
}
}.resume()
}
// Update Technician Status function for the Update API
func updateTechnicianStatus(name: String, newStatus: String) {
let urlString = "https://techtracker.gibbyb.com/api/update_status_by_name?apikey=" + apiKey
2024-05-22 16:06:53 -05:00
guard let url = URL(string: urlString) else { return }
let updateData = [TechnicianUpdate(name: name, status: newStatus)]
// Wrap the array in a dictionary with a key that matches the backend expectation of an array.
// Even though this app only lets you update one technician at a time, the API was written for a
// web app that lets you update multiple technicians, so it expects an array.
let wrappedData = ["technicians": updateData]
guard let jsonData = try? JSONEncoder().encode(wrappedData) else {
print("Failed to encode update data")
return
}
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = jsonData
URLSession.shared.dataTask(with: request) { data, response, error in
if let error = error {
DispatchQueue.main.async {
print("Error updating technician: \(error.localizedDescription)")
}
return
}
guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
DispatchQueue.main.async {
print("Error with the response, unexpected status code: \(String(describing: response))")
}
return
}
// The database automatically adds this update to the history so we do not need to think about that in this app.
DispatchQueue.main.async {
print("Technician updated successfully.")
// Refresh technician data
self.fetchTechnicians()
}
}.resume()
}
// Fetch Technician History Function for the History API. Very similar to Technician API
// but with some added metadata
func fetchTechnicianHistory(page: Int = 1) {
let urlString = "https://techtracker.gibbyb.com/api/get_paginated_history?apikey=" + apiKey + "&page=\(page)"
2024-05-22 16:06:53 -05:00
guard let url = URL(string: urlString) else { return }
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
guard let data = data else {
DispatchQueue.main.async {
print("Networking error: \(error?.localizedDescription ?? "Unknown error")")
}
return
}
let decoder = JSONDecoder()
let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
decoder.dateDecodingStrategy = .formatted(formatter)
do {
let jsonResponse = try decoder.decode(HistoryResponse.self, from: data)
DispatchQueue.main.async {
self?.technicianHistories = jsonResponse.data
self?.totalPageCount = jsonResponse.meta.total_pages
self?.currentPage = jsonResponse.meta.current_page
}
} catch {
DispatchQueue.main.async {
print("Decoding error: \(error)")
}
}
}.resume()
}
func nextPage() {
guard currentPage < totalPageCount else {return}
currentPage += 1
fetchTechnicianHistory(page: currentPage)
}
func previousPage() {
guard currentPage > 1 else {return}
currentPage -= 1
fetchTechnicianHistory(page: currentPage)
}
}
struct ContentView: View {
@StateObject var viewModel = TechnicianViewModel()
@State private var showingUpdateView = false
@State private var selectedTechnicianName = ""
@State private var selectedTechnicianCurrStatus = ""
var body: some View {
HStack {
Image("logo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height:50)
.imageScale(.large)
.foregroundStyle(.tint)
Text("Tech Tracker")
.bold()
.font(.system(size: 36))
}
TabView {
List {
ForEach(viewModel.technicians) { technician in
Button(action: {
2024-07-19 08:11:18 -05:00
viewModel.fetchTechnicians()
2024-05-22 16:06:53 -05:00
self.selectedTechnicianName = technician.name
self.selectedTechnicianCurrStatus = technician.status
2024-07-19 08:11:18 -05:00
self.showingUpdateView = true
}) {
2024-05-22 16:06:53 -05:00
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(technician.name).bold()
.font(.system(size: 23))
Text(technician.status).lineLimit(2)
}
Spacer()
VStack(alignment: .trailing, spacing: 5) {
VStack {
Text(technician.updatedAt, format: .dateTime.hour().minute()).font(.system(size: 20))
Text(technician.updatedAt, format: .dateTime.day().month()).font(.system(size: 18))
2024-05-22 16:06:53 -05:00
}
}
}
.foregroundColor(.primary)
}
}
}
.tabItem {
Image(systemName: "pencil")
Text("Update")
}
.onAppear {
viewModel.fetchTechnicians()
}
.refreshable {
viewModel.fetchTechnicians()
}
.sheet(isPresented: $showingUpdateView) {
StatusUpdateView(isPresented: $showingUpdateView,
technicianName: selectedTechnicianName,
technicianStatus: selectedTechnicianCurrStatus,
updateAction: { newStatus in
viewModel.updateTechnicianStatus(name: selectedTechnicianName, newStatus: newStatus)
})
}
VStack {
List(viewModel.technicianHistories) { history in
HStack {
VStack(alignment: .leading, spacing: 5) {
Text(history.name).bold().font(.system(size: 22))
Text(history.status).lineLimit(2)
}
Spacer()
VStack(alignment: .trailing, spacing: 5) {
Text(history.updatedAt, format: .dateTime.hour().minute()).font(.system(size: 20))
Text(history.updatedAt, format: .dateTime.day().month()).font(.system(size: 18))
2024-05-22 16:06:53 -05:00
}
}
}
HStack {
Button("Prev") {
viewModel.previousPage()
}
.disabled(viewModel.currentPage <= 1)
.font(.title2)
Spacer()
Text("Page \(viewModel.currentPage) of \(viewModel.totalPageCount)").font(.title3)
Spacer()
Button("Next") {
viewModel.nextPage()
}
.disabled(viewModel.currentPage >= viewModel.totalPageCount)
.font(.title2)
}.padding()
}
.tabItem {
Image(systemName: "book")
Text("History")
}
.onAppear {
viewModel.fetchTechnicianHistory()
}
.refreshable {
viewModel.fetchTechnicianHistory()
}
}
}
}
#Preview {
ContentView()
}