From 145135721943b3aa9d34db99f5b3ba965a09f34c Mon Sep 17 00:00:00 2001
From: William Walker <wnwalker@ucsc.edu>
Date: Mon, 5 Aug 2024 12:02:10 -0700
Subject: [PATCH] .

---
 build.gradle.kts                              |   1 +
 gradle/libs.versions.toml                     |  50 ++--
 settings.gradle.kts                           |   2 +-
 shared/compose/NOTICE                         |   7 +-
 shared/compose/build.gradle.kts               |   1 +
 .../composeResources/values/strings.xml       |  17 ++
 .../its/temerity/shared/ui/LoginScreen.kt     |   9 +-
 .../ucsc/its/temerity/shared/ui/MainScreen.kt | 194 +++++++++-----
 .../ucsc/its/temerity/shared/ui/RootScreen.kt |  25 +-
 .../its/temerity/shared/ui/TemerityApp.kt     |  12 +-
 .../its/temerity/shared/ui/UserEditScreen.kt  | 253 ++++++++++++++++++
 .../shared/ui/elements/CoursesElements.kt     | 129 +++++++++
 .../shared/ui/elements/UserElements.kt        |  60 +++--
 .../temerity/shared/ui/elements/Utility.kt    | 147 +++++++++-
 shared/shared/build.gradle.kts                |   9 +-
 .../ucsc/its/temerity/shared/AppSettings.kt   |  62 +++--
 .../data/repository/PlatformRepository.kt     | 105 ++++----
 .../temerity/shared/database/AppDatabase.kt   |  39 ++-
 .../temerity/shared/database/TemerityDao.kt   |  30 ++-
 .../its/temerity/shared/di/CommonModule.kt    |  12 +-
 .../{AppStateViewModel.kt => AppViewModel.kt} |   7 +-
 .../viewmodel/DevicesScreenViewModel.kt       |  58 ++--
 .../shared/viewmodel/LoginViewModel.kt        |   3 +-
 .../shared/viewmodel/MoleculeViewModel.kt     |  33 ++-
 .../shared/viewmodel/RootViewModel.kt         |  37 ++-
 .../shared/viewmodel/UserEditViewModel.kt     | 118 ++++++++
 .../shared/data/Actual.StorageModule.kt       |  31 +--
 .../edu/ucsc/its/temerity/TemerityLibrary.kt  |   2 +-
 .../ucsc/its/temerity/remote/PlatformApi.kt   |   4 +-
 29 files changed, 1135 insertions(+), 322 deletions(-)
 create mode 100644 shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/UserEditScreen.kt
 create mode 100644 shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/CoursesElements.kt
 rename shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/{AppStateViewModel.kt => AppViewModel.kt} (95%)
 create mode 100644 shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/UserEditViewModel.kt

diff --git a/build.gradle.kts b/build.gradle.kts
index 09a85f9..1f9c63b 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,6 +7,7 @@ plugins {
     alias(libs.plugins.kotlinMultiplatform) apply false
     alias(libs.plugins.kotlinxSerialization) apply false
     alias(libs.plugins.jetbrainsCompose) apply false
+    alias(libs.plugins.composeCompiler) apply false
     alias(libs.plugins.dokka) apply false
     alias(libs.plugins.spotless) apply false
 }
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index cf0d405..c10566e 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -26,33 +26,32 @@ jetbrainsLifecycle = "2.8.0"
 androidxRoom = "2.7.0-alpha05"
 sqlite = "2.5.0-SNAPSHOT"
 androidxDatastore = "1.1.1"
+exposed = "0.53.0"
 
 material3WindowSizeClass = "0.5.0"
 landscapistCoil3 = "2.3.6"
 composetheme = "1.2.0-alpha"
 voyager = "1.1.0-beta02"
 
-composables = "1.7.0"
-bottomsheet = "0.1.3"
-alertKmp = "0.0.7"
-
 dotenv-vault = "0.0.3"
+kermit = "2.0.4"
+jline = "3.26.3"
+appdirs = "1.2.0"
+kstore = "0.7.2"
 
 kotlinx-dataframe = "0.13.1"
 klaxon = "5.6"
 kotest = "5.9.1"
 
-kermit = "2.0.4"
+composables = "1.10.0"
+bottomsheet = "0.1.3"
+alertKmp = "0.0.7"
+filekit = "0.7.0"
 
 dokka = "1.9.20"
 spotless = "6.25.0"
 gitSemVer = "3.1.4"
 
-
-jline = "3.25.1"
-appdirs = "1.2.2"
-kstore = "0.7.2"
-
 minSdk = "24"
 targetSdk = "34"
 compileSdk = "34"
@@ -72,16 +71,28 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
 kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
 kotlinx-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "coroutines" }
 kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
+kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
 kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "datetime" }
 
 ktorfit-lib = { module = "de.jensklingenberg.ktorfit:ktorfit-lib-light", version.ref = "ktorfit" }
 
+kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
+kermit-koin = { module = "co.touchlab:kermit-koin", version.ref = "kermit" }
+
+jline = { module = "org.jline:jline", version.ref = "jline" }
+appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "appdirs" }
+kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
+kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
+
+
 sandwich = { module = "com.github.skydoves:sandwich", version.ref = "sandwich" }
 sandwich-ktor = { module = "com.github.skydoves:sandwich-ktor", version.ref = "sandwich" }
 sandwich-ktorfit = { module = "com.github.skydoves:sandwich-ktorfit", version.ref = "sandwich" }
 koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
 arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
-arrow-fx-coroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" }
+arrow-fxCoroutines = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" }
+arrow-optics = { module = "io.arrow-kt:arrow-optics", version.ref = "arrow" }
+arrow-opticsKspPlugin = { module = "io.arrow-kt:arrow-optics-ksp-plugin", version.ref = "arrow" }
 
 mosaic = { module = "com.jakewharton.mosaic:mosaic-runtime", version.ref = "mosaic" }
 koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinComposeMultiplatform" }
@@ -103,6 +114,8 @@ landscapist-coil3 = { module = "com.github.skydoves:landscapist-coil3", version.
 composables-core = { module = "com.composables:core", version.ref = "composables" }
 flexible-bottomsheet = { module = "com.github.skydoves:flexible-bottomsheet", version.ref = "bottomsheet" }
 alertKmp = { module = "io.github.khubaibkhan4:alert-kmp", version.ref = "alertKmp" }
+filekit-core = { group = "io.github.vinceglb", name = "filekit-core", version.ref = "filekit" }
+filekit-compose = { group = "io.github.vinceglb", name = "filekit-compose", version.ref = "filekit" }
 
 # Shared presenter code deps (some usages are platform-specific)
 molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" }
@@ -111,7 +124,10 @@ jetbrains-navigation-compose = { module = "org.jetbrains.androidx.navigation:nav
 androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
 androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
 sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
-
+exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
+exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" }
+exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
+exposed-kotlinDatetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" }
 
 # Testing deps
 dotenv-vault = { module = "com.github.dotenv-org:dotenv-vault-kotlin", version.ref = "dotenv-vault" }
@@ -126,20 +142,12 @@ dokka-base = { module = "org.jetbrains.dokka:dokka-base", version.ref = "dokka"
 android-documentation-plugin = { module = "org.jetbrains.dokka:android-documentation-plugin", version.ref = "dokka" }
 
 klaxon = { module = "com.beust:klaxon", version.ref = "klaxon" }
-kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
-kermit-koin = { module = "co.touchlab:kermit-koin", version.ref = "kermit" }
-
-kotlinx-coroutines-slf4j = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-slf4j", version.ref = "coroutines" }
-
-jline = { module = "org.jline:jline", version.ref = "jline" }
-appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
-kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
-kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
 
 [bundles]
 ktor = ["ktor-client-logging", "ktor-client-serialization", "kotlinx-serialization-json"]
 sandwich = ["sandwich", "sandwich-ktor", "sandwich-ktorfit"]
 voyager = ["voyager-navigator", "voyager-bottomSheetNavigator", "voyager-tabNavigator", "voyager-lifecycle-kmp", "voyager-transitions", "voyager-koin"]
+exposed = ["exposed-core", "exposed-crypt", "exposed-dao", "exposed-kotlinDatetime"]
 
 [plugins]
 androidApplication = { id = "com.android.application", version.ref = "agp" }
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 2fec6e4..4c61df6 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -13,7 +13,7 @@ dependencyResolutionManagement {
     repositories {
         google()
         mavenCentral()
-		maven { url = uri("https://jitpack.io") }
+        maven { url = uri("https://jitpack.io") }
         maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
         maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental")
         maven("https://androidx.dev/storage/compose-compiler/repository")
diff --git a/shared/compose/NOTICE b/shared/compose/NOTICE
index 7096b0d..7f911eb 100644
--- a/shared/compose/NOTICE
+++ b/shared/compose/NOTICE
@@ -18,8 +18,11 @@ License: None
 
 This product includes software developed by Composable Horizons (Alex Styl)
 
-File(s): ListDetailLayoutWithSearchBar.kt
-Description: A material 3 responsive layout built using the canonical list-detail pattern.
+File(s): ListDetailLayoutWithSearchBar.kt, UserEditScreen.kt, Utility.kt
+Descriptions:
+    - A material 3 responsive layout built using the canonical list-detail pattern.
+    - A screen containing a form for editing user details.
+    - A simple dropdown menu with chevron down icon, basic text items
 License: Purchased 06/2024
 
 --------------------------------------------
\ No newline at end of file
diff --git a/shared/compose/build.gradle.kts b/shared/compose/build.gradle.kts
index 9e5b0a4..659c61d 100644
--- a/shared/compose/build.gradle.kts
+++ b/shared/compose/build.gradle.kts
@@ -53,6 +53,7 @@ kotlin {
                 implementation(libs.composeTheme.material3)
                 implementation(libs.bundles.voyager)
                 implementation(libs.alertKmp)
+                implementation(libs.composables.core)
 
                 implementation(libs.dotenv.vault)
             }
diff --git a/shared/compose/src/commonMain/composeResources/values/strings.xml b/shared/compose/src/commonMain/composeResources/values/strings.xml
index 1400b07..3d7925d 100644
--- a/shared/compose/src/commonMain/composeResources/values/strings.xml
+++ b/shared/compose/src/commonMain/composeResources/values/strings.xml
@@ -19,8 +19,25 @@
     <string name="app_name">Temerity</string>
     <string name="login">Login</string>
     <string name="main">Main</string>
+
     <string name="notifications">Notifications</string>
+    <string name="notifications_short">Notifs.</string>
     <string name="devices">Devices</string>
     <string name="roster">Roster</string>
+    <string name="courses">Courses</string>
     <string name="settings">Settings</string>
+
+    <!-- Strings used in the User Screens -->
+    <string name="user_edit">Edit User Record</string>
+
+    <!-- Action item text -->
+    <string name="blank">No content</string>
+    <string name="save">Save</string>
+
+    <!-- Strings used in the Courses Screens -->
+    <string name="course_code_prefix">Course code: </string>
+    <string name="term_code_prefix">Term: </string>
+    <string name="direct_course_link_prefix">Direct course link: </string>
+    <string name="course_edit">Edit Course</string>
+
 </resources>
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/LoginScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/LoginScreen.kt
index 61cf79a..a02b701 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/LoginScreen.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/LoginScreen.kt
@@ -28,18 +28,17 @@ import androidx.compose.runtime.collectAsState
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
-import edu.ucsc.its.temerity.shared.viewmodel.AppStateViewModel
+import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.LoginViewModel
-import kotlinx.coroutines.ExperimentalCoroutinesApi
 import org.koin.compose.getKoin
 import org.koin.compose.viewmodel.koinViewModel
 import org.koin.core.annotation.KoinExperimentalAPI
 import org.koin.core.parameter.parametersOf
 
-@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class)
+@OptIn(KoinExperimentalAPI::class)
 @Composable
 fun LoginScreen(
-  appStateViewModel: AppStateViewModel = koinViewModel(),
+  appViewModel: AppViewModel = koinViewModel(),
   viewModel: LoginViewModel = koinViewModel(),
   onLogin: () -> Unit,
 ) {
@@ -82,7 +81,7 @@ fun LoginScreen(
 //    )
     Spacer(modifier = Modifier.height(16.dp))
     Text(
-      text = getKoin().get<String> { parametersOf("temerity", "1.0.0", "edu.ucsc.its") },
+      text = getKoin().get<String> { parametersOf("edu.ucsc.its.temerity") },
     )
     Spacer(modifier = Modifier.height(16.dp))
     Button(
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
index 353ae85..96af97f 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/MainScreen.kt
@@ -19,9 +19,6 @@ package edu.ucsc.its.temerity.shared.ui
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.fadeIn
 import androidx.compose.animation.fadeOut
-import androidx.compose.animation.slideInHorizontally
-import androidx.compose.animation.slideOutHorizontally
-import androidx.compose.animation.slideOutVertically
 import androidx.compose.foundation.background
 import androidx.compose.foundation.border
 import androidx.compose.foundation.layout.Arrangement
@@ -44,6 +41,7 @@ import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.outlined.AdUnits
 import androidx.compose.material.icons.outlined.Contacts
 import androidx.compose.material.icons.outlined.Edit
+import androidx.compose.material.icons.outlined.Group
 import androidx.compose.material.icons.outlined.Notifications
 import androidx.compose.material.icons.outlined.Settings
 import androidx.compose.material.icons.rounded.Clear
@@ -80,48 +78,83 @@ import androidx.compose.ui.graphics.vector.ImageVector
 import androidx.compose.ui.text.input.ImeAction
 import androidx.compose.ui.text.input.KeyboardType
 import androidx.compose.ui.unit.dp
-import androidx.navigation.compose.NavHost
+import androidx.lifecycle.viewModelScope
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.createGraph
+import androidx.navigation.toRoute
 import com.composables.composetheme.ComposeTheme
 import com.composables.composetheme.material3.colorScheme
 import edu.ucsc.its.temerity.PlatformClient
+import edu.ucsc.its.temerity.remote.Course
 import edu.ucsc.its.temerity.remote.Device
 import edu.ucsc.its.temerity.remote.User
+import edu.ucsc.its.temerity.shared.ui.MainContentWindow.Courses
+import edu.ucsc.its.temerity.shared.ui.MainContentWindow.Devices
+import edu.ucsc.its.temerity.shared.ui.MainContentWindow.Notifications
+import edu.ucsc.its.temerity.shared.ui.MainContentWindow.Roster
+import edu.ucsc.its.temerity.shared.ui.MainContentWindow.Settings
+import edu.ucsc.its.temerity.shared.ui.elements.CoursesList
 import edu.ucsc.its.temerity.shared.ui.elements.DeviceList
+import edu.ucsc.its.temerity.shared.ui.elements.NewNavHost
 import edu.ucsc.its.temerity.shared.ui.elements.UserList
+import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel
 import kotlinx.coroutines.Dispatchers.IO
+import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.Json
 import org.dotenv.vault.dotenvVault
 import org.jetbrains.compose.resources.StringResource
+import org.jetbrains.compose.resources.stringResource
 import org.koin.compose.getKoin
+import org.koin.compose.viewmodel.koinViewModel
+import org.koin.core.annotation.KoinExperimentalAPI
 import org.koin.core.parameter.parametersOf
 import temerity.shared.compose.generated.resources.Res
+import temerity.shared.compose.generated.resources.courses
 import temerity.shared.compose.generated.resources.devices
 import temerity.shared.compose.generated.resources.notifications
+import temerity.shared.compose.generated.resources.notifications_short
 import temerity.shared.compose.generated.resources.roster
 import temerity.shared.compose.generated.resources.settings
-import temerity.shared.compose.generated.resources.user_edit
 
-enum class MainContentWindow(val title: StringResource) {
-  Notifications(title = Res.string.notifications),
-  Devices(title = Res.string.devices),
-  Roster(title = Res.string.roster),
-  Settings(title = Res.string.settings),
+sealed class MainContentWindow {
+  @Serializable
+  data object Notifications : MainContentWindow()
+
+  @Serializable
+  data object Devices : MainContentWindow()
+
+  @Serializable
+  data object Roster : MainContentWindow()
+
+  @Serializable
+  data object Courses : MainContentWindow()
+
+  @Serializable
+  data object Settings : MainContentWindow()
 }
 
-enum class MainDetailWindow(val title: StringResource) {
-  UserEditScreen(title = Res.string.user_edit),
+sealed class UserDetailView {
+  @Serializable
+  data object BlankScreen : UserDetailView()
+
+  @Serializable
+  data class EditScreen(val user: String) : UserDetailView()
 }
 
-@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
+@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, KoinExperimentalAPI::class)
 @Composable
 fun MainScreen() {
+  val appViewModel: AppViewModel = koinViewModel()
+  val appViewModelScope = appViewModel.viewModelScope
   // Set initial main content pane to "Devices"
   var navigationIndex by remember { mutableStateOf(1) }
-  val selectedDeviceIndex by remember { mutableStateOf(0) }
-  val widthSizeClass = calculateWindowSizeClass().widthSizeClass
+  val windowSizeClass = calculateWindowSizeClass()
+  val widthSizeClass = windowSizeClass.widthSizeClass
 
   val dotenv = dotenvVault {
     directory = "../temerity"
@@ -129,49 +162,73 @@ fun MainScreen() {
   val pc: PlatformClient = getKoin().get(parameters = { parametersOf(dotenv["YUJAPROD_API_URL"], dotenv["YUJAPROD_TOKEN"]) })
   var devicesList by remember { mutableStateOf(listOf<Device>()) }
   var usersList by remember { mutableStateOf(listOf<User>()) }
+  var coursesList by remember { mutableStateOf(listOf<Course>()) }
+  var searchQuery by remember { mutableStateOf(String()) }
 
   LaunchedEffect(pc) {
-    withContext(IO) {
-      devicesList = pc.getDevices().sortedBy { it.stationName }
-      usersList = pc.getUsers().sortedBy { it.lastName }
+    appViewModelScope.launch {
+      withContext(IO) {
+        devicesList = pc.getDevices().sortedBy { it.stationName }
+        usersList = pc.getUsers().sortedBy { it.lastName }
+        coursesList = pc.getCourses()
+      }
+    }
+  }
+
+  val userDetailViewController = rememberNavController()
+  val userDetailViewGraph = userDetailViewController.createGraph(
+    startDestination = UserDetailView.BlankScreen,
+  ) {
+    composable<UserDetailView.BlankScreen> {
+      Box {
+      }
+    }
+    composable<UserDetailView.EditScreen> {
+      val args = it.toRoute<UserDetailView.EditScreen>()
+      val user = Json.decodeFromString<User>(args.user)
+      Box {
+        UserEditScreen(user)
+      }
     }
   }
 
   val contentPaneNavController = rememberNavController()
   val contentPaneNavGraph = contentPaneNavController.createGraph(
-    startDestination = MainContentWindow.Devices.name,
+    startDestination = Devices,
   ) {
-    composable(MainContentWindow.Notifications.name) {
+    composable<Notifications> {
       // NotificationsScreen()
     }
-    composable(MainContentWindow.Devices.name) {
+    composable<Devices> {
       DeviceList(
         devicesList,
       )
     }
-    composable(MainContentWindow.Roster.name) {
+    composable<Roster> {
       UserList(
         usersList,
-      )
+      ) {
+        userDetailViewController.popBackStack()
+        userDetailViewController.navigate(UserDetailView.EditScreen(Json.encodeToString(it)))
+      }
     }
-    composable(MainContentWindow.Settings.name) {
-      // SettingsScreen()
+    composable<Courses> {
+      CoursesList(
+        coursesList,
+      ) {
+      }
     }
-  }
-  val detailPaneNavController = rememberNavController()
-  val detailPaneNavGraph = detailPaneNavController.createGraph(
-    startDestination = MainDetailWindow.UserEditScreen,
-  ) {
-    composable(MainDetailWindow.UserEditScreen.name) {
-      UserEditScreen()
+    composable<Settings> {
+      // SettingsScreen()
     }
   }
 
   val navigationItems = listOf(
-    mapOf("icon" to Icons.Outlined.Notifications, "label" to "Notifications"),
-    mapOf("icon" to Icons.Outlined.AdUnits, "label" to "Devices"),
-    mapOf("icon" to Icons.Outlined.Contacts, "label" to "Roster"),
-    mapOf("icon" to Icons.Outlined.Settings, "label" to "Settings"),
+    mapOf("icon" to Icons.Outlined.Notifications, "label" to if (widthSizeClass == WindowWidthSizeClass.Compact) Res.string.notifications_short else Res.string.notifications, "route" to Notifications),
+    mapOf("icon" to Icons.Outlined.AdUnits, "label" to Res.string.devices, "route" to Devices),
+    mapOf("icon" to Icons.Outlined.Contacts, "label" to Res.string.roster, "route" to Roster),
+    mapOf("icon" to Icons.Outlined.Group, "label" to Res.string.courses, "route" to Courses),
+    mapOf("icon" to Icons.Outlined.Settings, "label" to Res.string.settings, "route" to Settings),
   )
 
   @Composable
@@ -202,14 +259,31 @@ fun MainScreen() {
     }
   }
 
+  fun filterContent(currentNavDestination: String) {
+//    when(contentPaneNavController.findDestination(currentNavDestination)) {
+//      is Devices -> {
+//        devicesList = devicesList.filter { it.stationName.contains(searchQuery, ignoreCase = true) }
+//      }
+//      is Roster -> {
+//        usersList = usersList.filter { it.loginId.contains(searchQuery, ignoreCase = true) }
+//      }
+//      is Courses -> {
+//        coursesList = coursesList.filter { it.courseCode.contains(searchQuery, ignoreCase = true) }
+//      }
+//
+//      Notifications -> TODO()
+//      Settings -> TODO()
+//    }
+  }
+
   @Composable
   fun ContentPanel(modifier: Modifier = Modifier) {
-    var searchQuery by remember { mutableStateOf("") }
     Column(modifier) {
       SearchBar(
         searchQuery = searchQuery,
         onSearchQueryChange = {
           searchQuery = it
+          contentPaneNavController.currentDestination!!.route?.let { currentNavDestination: String -> filterContent(currentNavDestination) }
         },
         onImeSearch = { /*TODO*/ },
       )
@@ -222,18 +296,9 @@ fun MainScreen() {
           .clip(RoundedCornerShape(18.dp))
           .background(ComposeTheme.colorScheme.surface),
       ) {
-        NavHost(
-          navController = contentPaneNavController,
-          graph = contentPaneNavGraph,
-          enterTransition = {
-            fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 })
-          },
-          exitTransition = {
-            fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 })
-          },
-          popExitTransition = {
-            fadeOut() + slideOutVertically(targetOffsetY = { -it })
-          },
+        NewNavHost(
+          contentPaneNavController,
+          contentPaneNavGraph,
         )
       }
     }
@@ -249,18 +314,9 @@ fun MainScreen() {
         .clip(RoundedCornerShape(18.dp))
         .background(ComposeTheme.colorScheme.surface),
     ) {
-      NavHost(
-        navController = detailPaneNavController,
-        graph = detailPaneNavGraph,
-        enterTransition = {
-          fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 })
-        },
-        exitTransition = {
-          fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 })
-        },
-        popExitTransition = {
-          fadeOut() + slideOutVertically(targetOffsetY = { -it })
-        },
+      NewNavHost(
+        navController = userDetailViewController,
+        navGraph = userDetailViewGraph,
       )
     }
   }
@@ -275,11 +331,13 @@ fun MainScreen() {
         navigationItems.forEachIndexed { index, _ ->
           val item = navigationItems[index]
           val icon = item["icon"] as ImageVector
-          val label = item["label"] as String
-          NavigationBarItem(selected = index == navigationIndex, icon = { Icon(icon, null) }, label = { Text(label) }, onClick = {
+          val label = item["label"] as StringResource
+          val route = item["route"] as MainContentWindow
+          NavigationBarItem(selected = index == navigationIndex, icon = { Icon(icon, null) }, label = { Text(stringResource(label)) }, onClick = {
             if (index != navigationIndex) {
               navigationIndex = index
-              contentPaneNavController.navigate(label.lowercase())
+              contentPaneNavController.popBackStack()
+              contentPaneNavController.navigate(route)
             }
           })
         }
@@ -307,16 +365,18 @@ fun MainScreen() {
                 itemsIndexed(navigationItems) { index, _ ->
                   val item = navigationItems[index]
                   val icon = item["icon"] as ImageVector
-                  val label = item["label"] as String
+                  val label = item["label"] as StringResource
+                  val route = item["route"] as MainContentWindow
                   NavigationDrawerItem(
                     modifier = Modifier.padding(horizontal = 16.dp),
                     selected = index == navigationIndex,
                     icon = { Icon(icon, null) },
-                    label = { Text(label) },
+                    label = { Text(stringResource(label)) },
                     onClick = {
                       if (index != navigationIndex) {
                         navigationIndex = index
-                        contentPaneNavController.navigate(label.lowercase())
+                        contentPaneNavController.popBackStack()
+                        contentPaneNavController.navigate(route)
                       }
                     },
                   )
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/RootScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/RootScreen.kt
index 1da1cd9..d88363d 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/RootScreen.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/RootScreen.kt
@@ -21,31 +21,28 @@ import androidx.navigation.compose.NavHost
 import androidx.navigation.compose.composable
 import androidx.navigation.compose.rememberNavController
 import androidx.navigation.createGraph
-import org.jetbrains.compose.resources.StringResource
-import temerity.shared.compose.generated.resources.Res
-import temerity.shared.compose.generated.resources.login
-import temerity.shared.compose.generated.resources.main
+import kotlinx.serialization.Serializable
 
-/**
- * enum values to represent different root windows in the application
- */
-enum class TemerityWindow(val title: StringResource) {
-  Login(title = Res.string.login),
-  Main(title = Res.string.main),
+object TemerityWindow {
+  @Serializable
+  object Login
+
+  @Serializable
+  object Main
 }
 
 @Composable
 fun RootScreen() {
   val navController = rememberNavController()
   val navGraph = navController.createGraph(
-    startDestination = TemerityWindow.Login.name,
+    startDestination = TemerityWindow.Login,
   ) {
-    composable(TemerityWindow.Login.name) {
+    composable<TemerityWindow.Login> {
       LoginScreen(onLogin = {
-        navController.navigate(TemerityWindow.Main.name)
+        navController.navigate(TemerityWindow.Main)
       })
     }
-    composable(TemerityWindow.Main.name) {
+    composable<TemerityWindow.Main> {
       MainScreen()
     }
   }
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
index 89f09bc..97c2c1a 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/TemerityApp.kt
@@ -21,7 +21,6 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.Surface
 import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
-import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import com.composables.composetheme.ComposeTheme
@@ -30,7 +29,7 @@ import edu.ucsc.its.temerity.shared.data.repository.PlatformRepository
 import edu.ucsc.its.temerity.shared.di.initKoin
 import edu.ucsc.its.temerity.shared.ui.theme.DarkTheme
 import edu.ucsc.its.temerity.shared.ui.theme.LightTheme
-import edu.ucsc.its.temerity.shared.viewmodel.AppStateViewModel
+import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.RootViewModel
 import org.koin.compose.koinInject
 import org.koin.compose.viewmodel.koinViewModel
@@ -41,16 +40,13 @@ private val koin = initKoin().koin
 @OptIn(KoinExperimentalAPI::class, ExperimentalMaterial3WindowSizeClassApi::class)
 @Composable
 internal fun TemerityApp(
-  appStateViewModel: AppStateViewModel = koinViewModel(),
+  appStateViewModel: AppViewModel = koinViewModel(),
   rootViewModel: RootViewModel = koinViewModel(),
   platformRepository: PlatformRepository = koinInject(),
 ) {
-  val windowSizeClass = calculateWindowSizeClass()
+  val appTheme = if (isSystemInDarkTheme()) DarkTheme else LightTheme
 
-  @Suppress("PropertyName")
-  val AppTheme = if (isSystemInDarkTheme()) DarkTheme else LightTheme
-
-  AppTheme {
+  appTheme {
     Surface(
       modifier = Modifier.fillMaxSize(),
       color = ComposeTheme.colorScheme.surface,
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/UserEditScreen.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/UserEditScreen.kt
new file mode 100644
index 0000000..ee2c80d
--- /dev/null
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/UserEditScreen.kt
@@ -0,0 +1,253 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package edu.ucsc.its.temerity.shared.ui
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.Abc
+import androidx.compose.material.icons.outlined.Cancel
+import androidx.compose.material.icons.outlined.Email
+import androidx.compose.material.icons.outlined.Person
+import androidx.compose.material.icons.outlined.Save
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.FloatingActionButton
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.OutlinedTextField
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusDirection
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.viewModelScope
+import com.composables.composetheme.ComposeTheme
+import com.composables.composetheme.material3.colorScheme
+import com.composables.composetheme.material3.typography
+import edu.ucsc.its.temerity.remote.User
+import edu.ucsc.its.temerity.shared.ui.elements.DropdownMenu
+import edu.ucsc.its.temerity.shared.ui.elements.ScrollableContent
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditViewModel
+import kotlinx.coroutines.launch
+import org.jetbrains.compose.resources.stringResource
+import org.koin.compose.viewmodel.koinViewModel
+import org.koin.core.annotation.KoinExperimentalAPI
+import temerity.shared.compose.generated.resources.Res
+import temerity.shared.compose.generated.resources.save
+import temerity.shared.compose.generated.resources.user_edit
+
+@OptIn(KoinExperimentalAPI::class)
+@Composable
+fun UserEditScreen(user: User? = null) {
+  val focusManager = LocalFocusManager.current
+  val viewmodel: UserEditViewModel = koinViewModel()
+  val viewModelScope = viewmodel.viewModelScope
+  LaunchedEffect(user) {
+    viewModelScope.launch { viewmodel.loadUser(user) }
+  }
+  val userEditState by viewmodel.models.collectAsState()
+
+  Scaffold(
+    floatingActionButton = {
+      FloatingActionButton(onClick = { viewModelScope.launch { viewmodel.saveUserEdits() } }) {
+        Icon(Icons.Outlined.Save, contentDescription = stringResource(Res.string.save))
+      }
+    },
+  ) {
+    ScrollableContent {
+      item {
+        Text(
+          text = stringResource(Res.string.user_edit),
+          modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 8.dp),
+          color = ComposeTheme.colorScheme.onSurface,
+          style = ComposeTheme.typography.titleMedium,
+        )
+      }
+      item {
+        val fieldName = "firstName"
+        Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+          Icon(Icons.Outlined.Person, null)
+          OutlinedTextField(
+            modifier = Modifier.weight(1f),
+            label = { Text("First name") },
+            value = userEditState.firstName,
+            onValueChange = { viewModelScope.launch { viewmodel.updateField(fieldName, it) } },
+            singleLine = true,
+            trailingIcon = {
+              AnimatedVisibility(visible = userEditState.firstName.isNotBlank(), enter = fadeIn(), exit = fadeOut()) {
+                IconButton(onClick = { viewModelScope.launch { viewmodel.clearField(fieldName) } }) {
+                  Icon(Icons.Outlined.Cancel, "Clear")
+                }
+              }
+            },
+            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next, capitalization = KeyboardCapitalization.Words),
+            keyboardActions = KeyboardActions {
+              focusManager.moveFocus(FocusDirection.Next)
+            },
+          )
+        }
+      }
+      item {
+        val fieldName = "lastName"
+        Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+          Box(Modifier.size(24.dp))
+          OutlinedTextField(
+            modifier = Modifier.weight(1f),
+            label = { Text("Last name") },
+            value = userEditState.lastName,
+            onValueChange = { viewModelScope.launch { viewmodel.updateField(fieldName, it) } },
+            singleLine = true,
+            trailingIcon = {
+              AnimatedVisibility(visible = userEditState.lastName.isNotBlank(), enter = fadeIn(), exit = fadeOut()) {
+                IconButton(onClick = { viewModelScope.launch { viewmodel.clearField(fieldName) } }) {
+                  Icon(Icons.Outlined.Cancel, "Clear")
+                }
+              }
+            },
+            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next, capitalization = KeyboardCapitalization.Words),
+            keyboardActions = KeyboardActions {
+              focusManager.moveFocus(FocusDirection.Next)
+            },
+          )
+        }
+      }
+      item { Spacer(Modifier.height(4.dp)) }
+      item {
+        val fieldName = "loginId"
+        Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+          Icon(Icons.Outlined.Abc, null)
+          OutlinedTextField(
+            modifier = Modifier.weight(1f),
+            label = { Text("Login ID") },
+            value = userEditState.loginId,
+            onValueChange = { viewModelScope.launch { viewmodel.updateField(fieldName, it) } },
+            trailingIcon = {
+              AnimatedVisibility(visible = userEditState.loginId.isNotBlank(), enter = fadeIn(), exit = fadeOut()) {
+                IconButton(onClick = { viewModelScope.launch { viewmodel.clearField(fieldName) } }) {
+                  Icon(Icons.Outlined.Cancel, "Clear")
+                }
+              }
+            },
+            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Next),
+            keyboardActions = KeyboardActions {
+              focusManager.moveFocus(FocusDirection.Next)
+            },
+            singleLine = true,
+          )
+        }
+      }
+      item { Spacer(Modifier.height(4.dp)) }
+      item {
+        val fieldName = "emailAddress"
+        Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp)) {
+          Icon(Icons.Outlined.Email, null)
+          OutlinedTextField(
+            modifier = Modifier.weight(1f),
+            label = { Text("Email Address") },
+            value = userEditState.emailAddress,
+            onValueChange = { viewModelScope.launch { viewmodel.updateField(fieldName, it) } },
+            trailingIcon = {
+              AnimatedVisibility(visible = userEditState.emailAddress.isNotBlank(), enter = fadeIn(), exit = fadeOut()) {
+                IconButton(onClick = { viewModelScope.launch { viewmodel.clearField(fieldName) } }) {
+                  Icon(Icons.Outlined.Cancel, "Clear")
+                }
+              }
+            },
+            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email, imeAction = ImeAction.Done),
+            keyboardActions = KeyboardActions {
+              focusManager.clearFocus()
+            },
+            singleLine = true,
+          )
+        }
+      }
+      item { Spacer(Modifier.height(4.dp)) }
+      item {
+        Text(
+          text = "Timezone",
+          modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 8.dp),
+          color = ComposeTheme.colorScheme.onSurface,
+          style = ComposeTheme.typography.titleMedium,
+        )
+      }
+      item {
+        DropdownMenu(
+          items = listOf(
+            "America/Los_Angeles",
+            "America/New_York",
+            "Canada/East",
+          ),
+          onItemSelected = {},
+          modifier = Modifier.height(40.dp).fillMaxWidth(),
+        )
+      }
+      item {
+        Text(text = "User Role", style = ComposeTheme.typography.titleMedium, modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 8.dp))
+      }
+      item {
+        DropdownMenu(
+          items = listOf(
+            "Student",
+            "Instructor",
+            "IT Manager",
+          ),
+          onItemSelected = {},
+          modifier = Modifier.height(40.dp).fillMaxWidth(),
+        )
+      }
+      item {
+        Text(
+          text = "Extra Options",
+          modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 24.dp, bottom = 8.dp),
+          color = ComposeTheme.colorScheme.onSurface,
+          style = ComposeTheme.typography.titleMedium,
+        )
+      }
+      item {
+        var selected by remember { mutableStateOf(false) }
+        Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(24.dp), modifier = Modifier.clickable { selected = selected.not() }.fillMaxWidth().padding(16.dp)) {
+          Checkbox(checked = selected, onCheckedChange = null)
+          Text(text = "Lock User")
+        }
+      }
+    }
+  }
+}
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/CoursesElements.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/CoursesElements.kt
new file mode 100644
index 0000000..2317f3a
--- /dev/null
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/CoursesElements.kt
@@ -0,0 +1,129 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package edu.ucsc.its.temerity.shared.ui.elements
+
+import androidx.compose.desktop.ui.tooling.preview.Preview
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.LinkAnnotation
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withLink
+import androidx.compose.ui.unit.dp
+import com.composables.composetheme.ComposeTheme
+import com.composables.composetheme.material3.colorScheme
+import com.composables.composetheme.material3.typography
+import edu.ucsc.its.temerity.remote.Course
+import org.jetbrains.compose.resources.stringResource
+import temerity.shared.compose.generated.resources.Res
+import temerity.shared.compose.generated.resources.course_code_prefix
+import temerity.shared.compose.generated.resources.course_edit
+import temerity.shared.compose.generated.resources.direct_course_link_prefix
+import temerity.shared.compose.generated.resources.term_code_prefix
+
+@Preview
+@Composable
+fun CoursesListPreview() {
+  val mockCourseList = listOf(
+    Course(),
+    Course(),
+    Course(),
+  )
+  PreviewThemeWrapper {
+    CoursesList(courseList = mockCourseList) {}
+  }
+}
+
+@Composable
+fun CoursesList(courseList: List<Course>, onEditClick: (Course) -> Unit) {
+  ScrollableList(courseList) {
+    CourseCardRow(course = it, onEditClick = { onEditClick(it) })
+  }
+}
+
+@Composable
+fun CourseCardRow(course: Course, onEditClick: () -> Unit) {
+  CardRow(
+    title = course.courseName,
+    subtitle = stringResource(Res.string.course_code_prefix) + course.courseCode,
+  ) {
+    LazyColumn(
+      verticalArrangement = Arrangement.spacedBy(16.dp),
+      modifier = Modifier.height(190.dp),
+      contentPadding = PaddingValues(vertical = 8.dp),
+    ) {
+      item {
+        Row(
+          verticalAlignment = Alignment.CenterVertically,
+          horizontalArrangement = Arrangement.SpaceBetween,
+          modifier = Modifier.fillMaxWidth(),
+        ) {
+          Column {
+            Text(
+              text = stringResource(Res.string.term_code_prefix) + course.courseTerm,
+              color = ComposeTheme.colorScheme.primary,
+              style = ComposeTheme.typography.bodyMedium,
+            )
+            Spacer(modifier = Modifier.width(4.dp).background(ComposeTheme.colorScheme.background))
+            val directCourseLinkPrefix = stringResource(Res.string.direct_course_link_prefix)
+            val courseLinkText = remember {
+              buildAnnotatedString {
+                append(directCourseLinkPrefix)
+                withLink(
+                  link = LinkAnnotation.Url(
+                    url = course.directLink,
+                  ),
+                ) {
+                  append(" ")
+                  append(course.directLink)
+                }
+              }
+            }
+            Text(
+              text = courseLinkText,
+              color = ComposeTheme.colorScheme.primary,
+              style = ComposeTheme.typography.bodyMedium,
+            )
+          }
+          IconButton(
+            onClick = onEditClick,
+            modifier = Modifier.padding(8.dp),
+          ) {
+            Icon(imageVector = Icons.Default.Edit, contentDescription = stringResource(Res.string.course_edit))
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/UserElements.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/UserElements.kt
index 62a4313..8b0ac23 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/UserElements.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/UserElements.kt
@@ -19,44 +19,54 @@ package edu.ucsc.its.temerity.shared.ui.elements
 import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.material.Text
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Edit
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.unit.dp
 import com.composables.composetheme.ComposeTheme
 import com.composables.composetheme.material3.colorScheme
 import com.composables.composetheme.material3.typography
 import edu.ucsc.its.temerity.remote.User
+import org.jetbrains.compose.resources.stringResource
+import temerity.shared.compose.generated.resources.Res
+import temerity.shared.compose.generated.resources.user_edit
 
 @Preview
 @Composable
 fun UserListPreview() {
-  val userList = listOf(
+  val mockUserList = listOf(
     User(),
     User(),
     User(),
   )
   PreviewThemeWrapper {
-    UserList(userList = userList)
+    UserList(userList = mockUserList) {}
   }
 }
 
 @Composable
-fun UserList(userList: List<User>) {
+fun UserList(userList: List<User>, onEditClick: (User) -> Unit) {
   ScrollableList(userList) {
-    UserCardRow(user = it)
+    UserCardRow(user = it, onEditClick = { onEditClick(it) })
   }
 }
 
 @Composable
-fun UserCardRow(user: User) {
+fun UserCardRow(user: User, onEditClick: () -> Unit) {
   CardRow(
     title = user.loginId,
     subtitle = "Role: ${user.userType}",
@@ -67,17 +77,31 @@ fun UserCardRow(user: User) {
       contentPadding = PaddingValues(vertical = 8.dp),
     ) {
       item {
-        Text(
-          text = "Name: ${user.firstName} ${user.lastName}",
-          color = ComposeTheme.colorScheme.primary,
-          style = ComposeTheme.typography.bodyMedium,
-        )
-        Spacer(modifier = Modifier.width(4.dp).background(ComposeTheme.colorScheme.background))
-        Text(
-          text = "Login email: ${user.emailAddress}",
-          color = ComposeTheme.colorScheme.primary,
-          style = ComposeTheme.typography.bodyMedium,
-        )
+        Row(
+          verticalAlignment = Alignment.CenterVertically,
+          horizontalArrangement = Arrangement.SpaceBetween,
+          modifier = Modifier.fillMaxWidth(),
+        ) {
+          Column {
+            Text(
+              text = "Name: ${user.firstName} ${user.lastName}",
+              color = ComposeTheme.colorScheme.primary,
+              style = ComposeTheme.typography.bodyLarge,
+            )
+            Spacer(modifier = Modifier.width(4.dp).background(ComposeTheme.colorScheme.background))
+            Text(
+              text = "Login email: ${user.emailAddress}",
+              color = ComposeTheme.colorScheme.primary,
+              style = ComposeTheme.typography.bodyLarge,
+            )
+          }
+          IconButton(
+            onClick = onEditClick,
+            modifier = Modifier.padding(8.dp),
+          ) {
+            Icon(imageVector = Icons.Default.Edit, contentDescription = stringResource(Res.string.user_edit))
+          }
+        }
       }
     }
   }
diff --git a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/Utility.kt b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/Utility.kt
index 57ff800..cddcc48 100644
--- a/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/Utility.kt
+++ b/shared/compose/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/ui/elements/Utility.kt
@@ -19,8 +19,15 @@ package edu.ucsc.its.temerity.shared.ui.elements
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibilityScope
 import androidx.compose.animation.core.animateFloatAsState
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.slideInHorizontally
+import androidx.compose.animation.slideOutHorizontally
+import androidx.compose.animation.slideOutVertically
+import androidx.compose.foundation.Image
 import androidx.compose.foundation.ScrollbarStyle
 import androidx.compose.foundation.background
+import androidx.compose.foundation.border
 import androidx.compose.foundation.gestures.Orientation
 import androidx.compose.foundation.gestures.draggable
 import androidx.compose.foundation.gestures.rememberDraggableState
@@ -41,9 +48,12 @@ import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.widthIn
 import androidx.compose.foundation.lazy.LazyColumn
 import androidx.compose.foundation.lazy.LazyItemScope
+import androidx.compose.foundation.lazy.LazyListScope
 import androidx.compose.foundation.lazy.items
 import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.onClick
 import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.BasicText
 import androidx.compose.material.icons.Icons
 import androidx.compose.material.icons.rounded.ChevronRight
 import androidx.compose.material3.CardDefaults
@@ -63,12 +73,30 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.clip
 import androidx.compose.ui.draw.rotate
 import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.StrokeJoin
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.path
 import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.toSize
+import androidx.navigation.NavGraph
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.NavHost
 import com.composables.composetheme.ComposeTheme
 import com.composables.composetheme.material3.colorScheme
 import com.composables.composetheme.material3.typography
+import com.composables.core.Menu
+import com.composables.core.MenuButton
+import com.composables.core.MenuContent
+import com.composables.core.MenuItem
+import com.composables.core.rememberMenuState
 import kotlinx.coroutines.launch
 
 /**
@@ -104,7 +132,7 @@ fun AdaptiveAspectRatioBox(
 }
 
 @Composable
-fun <T> ScrollableList(lazyListItems: List<T>, itemLayout: @Composable LazyItemScope.(T) -> Unit) {
+fun ScrollableContent(content: LazyListScope.() -> Unit) {
   val unHoverColor = ComposeTheme.colorScheme.surfaceContainerHighest
   val hoverColor = ComposeTheme.colorScheme.secondaryContainer
   val scrollbarStyle = ScrollbarStyle(
@@ -132,12 +160,10 @@ fun <T> ScrollableList(lazyListItems: List<T>, itemLayout: @Composable LazyItemS
         )
         .padding(end = 4.dp),
       state = scrollState,
-      verticalArrangement = Arrangement.spacedBy(16.dp),
-      contentPadding = PaddingValues(horizontal = 24.dp, vertical = 8.dp),
+      verticalArrangement = Arrangement.spacedBy(12.dp),
+      contentPadding = PaddingValues(horizontal = 24.dp, vertical = 24.dp),
     ) {
-      items(lazyListItems) { item: T ->
-        itemLayout(item)
-      }
+      content()
     }
     VerticalScrollbar(
       adapter = rememberScrollbarAdapter(scrollState),
@@ -150,6 +176,15 @@ fun <T> ScrollableList(lazyListItems: List<T>, itemLayout: @Composable LazyItemS
   }
 }
 
+@Composable
+fun <T> ScrollableList(lazyListItems: List<T> = emptyList(), itemLayout: @Composable LazyItemScope.(T) -> Unit) {
+  ScrollableContent {
+    items(lazyListItems) { item ->
+      itemLayout(item)
+    }
+  }
+}
+
 @Composable
 fun CardRow(
   title: String,
@@ -180,11 +215,9 @@ fun CardRow(
             text = title,
             color = textColor,
             style = ComposeTheme.typography.titleLarge,
-          )
-          Spacer(
-            modifier = Modifier
-              .weight(1f)
-              .background(spacerColor),
+            modifier = Modifier.weight(1f),
+            maxLines = 1,
+            overflow = TextOverflow.Ellipsis,
           )
           IconButton(onClick = { expanded = expanded.not() }) {
             Icon(
@@ -222,3 +255,95 @@ fun CardRow(
     }
   }
 }
+
+@Composable
+fun NewNavHost(navController: NavHostController, navGraph: NavGraph) = NavHost(
+  navController,
+  navGraph,
+  enterTransition = {
+    fadeIn() + slideInHorizontally(initialOffsetX = { it / 2 })
+  },
+  exitTransition = {
+    fadeOut() + slideOutHorizontally(targetOffsetX = { it / 2 })
+  },
+  popExitTransition = {
+    fadeOut() + slideOutVertically(targetOffsetY = { -it })
+  },
+)
+
+@Composable
+fun ChevronDown(): ImageVector {
+  val iconColor = ComposeTheme.colorScheme.onSurface
+  return remember {
+    ImageVector.Builder(
+      name = "ChevronDown",
+      defaultWidth = 16.dp,
+      defaultHeight = 16.dp,
+      viewportWidth = 24f,
+      viewportHeight = 24f,
+    ).apply {
+      path(
+        fill = null,
+        fillAlpha = 1.0f,
+        stroke = SolidColor(iconColor),
+        strokeAlpha = 1.0f,
+        strokeLineWidth = 2f,
+        strokeLineCap = StrokeCap.Round,
+        strokeLineJoin = StrokeJoin.Round,
+        strokeLineMiter = 1.0f,
+        pathFillType = PathFillType.NonZero,
+      ) {
+        moveTo(6f, 9f)
+        lineToRelative(6f, 6f)
+        lineToRelative(6f, -6f)
+      }
+    }.build()
+  }
+}
+
+@Composable
+fun DropdownMenu(
+  items: List<String>,
+  onItemSelected: (String) -> Unit,
+  modifier: Modifier = Modifier,
+) {
+  val state = rememberMenuState(expanded = false)
+  var selectedItem by mutableStateOf("Select")
+  val surfaceColor = ComposeTheme.colorScheme.surface
+  val textColor = ComposeTheme.colorScheme.onSurface
+
+  Box(modifier = modifier) {
+    Menu(modifier = Modifier.align(Alignment.TopStart).width(240.dp), state = state) {
+      val degrees by animateFloatAsState(if (state.expanded) -180f else 0f)
+
+      MenuButton(
+        Modifier.clip(RoundedCornerShape(6.dp)).background(surfaceColor)
+          .border(1.dp, Color(0xFFBDBDBD), RoundedCornerShape(6.dp)),
+      ) {
+        Row(
+          verticalAlignment = Alignment.CenterVertically,
+          modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
+        ) {
+          BasicText(selectedItem, modifier = Modifier.widthIn(min = 160.dp), style = TextStyle(fontWeight = FontWeight(500), color = textColor))
+          Spacer(Modifier.width(4.dp))
+          Image(ChevronDown(), null, Modifier.rotate(degrees))
+        }
+      }
+
+      MenuContent(
+        modifier = Modifier.padding(top = 4.dp).width(320.dp).clip(RoundedCornerShape(6.dp))
+          .border(1.dp, Color(0xFFE0E0E0), RoundedCornerShape(6.dp)).background(surfaceColor).padding(4.dp),
+        exit = fadeOut(),
+      ) {
+        items.forEach {
+          MenuItem(modifier = Modifier.clip(RoundedCornerShape(6.dp)), onClick = {
+            selectedItem = it
+            onItemSelected(it)
+          }) {
+            BasicText(it, Modifier.fillMaxWidth().padding(vertical = 10.dp, horizontal = 10.dp), style = TextStyle(color = textColor))
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/shared/shared/build.gradle.kts b/shared/shared/build.gradle.kts
index b8405b4..a4fae46 100644
--- a/shared/shared/build.gradle.kts
+++ b/shared/shared/build.gradle.kts
@@ -41,6 +41,10 @@ plugins {
     alias(libs.plugins.room)
 }
 
+dependencies {
+    ksp(libs.arrow.opticsKspPlugin)
+}
+
 kotlin {
     jvmToolchain(libs.versions.java.get().toInt())
     jvm()
@@ -59,7 +63,9 @@ kotlin {
                 implementation(libs.koin.composeVm)
                 api(libs.kermit)
                 api(libs.kermit.koin)
-
+                implementation(libs.arrow.core)
+                implementation(libs.arrow.fxCoroutines)
+                implementation(libs.arrow.optics)
                 api(libs.kotlinx.serialization.json)
                 api(libs.kstore)
                 api(libs.kstore.file)
@@ -70,6 +76,7 @@ kotlin {
                 implementation(libs.androidx.room.runtime)
                 implementation(libs.sqlite.bundled)
                 implementation(libs.datastore.preferences)
+                implementation(libs.bundles.exposed)
             }
         }
         val jvmMain by getting {
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/AppSettings.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/AppSettings.kt
index 71fe669..e0b26c2 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/AppSettings.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/AppSettings.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared
 
 import androidx.datastore.core.DataStore
@@ -10,37 +26,37 @@ import kotlinx.coroutines.flow.map
 import okio.Path.Companion.toPath
 
 fun createDataStore(
-    producePath: () -> String,
+  producePath: () -> String,
 ): DataStore<Preferences> = PreferenceDataStoreFactory.createWithPath(
-    corruptionHandler = null,
-    migrations = emptyList(),
-    produceFile = { producePath().toPath() },
+  corruptionHandler = null,
+  migrations = emptyList(),
+  produceFile = { producePath().toPath() },
 )
 
 class AppSettings(private val dataStore: DataStore<Preferences>) {
 
-    val userServiceEndpoint: Flow<String> = dataStore.data.map { preferences ->
-        preferences[USER_ENDPOINT_URL_SETTING] ?: ""
-    }
+  val userServiceEndpoint: Flow<String> = dataStore.data.map { preferences ->
+    preferences[USER_ENDPOINT_URL_SETTING] ?: ""
+  }
 
-    val userAuthToken: Flow<String> = dataStore.data.map { preferences ->
-        preferences[USER_AUTH_TOKEN_SETTING] ?: ""
-    }
+  val userAuthToken: Flow<String> = dataStore.data.map { preferences ->
+    preferences[USER_AUTH_TOKEN_SETTING] ?: ""
+  }
 
-    suspend fun updateServiceEndpoint(newEndpointUrl: String) {
-        dataStore.edit { preferences ->
-            preferences[USER_ENDPOINT_URL_SETTING] = newEndpointUrl
-        }
+  suspend fun updateServiceEndpoint(newEndpointUrl: String) {
+    dataStore.edit { preferences ->
+      preferences[USER_ENDPOINT_URL_SETTING] = newEndpointUrl
     }
+  }
 
-    suspend fun updateUserAuthToken(newToken: String) {
-        dataStore.edit { preferences ->
-            preferences[USER_AUTH_TOKEN_SETTING] = newToken
-        }
+  suspend fun updateUserAuthToken(newToken: String) {
+    dataStore.edit { preferences ->
+      preferences[USER_AUTH_TOKEN_SETTING] = newToken
     }
+  }
 
-    companion object {
-        val USER_AUTH_TOKEN_SETTING = stringPreferencesKey("user_auth_token")
-        val USER_ENDPOINT_URL_SETTING = stringPreferencesKey("user_endpoint_url")
-    }
-}
\ No newline at end of file
+  companion object {
+    val USER_AUTH_TOKEN_SETTING = stringPreferencesKey("user_auth_token")
+    val USER_ENDPOINT_URL_SETTING = stringPreferencesKey("user_endpoint_url")
+  }
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
index 803f4da..428fc9f 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/data/repository/PlatformRepository.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared.data.repository
 
 /*
@@ -38,57 +54,56 @@ import org.koin.core.parameter.parametersOf
 
 interface PlatformRepositoryInterface {
 
-    sealed class CacheFlag(val enable: Boolean) {
-        data object ENABLE: CacheFlag(true)
-        data object DISABLE: CacheFlag(false)
-    }
+  sealed class CacheFlag(val enable: Boolean) {
+    data object ENABLE : CacheFlag(true)
+    data object DISABLE : CacheFlag(false)
+  }
 
-    suspend fun fetchGroups(flag: CacheFlag = DISABLE): List<Group>
-    suspend fun fetchUsers(flag: CacheFlag = DISABLE): List<User>
-    suspend fun fetchDevices(flag: CacheFlag = DISABLE): List<Device>
+  suspend fun fetchGroups(flag: CacheFlag = DISABLE): List<Group>
+  suspend fun fetchUsers(flag: CacheFlag = DISABLE): List<User>
+  suspend fun fetchDevices(flag: CacheFlag = DISABLE): List<Device>
 }
 
 class PlatformRepository : KoinComponent, PlatformRepositoryInterface {
-    private lateinit var platformClient: PlatformClient
-    private val database : AppDatabase by inject()
-    private val appSettings: AppSettings by inject()
-
-    private val jobScope = CoroutineScope(Dispatchers.IO + Job())
-    var updateInterval = mutableLongStateOf(-1L)
-
-    val logger = Logger.withTag("PlatformRepository")
+  private lateinit var platformClient: PlatformClient
+  private val database: AppDatabase by inject()
+  private val appSettings: AppSettings by inject()
 
-    init {
-        jobScope.launch {
-            loadDataForAppStart()
-        }
-    }
-
-    private suspend fun updateServiceEndpoint(newEndpointUrl: String) {
-        appSettings.updateServiceEndpoint(newEndpointUrl)
-        platformClient = get { parametersOf(appSettings.userServiceEndpoint, appSettings.userAuthToken) }
-        // TODO: Test new PC here
-    }
+  private val jobScope = CoroutineScope(Dispatchers.IO + Job())
+  var updateInterval = mutableLongStateOf(-1L)
 
-    private suspend fun updateUserAuthToken(newToken: String) {
-        appSettings.updateUserAuthToken(newToken)
-        platformClient = get { parametersOf(appSettings.userServiceEndpoint, appSettings.userAuthToken) }
-        // TODO: Test new PC here
-    }
-
-    private suspend fun loadDataForAppStart() {
-
-    }
-
-    override suspend fun fetchGroups(flag: CacheFlag): List<Group> {
-        TODO("Not yet implemented")
-    }
-
-    override suspend fun fetchUsers(flag: CacheFlag): List<User> {
-        TODO("Not yet implemented")
-    }
+  val logger = Logger.withTag("PlatformRepository")
 
-    override suspend fun fetchDevices(flag: CacheFlag): List<Device> {
-        TODO("Not yet implemented")
+  init {
+    jobScope.launch {
+      loadDataForAppStart()
     }
+  }
+
+  private suspend fun updateServiceEndpoint(newEndpointUrl: String) {
+    appSettings.updateServiceEndpoint(newEndpointUrl)
+    platformClient = get { parametersOf(appSettings.userServiceEndpoint, appSettings.userAuthToken) }
+    // TODO: Test new PC here
+  }
+
+  private suspend fun updateUserAuthToken(newToken: String) {
+    appSettings.updateUserAuthToken(newToken)
+    platformClient = get { parametersOf(appSettings.userServiceEndpoint, appSettings.userAuthToken) }
+    // TODO: Test new PC here
+  }
+
+  private suspend fun loadDataForAppStart() {
+  }
+
+  override suspend fun fetchGroups(flag: CacheFlag): List<Group> {
+    TODO("Not yet implemented")
+  }
+
+  override suspend fun fetchUsers(flag: CacheFlag): List<User> {
+    TODO("Not yet implemented")
+  }
+
+  override suspend fun fetchDevices(flag: CacheFlag): List<Device> {
+    TODO("Not yet implemented")
+  }
 }
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/AppDatabase.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/AppDatabase.kt
index 38c1ca5..4c9a3b8 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/AppDatabase.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/AppDatabase.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared.database
 
 import androidx.room.Database
@@ -9,23 +25,22 @@ import edu.ucsc.its.temerity.remote.Group
 import edu.ucsc.its.temerity.remote.User
 import kotlinx.datetime.LocalDateTime
 
-internal const val dbFileName = "temerity.db"
+internal const val DB_FILENAME = "temerity.db"
 
 @Database(entities = [Group::class, User::class, Device::class], version = 1)
 @TypeConverters(LocalDateTimeConverter::class)
 abstract class AppDatabase : RoomDatabase() {
-    abstract fun temerityDao(): TemerityDao
-
+  abstract fun temerityDao(): TemerityDao
 }
 
 class LocalDateTimeConverter {
-    @TypeConverter
-    fun fromTimestamp(value: String?): LocalDateTime? {
-        return value?.let { LocalDateTime.parse(it) }
-    }
+  @TypeConverter
+  fun fromTimestamp(value: String?): LocalDateTime? {
+    return value?.let { LocalDateTime.parse(it) }
+  }
 
-    @TypeConverter
-    fun dateToTimestamp(date: LocalDateTime?): String? {
-        return date?.toString()
-    }
-}
\ No newline at end of file
+  @TypeConverter
+  fun dateToTimestamp(date: LocalDateTime?): String? {
+    return date?.toString()
+  }
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/TemerityDao.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/TemerityDao.kt
index fef61ed..f39d950 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/TemerityDao.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/database/TemerityDao.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared.database
 
 import androidx.room.Dao
@@ -10,12 +26,12 @@ import edu.ucsc.its.temerity.remote.User
 @Dao
 interface TemerityDao {
 
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    suspend fun insertGroupList(groupList: List<Group>)
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  suspend fun insertGroupList(groupList: List<Group>)
 
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    suspend fun insertUserList(userList: List<User>)
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  suspend fun insertUserList(userList: List<User>)
 
-    @Insert(onConflict = OnConflictStrategy.REPLACE)
-    suspend fun insertDeviceList(deviceList: List<Device>)
-}
\ No newline at end of file
+  @Insert(onConflict = OnConflictStrategy.REPLACE)
+  suspend fun insertDeviceList(deviceList: List<Device>)
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
index b10b58d..8ce97fd 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/di/CommonModule.kt
@@ -16,15 +16,17 @@
  */
 package edu.ucsc.its.temerity.shared.di
 
+import ca.gosyer.appdirs.AppDirs
 import co.touchlab.kermit.Logger
 import co.touchlab.kermit.koin.kermitLoggerModule
-import edu.ucsc.its.temerity.shared.data.storageModule
 import edu.ucsc.its.temerity.platformClient
 import edu.ucsc.its.temerity.shared.AppSettings
 import edu.ucsc.its.temerity.shared.data.repository.PlatformRepository
-import edu.ucsc.its.temerity.shared.viewmodel.AppStateViewModel
+import edu.ucsc.its.temerity.shared.data.storageModule
+import edu.ucsc.its.temerity.shared.viewmodel.AppViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.LoginViewModel
 import edu.ucsc.its.temerity.shared.viewmodel.RootViewModel
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditViewModel
 import org.koin.compose.viewmodel.dsl.viewModelOf
 import org.koin.core.context.startKoin
 import org.koin.dsl.KoinAppDeclaration
@@ -38,9 +40,10 @@ fun initKoin(enableNetworkLogs: Boolean = false, appDeclaration: KoinAppDeclarat
   }
 
 internal fun commonModule(isDebuggingEnabled: Boolean = false) = module {
-  viewModelOf(::AppStateViewModel)
+  viewModelOf(::AppViewModel)
   viewModelOf(::LoginViewModel)
   viewModelOf(::RootViewModel)
+  viewModelOf(::UserEditViewModel)
   single { (serviceEndpoint: String, serviceToken: String) ->
     platformClient {
       debugEnabled(isDebuggingEnabled)
@@ -50,6 +53,9 @@ internal fun commonModule(isDebuggingEnabled: Boolean = false) = module {
   }
   single { PlatformRepository() }
   single { AppSettings(get()) }
+  single<String> { (packageName: String) ->
+    AppDirs("", packageName).getUserConfigDir()
+  }
   includes(storageModule(), kermitLoggerModule(Logger.withTag("compose koin context")))
 }
 
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppStateViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
similarity index 95%
rename from shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppStateViewModel.kt
rename to shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
index 7b11602..c251b49 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppStateViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/AppViewModel.kt
@@ -41,7 +41,7 @@ data class AppState(
   val userLoggedIn: Boolean = false,
 )
 
-class AppStateViewModel : MoleculeViewModel<AppStateEvent, AppState>(), KoinComponent {
+class AppViewModel : MoleculeViewModel<AppStateEvent, AppState>(), KoinComponent {
   private var serviceEndpoint by mutableStateOf("")
   private var serviceToken by mutableStateOf("")
   private var userLoggedIn by mutableStateOf(false)
@@ -53,9 +53,9 @@ class AppStateViewModel : MoleculeViewModel<AppStateEvent, AppState>(), KoinComp
 
   @Composable
   private fun presentAppState(events: Flow<AppStateEvent>, platformClient: PlatformClient): AppState {
-    LaunchedEffect(Unit){
+    LaunchedEffect(Unit) {
       events.collect { event ->
-        when(event) {
+        when (event) {
           is AppStateEvent.ServiceEndpointUpdated -> {
             viewModelScope.launch {
               serviceEndpoint = event.newEndpoint
@@ -78,6 +78,5 @@ class AppStateViewModel : MoleculeViewModel<AppStateEvent, AppState>(), KoinComp
   }
 
   private fun attemptLogin() {
-
   }
 }
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
index 86cef55..df1d3cd 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/DevicesScreenViewModel.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared.viewmodel
 
 import androidx.compose.runtime.Composable
@@ -11,40 +27,36 @@ import edu.ucsc.its.temerity.remote.Device
 import kotlinx.coroutines.flow.Flow
 import org.koin.compose.getKoin
 
-sealed interface DevicesScreenEvent {
-
-}
+sealed interface DevicesScreenEvent
 
 data class DevicesScreenState(
-    val loading: Boolean,
-    val devices: List<Device>
+  val loading: Boolean,
+  val devices: List<Device>,
 )
 
 class DevicesScreenViewModel() : MoleculeViewModel<DevicesScreenEvent, DevicesScreenState>() {
 
-    @Composable
-    override fun models(events: Flow<DevicesScreenEvent>): DevicesScreenState {
-        return devicesScreenPresenter(events, getKoin().get())
-    }
-
+  @Composable
+  override fun models(events: Flow<DevicesScreenEvent>): DevicesScreenState {
+    return devicesScreenPresenter(events, getKoin().get())
+  }
 }
 
 @Composable
 fun devicesScreenPresenter(events: Flow<DevicesScreenEvent>, client: PlatformClient): DevicesScreenState {
-    var devices: List<Device> by remember { mutableStateOf(emptyList()) }
+  var devices: List<Device> by remember { mutableStateOf(emptyList()) }
 
-    LaunchedEffect(Unit) {
-        devices = client.getDevices()
-    }
-
-    LaunchedEffect(Unit) {
-        events.collect { event ->
+  LaunchedEffect(Unit) {
+    devices = client.getDevices()
+  }
 
-        }
+  LaunchedEffect(Unit) {
+    events.collect { event ->
     }
+  }
 
-    return DevicesScreenState(
-        loading = false,
-        devices = devices
-    )
-}
\ No newline at end of file
+  return DevicesScreenState(
+    loading = false,
+    devices = devices,
+  )
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/LoginViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/LoginViewModel.kt
index 7e57f09..e8d7574 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/LoginViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/LoginViewModel.kt
@@ -47,14 +47,13 @@ class LoginViewModel : MoleculeViewModel<LoginEvent, LoginState>(), KoinComponen
     return loginScreenPresenter(events)
   }
 
-
   @Composable
   fun loginScreenPresenter(eventCollector: Flow<LoginEvent>): LoginState {
     LaunchedEffect(Unit) {
       eventCollector.collect { event ->
         when (event) {
           is LoginEvent.AttemptLogin -> {
-            viewModelScope.launch(Dispatchers.IO){
+            viewModelScope.launch(Dispatchers.IO) {
               try {
                 // TODO: Update token and refresh platform client injected into repository here.
                 events.tryEmit(LoginEvent.LoginSuccess)
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/MoleculeViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/MoleculeViewModel.kt
index 5a2f87d..b8b257e 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/MoleculeViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/MoleculeViewModel.kt
@@ -1,27 +1,24 @@
 /*
- * Copyright (C) 2022 Square, Inc.
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
- *
- * Adapted from https://github.com/cashapp/molecule/blob/9cedc80ab10193a7c3afecbd5b2cb514aa88f9ff/sample-viewmodel/src/main/java/com/example/molecule/viewmodel/presentationLogic.kt
- * Modifications made by William Walker (wnwalker@ucsc.edu) in June 2024
  */
 package edu.ucsc.its.temerity.shared.viewmodel
 
 import androidx.compose.runtime.Composable
 import androidx.lifecycle.ViewModel
 import androidx.lifecycle.viewModelScope
-import app.cash.molecule.RecompositionMode.ContextClock
 import app.cash.molecule.RecompositionMode.Immediate
 import app.cash.molecule.launchMolecule
 import kotlinx.coroutines.flow.Flow
@@ -29,20 +26,20 @@ import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.StateFlow
 
 abstract class MoleculeViewModel<Event, Model> : ViewModel() {
-    internal val events = MutableSharedFlow<Event>(extraBufferCapacity = 20)
+  internal val events = MutableSharedFlow<Event>(extraBufferCapacity = 20)
 
-    val models: StateFlow<Model> by lazy(LazyThreadSafetyMode.NONE) {
-        viewModelScope.launchMolecule(mode = Immediate) {
-            models(events)
-        }
+  val models: StateFlow<Model> by lazy(LazyThreadSafetyMode.NONE) {
+    viewModelScope.launchMolecule(mode = Immediate) {
+      models(events)
     }
+  }
 
-    fun take(event: Event) {
-        if (!events.tryEmit(event)) {
-            error("Event buffer overflow.")
-        }
+  fun take(event: Event) {
+    if (!events.tryEmit(event)) {
+      error("Event buffer overflow.")
     }
+  }
 
-    @Composable
-    protected abstract fun models(events: Flow<Event>): Model
-}
\ No newline at end of file
+  @Composable
+  protected abstract fun models(events: Flow<Event>): Model
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
index a5018c0..091f4aa 100644
--- a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/RootViewModel.kt
@@ -1,3 +1,19 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
 package edu.ucsc.its.temerity.shared.viewmodel
 
 import androidx.compose.runtime.Composable
@@ -7,22 +23,21 @@ import org.koin.core.component.KoinComponent
 import org.koin.core.component.get
 
 sealed class RootEvent {
-    data object NavigateToDevicesPane : RootEvent()
+  data object NavigateToDevicesPane : RootEvent()
 }
 
 data class RootState(
-    val loading: Boolean = false
+  val loading: Boolean = false,
 )
 
 class RootViewModel : MoleculeViewModel<RootEvent, RootState>(), KoinComponent {
 
-    @Composable
-    override fun models(events: Flow<RootEvent>): RootState {
-        return rootPresenter(events, get<PlatformClient>())
-    }
+  @Composable
+  override fun models(events: Flow<RootEvent>): RootState {
+    return rootPresenter(events, get<PlatformClient>())
+  }
 
-    private fun rootPresenter(events: Flow<RootEvent>, client: PlatformClient): RootState {
-        return RootState(false)
-    }
-
-}
\ No newline at end of file
+  private fun rootPresenter(events: Flow<RootEvent>, client: PlatformClient): RootState {
+    return RootState(false)
+  }
+}
diff --git a/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/UserEditViewModel.kt b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/UserEditViewModel.kt
new file mode 100644
index 0000000..173bf0e
--- /dev/null
+++ b/shared/shared/src/commonMain/kotlin/edu/ucsc/its/temerity/shared/viewmodel/UserEditViewModel.kt
@@ -0,0 +1,118 @@
+/*
+ * Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ * Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package edu.ucsc.its.temerity.shared.viewmodel
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import edu.ucsc.its.temerity.remote.User
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditEvent.ClearField
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditEvent.LoadUser
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditEvent.Save
+import edu.ucsc.its.temerity.shared.viewmodel.UserEditEvent.UpdateField
+import kotlinx.coroutines.flow.Flow
+
+sealed class UserEditEvent {
+  data class LoadUser(val user: User?) : UserEditEvent()
+  data object Save : UserEditEvent()
+  data class ClearField(val fieldName: String) : UserEditEvent()
+  data class UpdateField(val fieldName: String, val value: String) : UserEditEvent()
+}
+
+data class UserEditState(
+  var firstName: String = "",
+  var lastName: String = "",
+  var loginId: String = "",
+  var emailAddress: String = "",
+  val timezone: String = "",
+  val role: String = "",
+  val lockUser: Boolean = false,
+)
+
+class UserEditViewModel : MoleculeViewModel<UserEditEvent, UserEditState>() {
+  @Composable
+  override fun models(events: Flow<UserEditEvent>): UserEditState {
+    return userEditPresenter(events)
+  }
+
+  @Composable
+  fun userEditPresenter(events: Flow<UserEditEvent>): UserEditState {
+    var state by remember { mutableStateOf(UserEditState()) }
+
+    LaunchedEffect(Unit) {
+      events.collect {
+        when (it) {
+          is LoadUser -> {
+            val user = it.user
+            if (user != null) {
+              state = state.copy(
+                firstName = user.firstName,
+                lastName = user.lastName,
+                loginId = user.loginId,
+                emailAddress = user.emailAddress,
+                timezone = user.timezone,
+                role = user.userType,
+              )
+            }
+          }
+          is Save -> {
+            // Save the user edits
+          }
+          is ClearField -> {
+            state = updateField(it.fieldName, "", state)
+          }
+
+          is UpdateField -> {
+            state = updateField(it.fieldName, it.value, state)
+          }
+        }
+      }
+    }
+    return state
+  }
+
+  private fun updateField(fieldName: String, value: String, state: UserEditState): UserEditState {
+    return when (fieldName) {
+      "firstName" -> state.copy(firstName = value)
+      "lastName" -> state.copy(lastName = value)
+      "loginId" -> state.copy(loginId = value)
+      "emailAddress" -> state.copy(emailAddress = value)
+      "timezone" -> state.copy(timezone = value)
+      "role" -> state.copy(role = value)
+      else -> state
+    }
+  }
+
+  suspend fun loadUser(user: User?) {
+    events.emit(LoadUser(user))
+  }
+
+  suspend fun clearField(fieldName: String) {
+    events.emit(ClearField(fieldName))
+  }
+
+  suspend fun updateField(fieldName: String, value: String) {
+    events.emit(UpdateField(fieldName, value))
+  }
+
+  suspend fun saveUserEdits() {
+    events.emit(Save)
+  }
+}
diff --git a/shared/shared/src/jvmMain/kotlin/edu/ucsc/its/temerity/shared/data/Actual.StorageModule.kt b/shared/shared/src/jvmMain/kotlin/edu/ucsc/its/temerity/shared/data/Actual.StorageModule.kt
index 9256ee8..aec9deb 100644
--- a/shared/shared/src/jvmMain/kotlin/edu/ucsc/its/temerity/shared/data/Actual.StorageModule.kt
+++ b/shared/shared/src/jvmMain/kotlin/edu/ucsc/its/temerity/shared/data/Actual.StorageModule.kt
@@ -22,43 +22,28 @@ import androidx.room.Room
 import androidx.sqlite.driver.bundled.BundledSQLiteDriver
 import edu.ucsc.its.temerity.shared.createDataStore
 import edu.ucsc.its.temerity.shared.database.AppDatabase
-import edu.ucsc.its.temerity.shared.database.dbFileName
-import java.io.File
-import net.harawata.appdirs.AppDirsFactory
-import okio.FileSystem
-import okio.Path.Companion.toPath
+import edu.ucsc.its.temerity.shared.database.DB_FILENAME
 import org.koin.core.context.GlobalContext.get
 import org.koin.core.parameter.parametersOf
 import org.koin.dsl.module
-
+import java.io.File
 
 internal actual fun storageModule() = module {
-  single<String> { (packageName: String, version: String, organization: String) ->
-    getAppDataDir(packageName, version, organization)
-  }
   single { dataStore() }
   single<AppDatabase> { createRoomDatabase() }
 }
 
-private fun getAppDataDir(packageName: String, version: String, organization: String): String {
-  val appDataDir = AppDirsFactory.getInstance().getUserDataDir(packageName, version, organization)
-  val path = appDataDir.toPath()
-  if (!FileSystem.SYSTEM.exists(path)) {
-    FileSystem.SYSTEM.createDirectories(path)
-  }
-  return appDataDir
-}
-
 fun createRoomDatabase(): AppDatabase {
-  val appDataDir = get().get<String> { parametersOf("edu.ucsc.its.temerity", "1.0", "UCSC") }
-  val dbFile = File(appDataDir, dbFileName)
+  val appDataDir = get().get<String> { parametersOf("edu.ucsc.its.temerity") }
+  val dbFile = File(appDataDir, DB_FILENAME)
   return Room.databaseBuilder<AppDatabase>(
-    name = dbFile.absolutePath,)
+    name = dbFile.absolutePath,
+  )
     .setDriver(BundledSQLiteDriver())
     .build()
 }
 
 fun dataStore(): DataStore<Preferences> =
   createDataStore(
-    producePath = { "temerity.preferences_pb" }
-  )
\ No newline at end of file
+    producePath = { "temerity.preferences_pb" },
+  )
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityLibrary.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityLibrary.kt
index 27970fa..95086c3 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityLibrary.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityLibrary.kt
@@ -27,7 +27,7 @@ import org.koin.dsl.koinApplication
  *
  */
 internal object TemerityLibraryKoinContext {
-  internal const val TIMEOUT = 15000L
+  internal const val TIMEOUT = 120000L
 
   private val koinApp = koinApplication {
     modules(libModule)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/remote/PlatformApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/remote/PlatformApi.kt
index 20f2333..bed4b22 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/remote/PlatformApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/remote/PlatformApi.kt
@@ -63,12 +63,12 @@ internal interface PlatformApi {
   @GET("users/{userId}/groups/content_owners")
   suspend fun getUserGroupsOwned(@Path userId: Long): HttpStatement
 
-  @POST("services/media/permission")
+  @POST("media/permission")
   suspend fun addFolderPermission(
     @Body addFolderPermissions: String,
   ): ApiResponse<HttpResponse>
 
-  @POST("services/media/permission")
+  @POST("media/permission")
   suspend fun removeFolderPermission(
     @Body deleteFolderPermissions: String,
   ): ApiResponse<HttpResponse>
-- 
GitLab