Added notifications for visiting chefs

You can now get notified when visiting chefs are on campus! A menu is available from the toolbar on the main screen that allows you to enable notifications and configure what visiting chefs you want to be notified for. You can also toggle if you want to be notified 1, 2, or 3 hours before the chef arrives on campus.
Other changes in this commit:
- Updated maps URL to be compatible with the new RIT map (however they don't open correctly- this is outside of my control)
- Removed button linking to OnDemand at the request of RIT ITS
This commit is contained in:
2025-12-08 01:45:30 -05:00
parent d7096980d7
commit 207fa788e1
11 changed files with 371 additions and 91 deletions

View File

@@ -0,0 +1,117 @@
//
// DiningModel.swift
// RIT Dining
//
// Created by Campbell on 10/1/25.
//
import SwiftUI
@Observable
class DiningModel {
var locationsByDay = [[DiningLocation]]()
var daysRepresented = [Date]()
var lastRefreshed: Date?
var visitingChefPushes = VisitingChefPushesModel()
var notifyingChefs = NotifyingChefs()
// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all
// of the information that the app needs for the various view. Making it part of the model allows it to be updated from
// any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7
// calls per launch).
func getHoursByDay() async throws {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let week: [Date] = (0..<7).compactMap { offset in
calendar.date(byAdding: .day, value: offset, to: today)
}
daysRepresented = week
var newLocationsByDay = [[DiningLocation]]()
for day in week {
let dateString = day.formatted(.iso8601
.year().month().day()
.dateSeparator(.dash))
switch await getAllDiningInfo(date: dateString) {
case .success(let locations):
var newDiningLocations = [DiningLocation]()
for i in 0..<locations.locations.count {
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: day)
newDiningLocations.append(diningInfo)
}
newLocationsByDay.append(newDiningLocations)
case .failure(let error):
throw(error)
}
}
locationsByDay = newLocationsByDay
lastRefreshed = Date()
}
// Iterates through all of the locations and updates their open status indicator based on the current time. Does a replace
// to make sure that it updates any views observing this model.
func updateOpenStatuses() {
locationsByDay = locationsByDay.map { day in
day.map { location in
var location = location
location.updateOpenStatus()
return location
}
}
}
func scheduleAllPushes() async {
for day in locationsByDay {
for location in day {
if let visitingChefs = location.visitingChefs {
for chef in visitingChefs {
if notifyingChefs.contains(chef.name) {
await visitingChefPushes.scheduleNewPush(
name: chef.name,
location: location.name,
startTime: chef.openTime,
endTime: chef.closeTime
)
}
}
}
}
}
// Run a cleanup task once we're done scheduling.
await cleanupPushes()
}
// Cleanup old push notifications that have already gone by so we're not still tracking them forever and ever.
func cleanupPushes() async {
let now = Date()
for push in visitingChefPushes.pushes {
if now > push.endTime {
visitingChefPushes.pushes.remove(at: visitingChefPushes.pushes.firstIndex(of: push)!)
}
}
}
func cancelAllPushes() async {
let uuids = visitingChefPushes.pushes.map(\.uuid)
await cancelVisitingChefNotifs(uuids: uuids)
visitingChefPushes.pushes.removeAll()
}
func schedulePushesForChef(_ chefName: String) async {
for day in locationsByDay {
for location in day {
if let visitingChefs = location.visitingChefs {
for chef in visitingChefs {
if chef.name == chefName && notifyingChefs.contains(chef.name) {
await visitingChefPushes.scheduleNewPush(
name: chef.name,
location: location.name,
startTime: chef.openTime,
endTime: chef.closeTime
)
}
}
}
}
}
}
}

View File

@@ -0,0 +1,86 @@
//
// PushesModel.swift
// RIT Dining
//
// Created by Campbell on 11/20/25.
//
import SwiftUI
@Observable
class VisitingChefPushesModel {
var pushes: [ScheduledVistingChefPush] = [] {
didSet {
save()
}
}
private let key = "ScheduledVisitingChefPushes"
init() {
load()
}
/// Schedule a new push notification with the notification center and save its information to UserDefaults if it succeeded.
func scheduleNewPush(name: String, location: String, startTime: Date, endTime: Date) async {
guard !pushAlreadyRegisered(name: name, location: location, startTime: startTime, endTime: endTime) else { return }
let uuid_string = await scheduleVisitingChefNotif(
name: name,
location: location,
startTime: startTime,
endTime: endTime
)
// An empty UUID means that the notification wasn't scheduled for one reason or another. This is ignored for now.
if uuid_string != "" {
pushes.append(
ScheduledVistingChefPush(
uuid: uuid_string,
name: name,
location: location,
startTime: startTime,
endTime: endTime
)
)
save()
}
}
/// Cancel all reigstered push notifications for a specified visiting chef.
func cancelPushesForChef(name: String) {
var uuids: [String] = []
for push in pushes {
if push.name == name {
uuids.append(push.uuid)
}
}
Task {
await cancelVisitingChefNotifs(uuids: uuids)
}
// Once they're canceled, we can drop them from the list.
pushes.removeAll { $0.name == name }
save()
}
func pushAlreadyRegisered(name: String, location: String, startTime: Date, endTime: Date) -> Bool {
for push in pushes {
if push.name == name && push.location == location && push.startTime == startTime && push.endTime == endTime {
return true
}
}
return false
}
private func save() {
let encoder = JSONEncoder()
if let data = try? encoder.encode(pushes) {
UserDefaults.standard.set(data, forKey: key)
}
}
private func load() {
let decoder = JSONDecoder()
if let data = UserDefaults.standard.data(forKey: key),
let decoded = try? decoder.decode([ScheduledVistingChefPush].self, from: data) {
pushes = decoded
}
}
}

View File

@@ -1,59 +0,0 @@
//
// TigerCenterModel.swift
// RIT Dining
//
// Created by Campbell on 10/1/25.
//
import SwiftUI
@Observable
class DiningModel {
var locationsByDay = [[DiningLocation]]()
var daysRepresented = [Date]()
var lastRefreshed: Date?
// This is the actual method responsible for making requests to the API for the current day and next 6 days to collect all
// of the information that the app needs for the various view. Making it part of the model allows it to be updated from
// any view at any time, and prevents excess API requests (if you never refresh, the app will never need to make more than 7
// calls per launch).
func getHoursByDay() async throws {
let calendar = Calendar.current
let today = calendar.startOfDay(for: Date())
let week: [Date] = (0..<7).compactMap { offset in
calendar.date(byAdding: .day, value: offset, to: today)
}
daysRepresented = week
var newLocationsByDay = [[DiningLocation]]()
for day in week {
let dateString = day.formatted(.iso8601
.year().month().day()
.dateSeparator(.dash))
switch await getAllDiningInfo(date: dateString) {
case .success(let locations):
var newDiningLocations = [DiningLocation]()
for i in 0..<locations.locations.count {
let diningInfo = parseLocationInfo(location: locations.locations[i], forDate: day)
newDiningLocations.append(diningInfo)
}
newLocationsByDay.append(newDiningLocations)
case .failure(let error):
throw(error)
}
}
locationsByDay = newLocationsByDay
lastRefreshed = Date()
}
// Iterates through all of the locations and updates their open status indicator based on the current time. Does a replace
// to make sure that it updates any views observing this model.
func updateOpenStatuses() {
locationsByDay = locationsByDay.map { day in
day.map { location in
var location = location
location.updateOpenStatus()
return location
}
}
}
}

View File

@@ -0,0 +1,17 @@
//
// PushTypes.swift
// RIT Dining
//
// Created by Campbell on 11/20/25.
//
import Foundation
/// Struct to represent a visiting chef notification that has already been scheduled, allowing it to be loaded again later to recall what notifications have been scheduled.
struct ScheduledVistingChefPush: Codable, Equatable {
let uuid: String
let name: String
let location: String
let startTime: Date
let endTime: Date
}