Skip to content
This repository was archived by the owner on Jul 20, 2026. It is now read-only.

Commit 527af4b

Browse files
Merge branch 'dev' into feature-animations
2 parents a9b9e9e + bb975e3 commit 527af4b

11 files changed

Lines changed: 421 additions & 40 deletions

File tree

app/src/main/java/fr/utbm/sy43/pilulito/MainActivity.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,23 @@ import androidx.activity.compose.setContent
66
import androidx.activity.enableEdgeToEdge
77
import androidx.compose.foundation.layout.fillMaxSize
88
import androidx.compose.material3.Surface
9+
import androidx.compose.runtime.remember
910
import androidx.compose.ui.Modifier
1011
import androidx.navigation.NavHostController
1112
import androidx.navigation.compose.rememberNavController
1213
import fr.utbm.sy43.pilulito.ui.navigation.AppNavGraph
1314
import fr.utbm.sy43.pilulito.ui.theme.SenPosTheme
15+
import fr.utbm.sy43.pilulito.ui.theme.ThemeState
1416

1517
class MainActivity : ComponentActivity() {
1618
override fun onCreate(savedInstanceState: Bundle?) {
1719
super.onCreate(savedInstanceState)
1820
enableEdgeToEdge()
1921
setContent {
2022
val navController: NavHostController = rememberNavController()
23+
val themeState = remember { ThemeState() }
2124

22-
SenPosTheme {
25+
SenPosTheme (themeState = themeState) {
2326
Surface(Modifier.fillMaxSize()) {
2427
AppNavGraph(navController)
2528
}

app/src/main/java/fr/utbm/sy43/pilulito/data/repositories/AuthRepository.kt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ interface AuthRepository {
2121
suspend fun logout()
2222

2323
suspend fun findSeniorByLinkCode(linkCode: String): User?
24+
suspend fun updateEmail(newEmail: String)
25+
suspend fun updatePassword(newPassword: String)
26+
suspend fun deleteAccount()
27+
2428
}
2529

2630
class OfflineAuthRepository : AuthRepository {
@@ -51,6 +55,12 @@ class OfflineAuthRepository : AuthRepository {
5155
)
5256
)
5357

58+
override suspend fun deleteAccount() {
59+
val user = _currentUser.value ?: return
60+
users.removeAll { it.id == user.id }
61+
_currentUser.value = null
62+
}
63+
5464
override suspend fun login(email: String, password: String): User? {
5565
val user = users.find { it.email == email && it.password == password }
5666
_currentUser.value = user
@@ -91,6 +101,20 @@ class OfflineAuthRepository : AuthRepository {
91101
return newUser
92102
}
93103

104+
override suspend fun updateEmail(newEmail: String) {
105+
val user = _currentUser.value ?: return
106+
val updated = user.copy(email = newEmail)
107+
users.replaceAll { if (it.id == user.id) updated else it }
108+
_currentUser.value = updated
109+
}
110+
111+
override suspend fun updatePassword(newPassword: String) {
112+
val user = _currentUser.value ?: return
113+
val updated = user.copy(password = newPassword)
114+
users.replaceAll { if (it.id == user.id) updated else it }
115+
_currentUser.value = updated
116+
}
117+
94118
override suspend fun findSeniorByLinkCode(linkCode: String): User? {
95119
return users.find {
96120
it.accountType == AccountType.SENIOR && it.linkCode == linkCode

app/src/main/java/fr/utbm/sy43/pilulito/data/repositories/FirestoreAuthRepository.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,36 @@ class FirestoreAuthRepository(
9999
_currentUser.value = null
100100
}
101101

102+
override suspend fun updateEmail(newEmail: String) {
103+
try {
104+
val userId = _currentUser.value?.id ?: return
105+
usersCollection.document(userId).update("email", newEmail).await()
106+
_currentUser.value = _currentUser.value?.copy(email = newEmail)
107+
} catch (e: Exception) {
108+
e.printStackTrace()
109+
}
110+
}
111+
112+
override suspend fun updatePassword(newPassword: String) {
113+
try {
114+
val userId = _currentUser.value?.id ?: return
115+
usersCollection.document(userId).update("password", newPassword).await()
116+
_currentUser.value = _currentUser.value?.copy(password = newPassword)
117+
} catch (e: Exception) {
118+
e.printStackTrace()
119+
}
120+
}
121+
122+
override suspend fun deleteAccount() {
123+
try {
124+
val userId = _currentUser.value?.id ?: return
125+
usersCollection.document(userId).delete().await()
126+
_currentUser.value = null
127+
} catch (e: Exception) {
128+
e.printStackTrace()
129+
}
130+
}
131+
102132
private fun generateLinkCode(name: String): String {
103133
val prefix = name.take(4).uppercase().ifBlank { "USER" }
104134
val suffix = (1000..9999).random()

app/src/main/java/fr/utbm/sy43/pilulito/ui/AppViewModelProvider.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import fr.utbm.sy43.pilulito.viewmodels.AuthViewModel
1111
import fr.utbm.sy43.pilulito.viewmodels.HistoryViewModel
1212
import fr.utbm.sy43.pilulito.viewmodels.HomeViewModel
1313
import fr.utbm.sy43.pilulito.viewmodels.PharmacyViewModel
14+
import fr.utbm.sy43.pilulito.viewmodels.SettingsViewModel
1415
import fr.utbm.sy43.pilulito.viewmodels.SupervisorViewModel
1516

1617
object AppViewModelProvider {
@@ -41,6 +42,10 @@ object AppViewModelProvider {
4142
)
4243
}
4344

45+
initializer {
46+
SettingsViewModel(pilulitoApplication().container.authRepository)
47+
}
48+
4449
initializer {
4550
val application =
4651
this[AndroidViewModelFactory.APPLICATION_KEY]

app/src/main/java/fr/utbm/sy43/pilulito/ui/components/TopBarComponent.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import androidx.compose.ui.unit.dp
2323
import androidx.compose.ui.unit.sp
2424
import androidx.navigation.NavHostController
2525
import androidx.navigation.compose.rememberNavController
26-
import fr.utbm.sy43.pilulito.ui.navigation.Route
2726
import fr.utbm.sy43.pilulito.ui.theme.SenPosTheme
27+
import fr.utbm.sy43.pilulito.ui.navigation.Route
28+
2829

2930
@Composable
3031
fun TopBarComponent(navController: NavHostController) {
@@ -60,7 +61,7 @@ fun TopBarComponent(navController: NavHostController) {
6061
)
6162
}
6263

63-
Button(onClick = { navController.navigate(Route.History.route) },
64+
Button(onClick = { navController.navigate(Route.Settings.route) },
6465
colors = ButtonDefaults.buttonColors(containerColor = Color.White, contentColor = Color.Black)
6566
) {
6667
Icon(

app/src/main/java/fr/utbm/sy43/pilulito/ui/navigation/AppNavGraph.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import fr.utbm.sy43.pilulito.ui.screens.AddPosologyScreen
99
import fr.utbm.sy43.pilulito.ui.screens.HistoryScreen
1010
import fr.utbm.sy43.pilulito.ui.screens.LoginScreen
1111
import fr.utbm.sy43.pilulito.ui.screens.PharmacyMapScreen
12+
import fr.utbm.sy43.pilulito.ui.screens.SettingsScreen
1213
import fr.utbm.sy43.pilulito.ui.screens.SignupScreen
1314
import fr.utbm.sy43.pilulito.ui.screens.TodayScreen
1415
import fr.utbm.sy43.pilulito.ui.screens.SupervisorScreen
@@ -31,6 +32,10 @@ fun AppNavGraph(navController: NavHostController) {
3132
HistoryScreen(navController)
3233
}
3334

35+
composable(Route.Settings.route) {
36+
SettingsScreen(navController)
37+
}
38+
3439
composable(Route.Login.route) {
3540
LoginScreen(navController)
3641
}

app/src/main/java/fr/utbm/sy43/pilulito/ui/navigation/Routes.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ sealed class Route(val route: String) {
99
object PharmacyMap: Route("pharmacy_map")
1010
object Supervisor : Route("supervisor")
1111
object Profile : Route("profile")
12+
object Settings : Route("settings")
1213
}
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
package fr.utbm.sy43.pilulito.ui.screens
2+
3+
import androidx.compose.foundation.layout.*
4+
import androidx.compose.foundation.rememberScrollState
5+
import androidx.compose.foundation.shape.RoundedCornerShape
6+
import androidx.compose.foundation.verticalScroll
7+
import androidx.compose.material3.*
8+
import androidx.compose.runtime.*
9+
import androidx.compose.ui.Alignment
10+
import androidx.compose.ui.Modifier
11+
import androidx.compose.ui.graphics.Color
12+
import androidx.compose.ui.text.font.FontWeight
13+
import androidx.compose.ui.text.input.PasswordVisualTransformation
14+
import androidx.compose.ui.unit.dp
15+
import androidx.compose.ui.unit.sp
16+
import androidx.lifecycle.viewmodel.compose.viewModel
17+
import androidx.navigation.NavHostController
18+
import fr.utbm.sy43.pilulito.ui.AppViewModelProvider
19+
import fr.utbm.sy43.pilulito.ui.components.NavBarComponent
20+
import fr.utbm.sy43.pilulito.ui.components.TopBarComponent
21+
import fr.utbm.sy43.pilulito.ui.navigation.Route
22+
import fr.utbm.sy43.pilulito.ui.theme.LocalThemeState
23+
import fr.utbm.sy43.pilulito.viewmodels.SettingsViewModel
24+
25+
26+
private val Orange = Color(0xFFFF6C00)
27+
private val OrangeLight = Color(0xFFFFF3EC)
28+
private val CardBg = Color(0xFFFAFAFA)
29+
30+
@Composable
31+
fun SettingsScreen(
32+
navController: NavHostController,
33+
viewModel: SettingsViewModel = viewModel(factory = AppViewModelProvider.Factory)
34+
) {
35+
val uiState by viewModel.uiState.collectAsState()
36+
val themeState = LocalThemeState.current
37+
38+
Column(modifier = Modifier.fillMaxSize()) {
39+
TopBarComponent(navController)
40+
41+
Column(
42+
modifier = Modifier
43+
.weight(1f)
44+
.fillMaxWidth()
45+
.verticalScroll(rememberScrollState())
46+
.padding(20.dp),
47+
verticalArrangement = Arrangement.spacedBy(20.dp)
48+
) {
49+
Text(
50+
text = "Settings",
51+
fontSize = 24.sp,
52+
fontWeight = FontWeight.Bold,
53+
color = Orange
54+
)
55+
56+
57+
SettingsCard(title = "Appearance") {
58+
Row(
59+
modifier = Modifier.fillMaxWidth(),
60+
horizontalArrangement = Arrangement.SpaceBetween,
61+
verticalAlignment = Alignment.CenterVertically
62+
) {
63+
Column {
64+
Text("Dark mode", fontSize = 16.sp, fontWeight = FontWeight.Medium)
65+
Text(
66+
if (themeState.isDark) "Dark theme enabled"
67+
else "Light theme enabled",
68+
fontSize = 13.sp,
69+
color = Color.Gray
70+
)
71+
}
72+
Switch(
73+
checked = themeState.isDark,
74+
onCheckedChange = { themeState.isDark = it },
75+
colors = SwitchDefaults.colors(
76+
checkedThumbColor = Color.White,
77+
checkedTrackColor = Orange,
78+
uncheckedThumbColor = Color.White,
79+
uncheckedTrackColor = Color.LightGray
80+
)
81+
)
82+
}
83+
}
84+
85+
SettingsCard(title = "Change email") {
86+
Text(
87+
text = "Current: ${uiState.email}",
88+
fontSize = 13.sp,
89+
color = Color.Gray
90+
)
91+
Spacer(Modifier.height(8.dp))
92+
OutlinedTextField(
93+
value = uiState.newEmail,
94+
onValueChange = { viewModel.onNewEmailChanged(it) },
95+
label = { Text("New email") },
96+
singleLine = true,
97+
modifier = Modifier.fillMaxWidth(),
98+
colors = settingsFieldColors()
99+
)
100+
Spacer(Modifier.height(8.dp))
101+
Button(
102+
onClick = { viewModel.saveEmail() },
103+
modifier = Modifier.fillMaxWidth(),
104+
shape = RoundedCornerShape(12.dp),
105+
colors = ButtonDefaults.buttonColors(containerColor = Orange)
106+
) {
107+
Text("Update email")
108+
}
109+
}
110+
111+
SettingsCard(title = "Change password") {
112+
OutlinedTextField(
113+
value = uiState.newPassword,
114+
onValueChange = { viewModel.onNewPasswordChanged(it) },
115+
label = { Text("New password") },
116+
singleLine = true,
117+
visualTransformation = PasswordVisualTransformation(),
118+
modifier = Modifier.fillMaxWidth(),
119+
colors = settingsFieldColors()
120+
)
121+
Spacer(Modifier.height(8.dp))
122+
OutlinedTextField(
123+
value = uiState.confirmPassword,
124+
onValueChange = { viewModel.onConfirmPasswordChanged(it) },
125+
label = { Text("Confirm password") },
126+
singleLine = true,
127+
visualTransformation = PasswordVisualTransformation(),
128+
modifier = Modifier.fillMaxWidth(),
129+
colors = settingsFieldColors()
130+
)
131+
Spacer(Modifier.height(8.dp))
132+
Button(
133+
onClick = { viewModel.savePassword() },
134+
modifier = Modifier.fillMaxWidth(),
135+
shape = RoundedCornerShape(12.dp),
136+
colors = ButtonDefaults.buttonColors(containerColor = Orange)
137+
) {
138+
Text("Update password")
139+
}
140+
}
141+
142+
uiState.successMessage?.let {
143+
Text(it, color = Color(0xFF4CAF50), fontSize = 14.sp, fontWeight = FontWeight.Medium)
144+
}
145+
uiState.errorMessage?.let {
146+
Text(it, color = MaterialTheme.colorScheme.error, fontSize = 14.sp)
147+
}
148+
149+
Spacer(Modifier.height(8.dp))
150+
}
151+
152+
var showDeleteDialog by remember { mutableStateOf(false) }
153+
154+
if (showDeleteDialog) {
155+
AlertDialog(
156+
onDismissRequest = { showDeleteDialog = false },
157+
shape = RoundedCornerShape(20.dp),
158+
title = { Text("Delete account", fontWeight = FontWeight.Bold) },
159+
text = { Text("This action is permanent. Your account will be deleted.") },
160+
confirmButton = {
161+
TextButton(onClick = {
162+
viewModel.deleteAccount(onAccountDeleted = {
163+
navController.navigate(Route.Login.route) {
164+
popUpTo(0) { inclusive = true }
165+
}})
166+
}) {
167+
Text("Delete", color = Color(0xFFE53935), fontWeight = FontWeight.Bold)
168+
}
169+
},
170+
dismissButton = {
171+
TextButton(onClick = { showDeleteDialog = false }) {
172+
Text("Cancel")
173+
}
174+
}
175+
)
176+
}
177+
178+
Button(
179+
onClick = { showDeleteDialog = true },
180+
modifier = Modifier.fillMaxWidth().height(56.dp),
181+
shape = RoundedCornerShape(16.dp),
182+
colors = ButtonDefaults.buttonColors(
183+
containerColor = Color(0xFFE53935).copy(alpha = 0.1f),
184+
contentColor = Color(0xFFE53935)
185+
)
186+
) {
187+
Text("Delete account", fontSize = 16.sp, fontWeight = FontWeight.Bold)
188+
}
189+
190+
NavBarComponent(navController)
191+
}
192+
}
193+
194+
@Composable
195+
private fun SettingsCard(title: String, content: @Composable ColumnScope.() -> Unit) {
196+
Card(
197+
shape = RoundedCornerShape(20.dp),
198+
colors = CardDefaults.cardColors(containerColor = CardBg),
199+
elevation = CardDefaults.cardElevation(0.dp),
200+
modifier = Modifier.fillMaxWidth()
201+
) {
202+
Column(
203+
modifier = Modifier.padding(16.dp),
204+
verticalArrangement = Arrangement.spacedBy(4.dp)
205+
) {
206+
Text(title, fontSize = 14.sp, color = Color.Gray)
207+
Spacer(Modifier.height(4.dp))
208+
content()
209+
}
210+
}
211+
}
212+
213+
@Composable
214+
private fun settingsFieldColors() = OutlinedTextFieldDefaults.colors(
215+
focusedBorderColor = Orange,
216+
unfocusedBorderColor = Color(0xFFDDDDDD),
217+
focusedLabelColor = Orange
218+
)

0 commit comments

Comments
 (0)