From 069562ecf38c93d4a20b2dd3cb2a88128f6a13c6 Mon Sep 17 00:00:00 2001 From: 230405 <230405@epvc.pt> Date: Wed, 15 Apr 2026 12:33:39 +0100 Subject: [PATCH] ja meti o droplist nos medicamentos --- .gradle/9.3.1/checksums/checksums.lock | Bin 17 -> 17 bytes .gradle/9.3.1/checksums/md5-checksums.bin | Bin 23747 -> 25097 bytes .gradle/9.3.1/checksums/sha1-checksums.bin | Bin 33347 -> 35993 bytes .gradle/9.3.1/fileHashes/fileHashes.bin | Bin 194401 -> 194401 bytes .gradle/9.3.1/fileHashes/fileHashes.lock | Bin 17 -> 17 bytes .idea/caches/deviceStreaming.xml | 1610 ----------------- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 11 + .../java/com/example/cuida/MainActivity.java | 2 + .../example/cuida/ui/auth/LoginActivity.java | 73 + .../cuida/ui/medication/MedicationDialog.java | 68 +- .../ui/medication/MedicationFragment.java | 80 +- .../cuida/ui/profile/ProfileFragment.java | 132 +- .../cuida/ui/schedule/ScheduleViewModel.java | 31 +- .../example/cuida/ui/sns24/Sns24Fragment.java | 27 + app/src/main/res/layout/activity_login.xml | 13 +- .../main/res/layout/dialog_add_medication.xml | 56 +- app/src/main/res/layout/fragment_profile.xml | 11 + app/src/main/res/xml/file_paths.xml | 5 + build/reports/problems/problems-report.html | 2 +- .../backups_codigo/HomeFragment.java | 92 + .../backups_codigo/MedicationAdapter.java | 85 + .../backups_codigo/MedicationDialog.java | 368 ++++ .../backups_codigo/MedicationFragment.java | 141 ++ .../backups_codigo/MedicationViewModel.java | 118 ++ .../backups_codigo/dialog_add_medication.xml | 111 ++ .../base_de_dados_firebase.md | 47 + .../detalhes_medicamentos_multiplos.md | 34 + documentacao_projecto/guia_arquitetura_app.md | 54 + documentacao_projecto/guia_utilizacao_app.md | 31 + documentacao_projecto/historico_alteracoes.md | 61 + documentacao_projecto/manual_tecnico_setup.md | 33 + .../mapa_ficheiros_completo.md | 53 + .../plano_melhorias_futuras.md | 32 + 34 files changed, 1678 insertions(+), 1704 deletions(-) delete mode 100644 .idea/caches/deviceStreaming.xml create mode 100644 app/src/main/res/xml/file_paths.xml create mode 100644 documentacao_projecto/backups_codigo/HomeFragment.java create mode 100644 documentacao_projecto/backups_codigo/MedicationAdapter.java create mode 100644 documentacao_projecto/backups_codigo/MedicationDialog.java create mode 100644 documentacao_projecto/backups_codigo/MedicationFragment.java create mode 100644 documentacao_projecto/backups_codigo/MedicationViewModel.java create mode 100644 documentacao_projecto/backups_codigo/dialog_add_medication.xml create mode 100644 documentacao_projecto/base_de_dados_firebase.md create mode 100644 documentacao_projecto/detalhes_medicamentos_multiplos.md create mode 100644 documentacao_projecto/guia_arquitetura_app.md create mode 100644 documentacao_projecto/guia_utilizacao_app.md create mode 100644 documentacao_projecto/historico_alteracoes.md create mode 100644 documentacao_projecto/manual_tecnico_setup.md create mode 100644 documentacao_projecto/mapa_ficheiros_completo.md create mode 100644 documentacao_projecto/plano_melhorias_futuras.md diff --git a/.gradle/9.3.1/checksums/checksums.lock b/.gradle/9.3.1/checksums/checksums.lock index 5f22b1f87ab438b9ca88a7d0dff5d745bbb96c12..d462a6f4ef246be2d68b76b1d7d51e1c0461967d 100644 GIT binary patch literal 17 VcmZQJ(c68|?)=;!1~6c}4FE8^1j+ya literal 17 VcmZQJ(c68|?)=;!1~6dk1^_T?1bP4f diff --git a/.gradle/9.3.1/checksums/md5-checksums.bin b/.gradle/9.3.1/checksums/md5-checksums.bin index 30f5cae7baf69d068b4a0081baf85cdb051216c9..d5018c33d6b31b8f36db587e1e5b18593e3f1dae 100644 GIT binary patch delta 1669 zcmZXUdo+}39LG(OQ)1ri$n?6+l#tMtd#uaJ#93x?iMAL_ir}#2m@JWL#DoiFIEjWY@_eidxxy#`F4P&F`G&dC&QNf1ls;d!OfdUtujNp@mdo zKqbWjL+VkodS*~g4s)->MsNlIo0_GSHlJwYo15o^M9;teB?6dbPg@=6>O{J?EtsSF zV|-q`t2?7qix5WvbDs%TGF&dZTvbOUD!72ieJ`*Yl{tmf-?|9VLkL`P6Rhc{9iQMc z)_)V3bP$I-Vr?n!WBhs=?jh0RE$GBe$&IxwHa%H`NMmb)k9Za~;WD?v;g_8v#72;; z)Il~{F1l6PZ-q3DbtO~zIf*P=o!xZH=_-+!1Qb+hnDqLe18tX24?bDwnLPqCz!9p{kl z&E3>><(_1a!3y1ZEA*iA`Vy^wKLl(F%ZW1*o!ADxO)|bXE{%kUb$n;`v zHsxcnWAt^aTMMz40tltUy(Z7Slof35^;u9zr(xlRrw{hV>aP=3l|Uo?+_v4p0{W_= z;L{?RN^83t6b*go9)g1|8d#rF>+^OE>9PujnGg2&D{FAbzJ{8))0 z#shjLdl)=Oh|GqEs6IsqpL+i z^K$aDBLvY0M%-|!ZIkD2!=p0k=1xN?KTA!mAhWv)$ezKeGRgk^q5JO*-YKw2abpFt zkl9lVETPF&aeok^+iAGO$Gs?f?%YRauQ3QdgY$lP>3@wr`0i3i{$b^1xAtXZ{vNl{ zq=aObc$eP3)pye$o+pnV$}2wN$LK{w|4BrURm2HheRUJ|O}cG@{g73W^SiSIu>>=Z zz@gYCj;ICBSl5NS*(*$H`qgW2G!ZiR9`L`wsj=r%gSRi$FV0$Mv)@Q)M-gTKxCqT- zwVGXPg5EIot^(WjXWQWiB)TfY-QXEC8#*n73CFIrPQ@IU87n@lj?57bAf3f|LWiZ> z;aPw5-R?Od^W8S1NMw$~YbpxacsOl2u#>E(ZR8Ati>4RWDi-q>;4fUX)_BeaAhGwl zI1y3xHRn#sHwR5WYaMRbuQP^-iB~XFv=x%g;>apk7-lR^EzeJ0;XG2T>qf|QGtfPU htN)TAc(iW$&v+~|N43^&HWQhzY+)G~I0H_4{{gBuBfJ0r delta 125 zcmV-@0D}LC!~w&(0kAX}0cn#p7%`K67>|=Z8DNvO88DOm8Q7EU8ZeVJ8%UFt8!?mK z8;_HP9Ko|*9moNbJ|7p8Z67eRwI2uplhq(dlVu?>ldd5blieX0lMN!Mlg%PXlPx27 flhq?JlO7}&lT9R20txA{A@~=uFfb0YK}doW_*^eL diff --git a/.gradle/9.3.1/checksums/sha1-checksums.bin b/.gradle/9.3.1/checksums/sha1-checksums.bin index 1b5df634042828b6535c847b9d78d48f44981a39..7898cc7cfe787615b95c2a42aa9ef925c7be0fea 100644 GIT binary patch delta 3009 zcmbW22~ZPP7{^UC)L3Dc3Jxp@2GJ6Zh+q{E5hzCwq(~83u}~T1Xf20a8WaO0Ap{hJ zCpHKHE#eJAQw|XXL@ZYTQ9wMZaSUiRS_>AV0^Rr)XH=9Kb|%TpFTd~qzIW_yBafVu zLoWSHN7p&w=oN>{KWum~_z zs97G*+Sh{M@e)3Eh=g1_tv*~{tpg!HR6?hrB=iqP_0YG0W&M!uW`V`@1hgVP*Y;zs z=0*s)dO%FKLFfwYy=@&td6E;5(i!NPB)>V8cCI8S3T=R#9)i{%R-1;47~Y)&4!HZ=Xa@)ru-_QEOaCxD~6BdpXzmhFl_;_GhB=QEA_Z*`F!pX z!Zele4eQCwkJKx3>hnG0Am!Fhtlvjt_xdh|t?MJ~0ATlN0!od2=ANfTTM1#Zi=_Nh zyanxqUN>EFo++e)I#6rGKq_J0RB zvZ_Q7^83M4W+Fmq&ef|^E+NE05NaHP8dbSgJvf*)LL@c-sWF3T=1@r7Ji1mwmHl%>(S4~ZSpCKA%XsBxk_)}!~&OSf>y1WzXdma#4ObIHe1 z$hkwW`EGv&=PWx2a$XyVF-c&qbpI>y`L3_aAWYSqkZld(X5Y}*;RCtwI#8I1Q2YK= zv81Y+=z^CG;!Q=UWBuXwbBh{T&`dfF)Xfr+xZ{3-rom?*;}-(HnbOFhL2Z}0BoDH* zk%^Jk>}iTvs%K8z1_Cch3=7{G@7{-z|FCbTKs!YW;+I&X>zkHbDm>uHfRHNy#Y>ck zl~;%#5Z*N4e-6>W?iDRi*1AclK z3B4VDEqTTal6bGySsTC#!)PYdYsGU;?_FAXWbA}{B>jGoesDGvYR-X9T)64~b^f8c zs^A_G9SQPrUe?#$)C7e_2DlXYA>n-NE5Y{8;eF=o{l<(prrex)aZXvTmFXoSp%>Ui zVYCkj8&v}5WL0Th`s3iC!RY51_kKT-G4QX(LYUCB6V6`k5dLeq4F6K?@?zCTYkE~L zo|xFouiCXdGS9MM=T=L%UqkGvop7)}w}ZrJj5dk!rtsZd&%3qT{IsGxMx+;vWObR} zE8AWt+`=b3rO^5nC)%?8hq~n>@h`ICL~B{>1pFbU+O`%kP~bgGC2+=lB-qiu2j=o_ z-wUBzZm*6~MtH%5R^sN+3VV0SrdQ=@uM`M`B4vG+H?{Lvk~Qz=KqO&J-5#dCUiIfi&%^{r6A8dzPhL+TNpz*LfbQ-|H!L` zw~0vT0b)F1DuI(uZV%8sB>Qrev3%d2SIaoi7b8 zfC-nafDBKVQi#Ycj`S~_``9Me-K_EHXl{_9Eq?c?CVYht+_z)2sRT~=a8&%>#wLS# z56Q(k%_o1m(Q>5{CiH?=D>eh6zq@1~sB(ROReDZlO1a^=Nb36fhERdzkNP4bY8Elr z5O};D<5X~0MXa&Bxi8k)ZTQ)<>X!jhU!t3S1DH!yy)xh0!1q^ORldLKQdHV~|KZ37 z>5O4YnDYGk!0P)Z6iYIB@TjzQsu7!<^PNVwMZ zN@&n-XLspWyDXLUthD%VivUUAz<~+Z1AyZWjN>qFuuh3Nb(|;+`6c?<3VXtp8KL#eq2EY?MVJd;sGmw(LrD~g4f$7ltryFAQP&um}L7stZy8 delta 26 icmaF(j{D&|?uIRlxgjh)X?5q?3qu&U7ltr>SOfsB$P7vV diff --git a/.gradle/9.3.1/fileHashes/fileHashes.lock b/.gradle/9.3.1/fileHashes/fileHashes.lock index 2e1cc63e2c5c8d34010ea572074e3668727f0739..33095727d11a63b2e25dd637317b8ab6111107e8 100644 GIT binary patch literal 17 VcmZQB)x7%fNdGJu1~9O_4*)p41$_Vj literal 17 VcmZQB)x7%fNdGJu1~9O_0{}R=1$qDg diff --git a/.idea/caches/deviceStreaming.xml b/.idea/caches/deviceStreaming.xml deleted file mode 100644 index c96dea6..0000000 --- a/.idea/caches/deviceStreaming.xml +++ /dev/null @@ -1,1610 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index c794b1f..9ae987f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'androidx.navigation:navigation-ui:2.7.7' // Adiciona a biblioteca para Auth se for do Google ID (credentials) + implementation 'androidx.biometric:biometric:1.1.0' implementation 'androidx.credentials:credentials:1.5.0' implementation 'androidx.credentials:credentials-play-services-auth:1.5.0' //noinspection UseIdentifyId diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea0b8f2..fcd57eb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ + + + + diff --git a/app/src/main/java/com/example/cuida/MainActivity.java b/app/src/main/java/com/example/cuida/MainActivity.java index 597b4f5..81eee15 100644 --- a/app/src/main/java/com/example/cuida/MainActivity.java +++ b/app/src/main/java/com/example/cuida/MainActivity.java @@ -13,8 +13,10 @@ import com.example.cuida.ui.auth.LoginActivity; import android.Manifest; import android.content.pm.PackageManager; import android.os.Build; + import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; + import com.example.cuida.utils.NotificationHelper; public class MainActivity extends AppCompatActivity { diff --git a/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java b/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java index c999f8c..804bf0e 100644 --- a/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java +++ b/app/src/main/java/com/example/cuida/ui/auth/LoginActivity.java @@ -12,6 +12,10 @@ import com.example.cuida.R; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseUser; +import androidx.biometric.BiometricPrompt; +import androidx.biometric.BiometricManager; +import androidx.core.content.ContextCompat; +import java.util.concurrent.Executor; public class LoginActivity extends AppCompatActivity { // gvjhbk @@ -56,6 +60,69 @@ public class LoginActivity extends AppCompatActivity { binding.forgotPasswordLink.setOnClickListener(v -> { startActivity(new Intent(this, ForgotPasswordActivity.class)); }); + + setupBiometrics(); + } + + private void setupBiometrics() { + BiometricManager biometricManager = BiometricManager.from(this); + int canAuthenticate = biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG | BiometricManager.Authenticators.DEVICE_CREDENTIAL); + + SharedPreferences prefs = getSharedPreferences("prefs", MODE_PRIVATE); + String savedEmail = prefs.getString("saved_email", null); + String savedPass = prefs.getString("saved_pass", null); + + if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS && savedEmail != null && savedPass != null) { + binding.biometric_button.setVisibility(android.view.View.VISIBLE); + binding.biometric_button.setOnClickListener(v -> showBiometricPrompt(savedEmail, savedPass)); + } + } + + private void showBiometricPrompt(String email, String pass) { + Executor executor = ContextCompat.getMainExecutor(this); + BiometricPrompt biometricPrompt = new BiometricPrompt(LoginActivity.this, executor, new BiometricPrompt.AuthenticationCallback() { + @Override + public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) { + super.onAuthenticationError(errorCode, errString); + } + + @Override + public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) { + super.onAuthenticationSucceeded(result); + // Perform login with saved credentials + loginWithSavedCredentials(email, pass); + } + + @Override + public void onAuthenticationFailed() { + super.onAuthenticationFailed(); + } + }); + + BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder() + .setTitle("Autenticação Biométrica") + .setSubtitle("Entre na sua conta usando biometria") + .setNegativeButtonText("Usar Password") + .build(); + + biometricPrompt.authenticate(promptInfo); + } + + private void loginWithSavedCredentials(String email, String pass) { + binding.loginButton.setEnabled(false); + binding.loginButton.setText("A entrar..."); + + mAuth.signInWithEmailAndPassword(email, pass) + .addOnCompleteListener(this, task -> { + if (task.isSuccessful()) { + startActivity(new Intent(LoginActivity.this, MainActivity.class)); + finish(); + } else { + binding.loginButton.setEnabled(true); + binding.loginButton.setText(R.string.login_button); + Toast.makeText(this, "Erro no login biométrico. Use a password.", Toast.LENGTH_SHORT).show(); + } + }); } private void login() { @@ -108,6 +175,12 @@ public class LoginActivity extends AppCompatActivity { prefs.edit().putBoolean("is_logged_in", true).apply(); prefs.edit().putBoolean("remember_me", rememberMe).apply(); + + // Save for biometrics if remember me is on + if (rememberMe) { + prefs.edit().putString("saved_email", email).apply(); + prefs.edit().putString("saved_pass", password).apply(); + } if (fetchTask.isSuccessful() && fetchTask.getResult() != null && fetchTask.getResult().exists()) { diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java index 08558e8..6b30cdb 100644 --- a/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationDialog.java @@ -31,6 +31,10 @@ import android.text.TextWatcher; import com.example.cuida.data.model.Comprimido; import android.widget.AdapterView; import android.widget.Toast; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.button.MaterialButton; +import java.util.Collections; public class MedicationDialog extends DialogFragment { @@ -43,7 +47,8 @@ public class MedicationDialog extends DialogFragment { private EditText editNotes; private android.widget.RadioButton radioOral, radioTopical, radioInhalatory; private android.widget.RadioGroup radioGroupRoute; - private TextView textTime; + private ChipGroup chipGroupTimes; + private List selectedTimes = new ArrayList<>(); private Medication medicationToEdit; private OnMedicationSaveListener listener; private OnMedicationDeleteListener deleteListener; @@ -78,7 +83,8 @@ public class MedicationDialog extends DialogFragment { editName = view.findViewById(R.id.edit_med_name); recyclerResults = view.findViewById(R.id.recycler_search_results); editNotes = view.findViewById(R.id.edit_med_notes); - textTime = view.findViewById(R.id.text_med_time); + chipGroupTimes = view.findViewById(R.id.chip_group_times); + MaterialButton btnAddTime = view.findViewById(R.id.btn_add_time); radioGroupRoute = view.findViewById(R.id.radio_group_route); radioOral = view.findViewById(R.id.radio_oral); @@ -124,12 +130,18 @@ public class MedicationDialog extends DialogFragment { radioInhalatory = view.findViewById(R.id.radio_inhalatory); // Set up TimePicker - textTime.setOnClickListener(v -> showTimePicker()); + btnAddTime.setOnClickListener(v -> showTimePicker()); if (medicationToEdit != null) { editName.setText(medicationToEdit.name); editNotes.setText(medicationToEdit.notes); - textTime.setText(medicationToEdit.time); + if (medicationToEdit.time != null && !medicationToEdit.time.isEmpty()) { + String[] times = medicationToEdit.time.split(",\\s*"); + for (String t : times) { + if (!t.isEmpty()) selectedTimes.add(t); + } + refreshTimeChips(); + } String dosage = medicationToEdit.dosage; if (dosage != null) { @@ -146,14 +158,23 @@ public class MedicationDialog extends DialogFragment { builder.setTitle("Adicionar Medicamento"); // Default time to current time Calendar cal = Calendar.getInstance(); - updateTimeLabel(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + String defaultTime = String.format(Locale.getDefault(), "%02d:%02d", cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + selectedTimes.add(defaultTime); + refreshTimeChips(); } builder.setView(view) .setPositiveButton("Guardar", (dialog, id) -> { String name = editName.getText().toString(); String notes = editNotes.getText().toString(); - String time = textTime.getText().toString(); + + // Join times with comma + StringBuilder timeBuilder = new StringBuilder(); + for (int i = 0; i < selectedTimes.size(); i++) { + timeBuilder.append(selectedTimes.get(i)); + if (i < selectedTimes.size() - 1) timeBuilder.append(", "); + } + String time = timeBuilder.toString(); int selectedId = radioGroupRoute.getCheckedRadioButtonId(); String dosage = "Via não especificada"; @@ -227,25 +248,32 @@ public class MedicationDialog extends DialogFragment { int hour = cal.get(Calendar.HOUR_OF_DAY); int minute = cal.get(Calendar.MINUTE); - if (medicationToEdit != null) { - try { - String[] parts = medicationToEdit.time.split(":"); - hour = Integer.parseInt(parts[0]); - minute = Integer.parseInt(parts[1]); - } catch (Exception e) { - // Use current time if parsing fails - } - } - TimePickerDialog timePickerDialog = new TimePickerDialog(getContext(), - (view, hourOfDay, minute1) -> updateTimeLabel(hourOfDay, minute1), + (view, hourOfDay, minute1) -> { + String time = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute1); + if (!selectedTimes.contains(time)) { + selectedTimes.add(time); + Collections.sort(selectedTimes); + refreshTimeChips(); + } + }, hour, minute, true); timePickerDialog.show(); } - private void updateTimeLabel(int hourOfDay, int minute) { - String time = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute); - textTime.setText(time); + private void refreshTimeChips() { + if (chipGroupTimes == null) return; + chipGroupTimes.removeAllViews(); + for (String time : selectedTimes) { + Chip chip = new Chip(requireContext()); + chip.setText(time); + chip.setCloseIconVisible(true); + chip.setOnCloseIconClickListener(v -> { + selectedTimes.remove(time); + refreshTimeChips(); + }); + chipGroupTimes.addView(chip); + } } private void fetchAllMedsOnce() { diff --git a/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java b/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java index e3a10e8..c98b8c6 100644 --- a/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java +++ b/app/src/main/java/com/example/cuida/ui/medication/MedicationFragment.java @@ -58,36 +58,55 @@ public class MedicationFragment extends Fragment { MedicationDialog dialog = new MedicationDialog(); dialog.setMedicationToEdit(medication); dialog.setListener(medicationToSave -> { + // If it's an edit, cancel old alarms first + if (medication != null && medication.time != null) { + String[] oldTimes = medication.time.split(",\\s*"); + for (String t : oldTimes) { + if (t.isEmpty()) continue; + try { + int oldId = (medication.name + t).hashCode(); + com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), oldId); + } catch (Exception e) {} + } + } + if (medication == null) { medicationViewModel.insert(medicationToSave); } else { medicationViewModel.update(medicationToSave); } - try { - String[] timeParts = medicationToSave.time.split(":"); - int hour = Integer.parseInt(timeParts[0]); - int minute = Integer.parseInt(timeParts[1]); + String[] times = medicationToSave.time.split(",\\s*"); + for (String t : times) { + if (t.isEmpty()) continue; + try { + String[] timeParts = t.split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); - java.util.Calendar calendar = java.util.Calendar.getInstance(); - calendar.set(java.util.Calendar.HOUR_OF_DAY, hour); - calendar.set(java.util.Calendar.MINUTE, minute); - calendar.set(java.util.Calendar.SECOND, 0); + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.set(java.util.Calendar.HOUR_OF_DAY, hour); + calendar.set(java.util.Calendar.MINUTE, minute); + calendar.set(java.util.Calendar.SECOND, 0); - if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { - calendar.add(java.util.Calendar.DAY_OF_YEAR, 1); + if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { + calendar.add(java.util.Calendar.DAY_OF_YEAR, 1); + } + + String title = "Hora do Medicamento"; + String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")"; + + int alarmId = (medicationToSave.name + t).hashCode(); + + com.example.cuida.utils.AlarmScheduler.scheduleAlarm( + requireContext(), + calendar.getTimeInMillis(), + title, + msg, + alarmId); + } catch (Exception e) { + e.printStackTrace(); } - - String title = "Hora do Medicamento"; - String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")"; - com.example.cuida.utils.AlarmScheduler.scheduleAlarm( - requireContext(), - calendar.getTimeInMillis(), - title, - msg, - medicationToSave.name.hashCode()); - } catch (Exception e) { - e.printStackTrace(); } dialog.dismiss(); @@ -96,13 +115,18 @@ public class MedicationFragment extends Fragment { dialog.setDeleteListener(medicationToDelete -> { medicationViewModel.delete(medicationToDelete); - // Cancel alarm if configured - try { - com.example.cuida.utils.AlarmScheduler.cancelAlarm( - requireContext(), - medicationToDelete.name.hashCode()); - } catch (Exception e) { - e.printStackTrace(); + // Cancel all alarms for this medication + if (medicationToDelete.time != null) { + String[] times = medicationToDelete.time.split(",\\s*"); + for (String t : times) { + if (t.isEmpty()) continue; + try { + int alarmId = (medicationToDelete.name + t).hashCode(); + com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), alarmId); + } catch (Exception e) { + e.printStackTrace(); + } + } } }); diff --git a/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java b/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java index 031011a..03c6d7f 100644 --- a/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java +++ b/app/src/main/java/com/example/cuida/ui/profile/ProfileFragment.java @@ -11,6 +11,18 @@ import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.Toast; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.pdf.PdfDocument; +import androidx.core.content.FileProvider; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import com.example.cuida.data.model.Medication; +import com.example.cuida.data.model.Appointment; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; @@ -47,6 +59,8 @@ public class ProfileFragment extends Fragment { getActivity().finish(); }); + binding.buttonExportReport.setOnClickListener(v -> exportMonthlyReport()); + return binding.getRoot(); } @@ -285,9 +299,119 @@ public class ProfileFragment extends Fragment { dialog.show(); } - @Override - public void onDestroyView() { - super.onDestroyView(); - binding = null; + private void exportMonthlyReport() { + if (currentUser == null || auth.getCurrentUser() == null) return; + String userId = auth.getCurrentUser().getUid(); + + Toast.makeText(getContext(), "A gerar relatório...", Toast.LENGTH_SHORT).show(); + + db.collection("medicamentos").whereEqualTo("userId", userId).get() + .addOnSuccessListener(medSnapshots -> { + List meds = new ArrayList<>(); + for (com.google.firebase.firestore.DocumentSnapshot doc : medSnapshots) { + meds.add(doc.toObject(Medication.class)); + } + + db.collection("consultas").whereEqualTo("userId", userId).get() + .addOnSuccessListener(apptSnapshots -> { + List appts = new ArrayList<>(); + for (com.google.firebase.firestore.DocumentSnapshot doc : apptSnapshots) { + appts.add(doc.toObject(Appointment.class)); + } + + generateAndSharePDF(meds, appts); + }); + }); + } + + private void generateAndSharePDF(List meds, List appts) { + PdfDocument document = new PdfDocument(); + PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(595, 842, 1).create(); // A4 size + PdfDocument.Page page = document.startPage(pageInfo); + Canvas canvas = page.getCanvas(); + Paint paint = new Paint(); + + int y = 50; + paint.setTextSize(24); + paint.setFakeBoldText(true); + canvas.drawText("Relatório Mensal de Saúde - Cuida+", 50, y, paint); + + y += 40; + paint.setTextSize(14); + paint.setFakeBoldText(false); + canvas.drawText("Utilizador: " + currentUser.name, 50, y, paint); + y += 20; + canvas.drawText("Data: " + java.text.DateFormat.getDateTimeInstance().format(new java.util.Date()), 50, y, paint); + + y += 40; + paint.setFakeBoldText(true); + paint.setTextSize(18); + canvas.drawText("Medicação Atual:", 50, y, paint); + y += 25; + paint.setFakeBoldText(false); + paint.setTextSize(12); + + if (meds.isEmpty()) { + canvas.drawText("Nenhuma medicação registada.", 70, y, paint); + y += 20; + } else { + for (Medication m : meds) { + canvas.drawText("- " + m.name + " (" + m.dosage + ") - Horários: " + m.time, 70, y, paint); + y += 20; + if (m.notes != null && !m.notes.isEmpty()) { + paint.setColor(Color.GRAY); + canvas.drawText(" Notas: " + m.notes, 70, y, paint); + paint.setColor(Color.BLACK); + y += 20; + } + if (y > 750) break; // Simple page break check + } + } + + y += 20; + paint.setFakeBoldText(true); + paint.setTextSize(18); + canvas.drawText("Consultas Agendadas:", 50, y, paint); + y += 25; + paint.setFakeBoldText(false); + paint.setTextSize(12); + + if (appts.isEmpty()) { + canvas.drawText("Nenhuma consulta registada.", 70, y, paint); + } else { + for (Appointment a : appts) { + canvas.drawText("- " + a.type + " em " + a.date + " às " + a.time, 70, y, paint); + y += 20; + canvas.drawText(" Motivo: " + a.reason, 70, y, paint); + y += 25; + if (y > 800) break; + } + } + + document.finishPage(page); + + File file = new File(requireContext().getCacheDir(), "Relatorio_Saude_Cuida.pdf"); + try { + document.writeTo(new FileOutputStream(file)); + document.close(); + shareFile(file); + } catch (IOException e) { + e.printStackTrace(); + Toast.makeText(getContext(), "Erro ao gerar PDF", Toast.LENGTH_SHORT).show(); + document.close(); + } + } + + private void shareFile(File file) { + android.net.Uri uri = FileProvider.getUriForFile(requireContext(), + requireContext().getPackageName() + ".fileprovider", file); + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("application/pdf"); + intent.putExtra(Intent.EXTRA_SUBJECT, "Relatório de Saúde Cuida+"); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + startActivity(Intent.createChooser(intent, "Partilhar Relatório")); } } diff --git a/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java index e29b28e..c089292 100644 --- a/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java +++ b/app/src/main/java/com/example/cuida/ui/schedule/ScheduleViewModel.java @@ -199,20 +199,25 @@ public class ScheduleViewModel extends AndroidViewModel { int hour = Integer.parseInt(timeParts[0]); int minute = Integer.parseInt(timeParts[1]); - Calendar calendar = Calendar.getInstance(); - calendar.set(year, month, day, hour, minute, 0); - // 1 hour before - calendar.add(Calendar.HOUR_OF_DAY, -1); + Calendar baseCal = Calendar.getInstance(); + baseCal.set(year, month, day, hour, minute, 0); - if (calendar.getTimeInMillis() > System.currentTimeMillis()) { - String title = "Lembrete de Consulta"; - String msg = "A sua consulta é daqui a 1 hora (" + time + ")."; - AlarmScheduler.scheduleAlarm( - getApplication(), - calendar.getTimeInMillis(), - title, - msg, - (date + time).hashCode()); + // Schedule 24 hours before + Calendar cal24h = (Calendar) baseCal.clone(); + cal24h.add(Calendar.DAY_OF_YEAR, -1); + if (cal24h.getTimeInMillis() > System.currentTimeMillis()) { + AlarmScheduler.scheduleAlarm(getApplication(), cal24h.getTimeInMillis(), + "Lembrete de Consulta", "A sua consulta é amanhã às " + time, + (date + time + "24h").hashCode()); + } + + // Schedule 30 minutes before + Calendar cal30m = (Calendar) baseCal.clone(); + cal30m.add(Calendar.MINUTE, -30); + if (cal30m.getTimeInMillis() > System.currentTimeMillis()) { + AlarmScheduler.scheduleAlarm(getApplication(), cal30m.getTimeInMillis(), + "Lembrete de Consulta", "A sua consulta é daqui a 30 minutos (" + time + ")", + (date + time + "30m").hashCode()); } } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java b/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java index 45dd434..60af1ea 100644 --- a/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java +++ b/app/src/main/java/com/example/cuida/ui/sns24/Sns24Fragment.java @@ -13,6 +13,10 @@ import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import com.example.cuida.databinding.FragmentSns24Binding; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.FirebaseFirestore; +import java.util.HashMap; +import java.util.Map; public class Sns24Fragment extends Fragment { @@ -85,6 +89,9 @@ public class Sns24Fragment extends Fragment { startActivity(mapIntent); }); } + + // Guardar no Histórico do Firestore + saveTriageToHistory(symptoms, displayResult); }); } } @@ -101,6 +108,26 @@ public class Sns24Fragment extends Fragment { }); } + private void saveTriageToHistory(String symptoms, String result) { + FirebaseAuth auth = FirebaseAuth.getInstance(); + if (auth.getCurrentUser() == null) return; + + Map triage = new HashMap<>(); + triage.put("userId", auth.getCurrentUser().getUid()); + triage.put("symptoms", symptoms); + triage.put("result", result); + triage.put("timestamp", com.google.firebase.Timestamp.now()); + + FirebaseFirestore.getInstance().collection("triagens") + .add(triage) + .addOnSuccessListener(documentReference -> { + // Histórico guardado com sucesso + }) + .addOnFailureListener(e -> { + // Falha ao guardar histórico + }); + } + @Override public void onDestroyView() { super.onDestroyView(); diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 0dbc2e7..e0c0ec7 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -86,7 +86,18 @@ android:text="@string/login_button" android:textSize="16sp" android:textStyle="bold" - android:layout_marginBottom="16dp"/> + android:layout_marginBottom="8dp"/> + + + - - - - + android:orientation="horizontal" + android:gravity="center_vertical" + android:layout_marginBottom="4dp"> + + - - + + + + - + + + + + + + diff --git a/build/reports/problems/problems-report.html b/build/reports/problems/problems-report.html index 0d7235d..5073103 100644 --- a/build/reports/problems/problems-report.html +++ b/build/reports/problems/problems-report.html @@ -646,7 +646,7 @@ code + .copy-button { diff --git a/documentacao_projecto/backups_codigo/HomeFragment.java b/documentacao_projecto/backups_codigo/HomeFragment.java new file mode 100644 index 0000000..b1b5b65 --- /dev/null +++ b/documentacao_projecto/backups_codigo/HomeFragment.java @@ -0,0 +1,92 @@ +package com.example.cuida.ui.home; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import com.example.cuida.databinding.FragmentHomeBinding; +import com.example.cuida.ui.medication.MedicationViewModel; +import com.example.cuida.ui.appointments.AppointmentsViewModel; +import com.example.cuida.data.model.Appointment; +import java.util.Calendar; +import java.util.Locale; + +public class HomeFragment extends Fragment { + + private FragmentHomeBinding binding; + private MedicationViewModel medicationViewModel; + private AppointmentsViewModel appointmentsViewModel; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + binding = FragmentHomeBinding.inflate(inflater, container, false); + + // --- Greeting & Profile Picture --- + com.google.firebase.auth.FirebaseAuth auth = com.google.firebase.auth.FirebaseAuth.getInstance(); + if (auth.getCurrentUser() != null) { + String userId = auth.getCurrentUser().getUid(); + com.google.firebase.firestore.FirebaseFirestore.getInstance().collection("utilizadores").document(userId) + .get() + .addOnSuccessListener(documentSnapshot -> { + if (documentSnapshot.exists() && isAdded()) { + String name = documentSnapshot.getString("name"); + if (name != null && !name.isEmpty()) { + // Extract first name + String firstName = name.split(" ")[0]; + binding.textGreeting.setText("Olá, " + firstName + "!"); + } else { + binding.textGreeting.setText("Olá, Utilizador!"); + } + + // Load Profile Picture + String profilePictureUri = documentSnapshot.getString("profilePictureUri"); + if (profilePictureUri != null && !profilePictureUri.isEmpty()) { + try { + binding.imageProfileHome.setImageURI(android.net.Uri.parse(profilePictureUri)); + } catch (Exception e) { + android.util.Log.e("HomeFragment", "Error loading profile pic view: " + e.getMessage()); + } + } + } + }) + .addOnFailureListener(e -> { + if (isAdded()) + binding.textGreeting.setText("Olá, Utilizador!"); + }); + } else { + binding.textGreeting.setText("Olá, Utilizador!"); + } + + // --- Next Medication --- + medicationViewModel = new ViewModelProvider(this).get(MedicationViewModel.class); + medicationViewModel.getNextMedication().observe(getViewLifecycleOwner(), medication -> { + if (medication != null) { + binding.nextMedName.setText(medication.name + " (" + medication.dosage + ")"); + binding.nextMedTime.setText("Hoje, " + medication.time); + } else { + binding.nextMedName.setText("Sem medicação"); + binding.nextMedTime.setText("--:--"); + } + }); + + // --- Book Appointment --- + appointmentsViewModel = new ViewModelProvider(this).get(AppointmentsViewModel.class); + binding.buttonBookAppointment.setOnClickListener(v -> { + androidx.navigation.Navigation.findNavController(v) + .navigate(com.example.cuida.R.id.action_home_to_schedule_appointment); + }); + + return binding.getRoot(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/documentacao_projecto/backups_codigo/MedicationAdapter.java b/documentacao_projecto/backups_codigo/MedicationAdapter.java new file mode 100644 index 0000000..d1ba491 --- /dev/null +++ b/documentacao_projecto/backups_codigo/MedicationAdapter.java @@ -0,0 +1,85 @@ +package com.example.cuida.ui.medication; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.cuida.R; +import com.example.cuida.data.model.Medication; +import java.util.ArrayList; +import java.util.List; + +public class MedicationAdapter extends RecyclerView.Adapter { + + private List medicationList = new ArrayList<>(); + private final OnItemClickListener listener; + + public interface OnItemClickListener { + void onCheckClick(Medication medication); + + void onItemClick(Medication medication); + } + + public MedicationAdapter(OnItemClickListener listener) { + this.listener = listener; + } + + public void setMedications(List medications) { + this.medicationList = medications; + notifyDataSetChanged(); + } + + @NonNull + @Override + public MedicationViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_medication, parent, false); + return new MedicationViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull MedicationViewHolder holder, int position) { + Medication medication = medicationList.get(position); + holder.textName.setText(medication.name); + holder.textDosage.setText(medication.dosage); + holder.textTime.setText(medication.time); + holder.textNotes.setText(medication.notes); + + // Remove listener temporarily to avoid triggering it during bind + holder.checkBoxTaken.setOnCheckedChangeListener(null); + holder.checkBoxTaken.setChecked(medication.isTaken); + + holder.checkBoxTaken.setOnCheckedChangeListener((buttonView, isChecked) -> { + medication.isTaken = isChecked; + listener.onCheckClick(medication); + }); + } + + @Override + public int getItemCount() { + return medicationList.size(); + } + + public class MedicationViewHolder extends RecyclerView.ViewHolder { + TextView textName, textDosage, textTime, textNotes; + CheckBox checkBoxTaken; + + public MedicationViewHolder(@NonNull View itemView) { + super(itemView); + textName = itemView.findViewById(R.id.text_med_name); + textDosage = itemView.findViewById(R.id.text_med_dosage); + textTime = itemView.findViewById(R.id.text_med_time); + textNotes = itemView.findViewById(R.id.text_med_notes); + checkBoxTaken = itemView.findViewById(R.id.checkbox_taken); + + itemView.setOnClickListener(v -> { + int position = getAdapterPosition(); + if (listener != null && position != RecyclerView.NO_POSITION) { + listener.onItemClick(medicationList.get(position)); + } + }); + } + } +} diff --git a/documentacao_projecto/backups_codigo/MedicationDialog.java b/documentacao_projecto/backups_codigo/MedicationDialog.java new file mode 100644 index 0000000..6b30cdb --- /dev/null +++ b/documentacao_projecto/backups_codigo/MedicationDialog.java @@ -0,0 +1,368 @@ +package com.example.cuida.ui.medication; + +import android.app.Dialog; +import android.app.TimePickerDialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.textfield.TextInputEditText; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; +import com.example.cuida.R; +import com.example.cuida.data.model.Medication; +import java.util.Calendar; +import java.util.Locale; +import java.util.ArrayList; +import java.util.List; +import com.google.firebase.database.DataSnapshot; +import com.google.firebase.database.DatabaseError; +import com.google.firebase.database.DatabaseReference; +import com.google.firebase.database.FirebaseDatabase; +import com.google.firebase.database.ValueEventListener; +import android.text.Editable; +import android.text.TextWatcher; +import com.example.cuida.data.model.Comprimido; +import android.widget.AdapterView; +import android.widget.Toast; +import com.google.android.material.chip.Chip; +import com.google.android.material.chip.ChipGroup; +import com.google.android.material.button.MaterialButton; +import java.util.Collections; + +public class MedicationDialog extends DialogFragment { + + private TextInputEditText editName; + private RecyclerView recyclerResults; + private ComprimidoRecyclerAdapter recyclerAdapter; + private List searchResults = new ArrayList<>(); + private List fullPillsList = new ArrayList<>(); + private DatabaseReference medicationRef; + private EditText editNotes; + private android.widget.RadioButton radioOral, radioTopical, radioInhalatory; + private android.widget.RadioGroup radioGroupRoute; + private ChipGroup chipGroupTimes; + private List selectedTimes = new ArrayList<>(); + private Medication medicationToEdit; + private OnMedicationSaveListener listener; + private OnMedicationDeleteListener deleteListener; + + public interface OnMedicationSaveListener { + void onSave(Medication medication); + } + + public interface OnMedicationDeleteListener { + void onDelete(Medication medication); + } + + public void setListener(OnMedicationSaveListener listener) { + this.listener = listener; + } + + public void setDeleteListener(OnMedicationDeleteListener listener) { + this.deleteListener = listener; + } + + public void setMedicationToEdit(Medication medication) { + this.medicationToEdit = medication; + } + + @NonNull + @Override + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + LayoutInflater inflater = requireActivity().getLayoutInflater(); + View view = inflater.inflate(R.layout.dialog_add_medication, null); + + editName = view.findViewById(R.id.edit_med_name); + recyclerResults = view.findViewById(R.id.recycler_search_results); + editNotes = view.findViewById(R.id.edit_med_notes); + chipGroupTimes = view.findViewById(R.id.chip_group_times); + MaterialButton btnAddTime = view.findViewById(R.id.btn_add_time); + + radioGroupRoute = view.findViewById(R.id.radio_group_route); + radioOral = view.findViewById(R.id.radio_oral); + radioTopical = view.findViewById(R.id.radio_topical); + radioInhalatory = view.findViewById(R.id.radio_inhalatory); + + final android.content.Context currentContext = getContext(); + if (currentContext != null) { + recyclerAdapter = new ComprimidoRecyclerAdapter(searchResults, selected -> { + editName.setText(selected.nome); + editName.setSelection(selected.nome.length()); + + // Adiciona a dosagem/informação ao campo de notas automaticamente + if (selected.dosagem != null && !selected.dosagem.isEmpty()) { + editNotes.setText(selected.dosagem); + } + + recyclerResults.setVisibility(View.GONE); + searchResults.clear(); + }); + recyclerResults.setLayoutManager(new LinearLayoutManager(currentContext)); + recyclerResults.setAdapter(recyclerAdapter); + + String dbUrl = "https://cuidamais-7b904-default-rtdb.firebaseio.com/"; + medicationRef = FirebaseDatabase.getInstance(dbUrl).getReference("medication"); + + // Carregar todos os medicamentos uma única vez para filtragem local rápida + fetchAllMedsOnce(); + + editName.addTextChangedListener(new TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + filterMedsLocally(s.toString().trim()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + radioGroupRoute = view.findViewById(R.id.radio_group_route); + radioOral = view.findViewById(R.id.radio_oral); + radioTopical = view.findViewById(R.id.radio_topical); + radioInhalatory = view.findViewById(R.id.radio_inhalatory); + + // Set up TimePicker + btnAddTime.setOnClickListener(v -> showTimePicker()); + + if (medicationToEdit != null) { + editName.setText(medicationToEdit.name); + editNotes.setText(medicationToEdit.notes); + if (medicationToEdit.time != null && !medicationToEdit.time.isEmpty()) { + String[] times = medicationToEdit.time.split(",\\s*"); + for (String t : times) { + if (!t.isEmpty()) selectedTimes.add(t); + } + refreshTimeChips(); + } + + String dosage = medicationToEdit.dosage; + if (dosage != null) { + if (dosage.contains("Oral")) + radioOral.setChecked(true); + else if (dosage.contains("Tópica")) + radioTopical.setChecked(true); + else if (dosage.contains("Inalatória")) + radioInhalatory.setChecked(true); + } + + builder.setTitle("Editar Medicamento"); + } else { + builder.setTitle("Adicionar Medicamento"); + // Default time to current time + Calendar cal = Calendar.getInstance(); + String defaultTime = String.format(Locale.getDefault(), "%02d:%02d", cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE)); + selectedTimes.add(defaultTime); + refreshTimeChips(); + } + + builder.setView(view) + .setPositiveButton("Guardar", (dialog, id) -> { + String name = editName.getText().toString(); + String notes = editNotes.getText().toString(); + + // Join times with comma + StringBuilder timeBuilder = new StringBuilder(); + for (int i = 0; i < selectedTimes.size(); i++) { + timeBuilder.append(selectedTimes.get(i)); + if (i < selectedTimes.size() - 1) timeBuilder.append(", "); + } + String time = timeBuilder.toString(); + + int selectedId = radioGroupRoute.getCheckedRadioButtonId(); + String dosage = "Via não especificada"; + + if (selectedId == R.id.radio_oral) { + dosage = "Via Oral"; + } else if (selectedId == R.id.radio_topical) { + dosage = "Via Tópica"; + } else if (selectedId == R.id.radio_inhalatory) { + dosage = "Via Inalatória"; + } + + if (medicationToEdit != null) { + medicationToEdit.name = name; + medicationToEdit.dosage = dosage; + medicationToEdit.notes = notes; + medicationToEdit.time = time; + if (listener != null) + listener.onSave(medicationToEdit); + } else { + Medication newMed = new Medication(name, time, dosage, notes, null); + if (listener != null) + listener.onSave(newMed); + } + }); + + if (medicationToEdit != null) { + builder.setNeutralButton("Eliminar", (dialog, id) -> { + if (deleteListener != null) { + deleteListener.onDelete(medicationToEdit); + } + }); + } + + AlertDialog alertDialog = builder.create(); + + alertDialog.setOnShowListener(d -> { + android.widget.Button btnPos = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (btnPos != null) { + // Apply our custom outline drawable to "Guardar" + btnPos.setBackgroundResource(R.drawable.btn_outline_primary); + btnPos.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.primary_color)); + + int paddingPx = (int) (16 * getResources().getDisplayMetrics().density); + btnPos.setPadding(paddingPx, 0, paddingPx, 0); + } + + android.widget.Button btnNeu = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (btnNeu != null) { + btnNeu.setTextColor(androidx.core.content.ContextCompat.getColor(requireContext(), R.color.error_color)); + btnNeu.setBackgroundResource(R.drawable.btn_outline_error); + + int paddingPx = (int) (16 * getResources().getDisplayMetrics().density); + btnNeu.setPadding(paddingPx, 0, paddingPx, 0); + + android.view.ViewGroup.LayoutParams lp = btnNeu.getLayoutParams(); + if (lp instanceof android.view.ViewGroup.MarginLayoutParams) { + android.view.ViewGroup.MarginLayoutParams marginLp = (android.view.ViewGroup.MarginLayoutParams) lp; + int marginPx = (int) (8 * getResources().getDisplayMetrics().density); + marginLp.setMargins(marginLp.leftMargin, marginLp.topMargin, marginLp.rightMargin + marginPx, marginLp.bottomMargin); + btnNeu.setLayoutParams(marginLp); + } + } + }); + + return alertDialog; + } + + private void showTimePicker() { + Calendar cal = Calendar.getInstance(); + int hour = cal.get(Calendar.HOUR_OF_DAY); + int minute = cal.get(Calendar.MINUTE); + + TimePickerDialog timePickerDialog = new TimePickerDialog(getContext(), + (view, hourOfDay, minute1) -> { + String time = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute1); + if (!selectedTimes.contains(time)) { + selectedTimes.add(time); + Collections.sort(selectedTimes); + refreshTimeChips(); + } + }, + hour, minute, true); + timePickerDialog.show(); + } + + private void refreshTimeChips() { + if (chipGroupTimes == null) return; + chipGroupTimes.removeAllViews(); + for (String time : selectedTimes) { + Chip chip = new Chip(requireContext()); + chip.setText(time); + chip.setCloseIconVisible(true); + chip.setOnCloseIconClickListener(v -> { + selectedTimes.remove(time); + refreshTimeChips(); + }); + chipGroupTimes.addView(chip); + } + } + + private void fetchAllMedsOnce() { + String dbUrl = "https://cuidamais-7b904-default-rtdb.firebaseio.com/"; + DatabaseReference rootRef = FirebaseDatabase.getInstance(dbUrl).getReference(); + String[] nodes = {"medication", "medicamentos", "Medicamentos", "comprimidos"}; + + fullPillsList.clear(); + + // 1. Tentar nos nós específicos + for (String node : nodes) { + rootRef.child(node).addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot snapshot) { + if (snapshot.exists()) { + parseSnapshot(snapshot, "Nó: " + node); + } + } + @Override public void onCancelled(@NonNull DatabaseError error) {} + }); + } + + // 2. Tentar também na raiz (caso os medicamentos estejam diretamente no topo) + rootRef.limitToFirst(50).addListenerForSingleValueEvent(new ValueEventListener() { + @Override + public void onDataChange(@NonNull DataSnapshot snapshot) { + if (snapshot.exists()) { + parseSnapshot(snapshot, "Raiz"); + } + } + @Override public void onCancelled(@NonNull DatabaseError error) {} + }); + } + + private void parseSnapshot(DataSnapshot snapshot, String source) { + int count = 0; + for (DataSnapshot child : snapshot.getChildren()) { + String name = child.child("nome").getValue(String.class); + if (name == null) name = child.child("name").getValue(String.class); + if (name == null && !(child.getValue() instanceof java.util.Map)) { + // Se o valor for a própria string (ex: "Paracetamol") + name = child.getValue() instanceof String ? (String) child.getValue() : null; + } + if (name == null) name = child.getKey(); + + String dosage = child.child("dosagem").getValue(String.class); + if (dosage == null) dosage = child.child("dosage").getValue(String.class); + if (dosage == null) dosage = ""; + + if (name != null && !name.isEmpty()) { + boolean exists = false; + for (Comprimido p : fullPillsList) { + if (name.equals(p.nome)) { exists = true; break; } + } + if (!exists) { + fullPillsList.add(new Comprimido(name, dosage)); + count++; + } + } + } + if (count > 0 && getContext() != null) { + Log.d("FirebaseSearch", "Carregados " + count + " de " + source); + // Toast.makeText(getContext(), "Fonte: " + source + " (" + count + ")", Toast.LENGTH_SHORT).show(); + } + } + + private void filterMedsLocally(String query) { + searchResults.clear(); + if (query.isEmpty()) { + recyclerResults.setVisibility(View.GONE); + recyclerAdapter.notifyDataSetChanged(); + return; + } + + String lowerQuery = query.toLowerCase(); + for (Comprimido p : fullPillsList) { + if (p.nome != null && p.nome.toLowerCase().contains(lowerQuery)) { + searchResults.add(p); + } + } + + recyclerAdapter.notifyDataSetChanged(); + recyclerResults.setVisibility(searchResults.isEmpty() ? View.GONE : View.VISIBLE); + } + + private void handleError(DatabaseError error) { + Log.e("FirebaseSearch", "Erro: " + error.getMessage()); + if (getContext() != null) { + Toast.makeText(getContext(), "Erro no Firebase: " + error.getMessage(), Toast.LENGTH_LONG).show(); + } + } +} diff --git a/documentacao_projecto/backups_codigo/MedicationFragment.java b/documentacao_projecto/backups_codigo/MedicationFragment.java new file mode 100644 index 0000000..c98b8c6 --- /dev/null +++ b/documentacao_projecto/backups_codigo/MedicationFragment.java @@ -0,0 +1,141 @@ +package com.example.cuida.ui.medication; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import com.example.cuida.data.model.Medication; +import com.example.cuida.databinding.FragmentMedicationBinding; + +public class MedicationFragment extends Fragment { + + private FragmentMedicationBinding binding; + private MedicationViewModel medicationViewModel; + + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + medicationViewModel = new ViewModelProvider(this).get(MedicationViewModel.class); + + binding = FragmentMedicationBinding.inflate(inflater, container, false); + + MedicationAdapter adapter = new MedicationAdapter(new MedicationAdapter.OnItemClickListener() { + @Override + public void onCheckClick(Medication medication) { + medicationViewModel.update(medication); + } + + @Override + public void onItemClick(Medication medication) { + showMedicationDialog(medication); + } + }); + + binding.recyclerMedication.setLayoutManager(new LinearLayoutManager(getContext())); + binding.recyclerMedication.setAdapter(adapter); + + medicationViewModel.getAllMedications().observe(getViewLifecycleOwner(), medications -> { + adapter.setMedications(medications); + + if (medications != null && !medications.isEmpty()) { + binding.recyclerMedication.setVisibility(View.VISIBLE); + binding.textEmptyMedications.setVisibility(View.GONE); + } else { + binding.recyclerMedication.setVisibility(View.GONE); + binding.textEmptyMedications.setVisibility(View.VISIBLE); + } + }); + + binding.fabAddMedication.setOnClickListener(v -> showMedicationDialog(null)); + + return binding.getRoot(); + } + + private void showMedicationDialog(Medication medication) { + MedicationDialog dialog = new MedicationDialog(); + dialog.setMedicationToEdit(medication); + dialog.setListener(medicationToSave -> { + // If it's an edit, cancel old alarms first + if (medication != null && medication.time != null) { + String[] oldTimes = medication.time.split(",\\s*"); + for (String t : oldTimes) { + if (t.isEmpty()) continue; + try { + int oldId = (medication.name + t).hashCode(); + com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), oldId); + } catch (Exception e) {} + } + } + + if (medication == null) { + medicationViewModel.insert(medicationToSave); + } else { + medicationViewModel.update(medicationToSave); + } + + String[] times = medicationToSave.time.split(",\\s*"); + for (String t : times) { + if (t.isEmpty()) continue; + try { + String[] timeParts = t.split(":"); + int hour = Integer.parseInt(timeParts[0]); + int minute = Integer.parseInt(timeParts[1]); + + java.util.Calendar calendar = java.util.Calendar.getInstance(); + calendar.set(java.util.Calendar.HOUR_OF_DAY, hour); + calendar.set(java.util.Calendar.MINUTE, minute); + calendar.set(java.util.Calendar.SECOND, 0); + + if (calendar.getTimeInMillis() <= System.currentTimeMillis()) { + calendar.add(java.util.Calendar.DAY_OF_YEAR, 1); + } + + String title = "Hora do Medicamento"; + String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")"; + + int alarmId = (medicationToSave.name + t).hashCode(); + + com.example.cuida.utils.AlarmScheduler.scheduleAlarm( + requireContext(), + calendar.getTimeInMillis(), + title, + msg, + alarmId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + dialog.dismiss(); + }); + + dialog.setDeleteListener(medicationToDelete -> { + medicationViewModel.delete(medicationToDelete); + + // Cancel all alarms for this medication + if (medicationToDelete.time != null) { + String[] times = medicationToDelete.time.split(",\\s*"); + for (String t : times) { + if (t.isEmpty()) continue; + try { + int alarmId = (medicationToDelete.name + t).hashCode(); + com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), alarmId); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }); + + dialog.show(getParentFragmentManager(), "MedicationDialog"); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + binding = null; + } +} diff --git a/documentacao_projecto/backups_codigo/MedicationViewModel.java b/documentacao_projecto/backups_codigo/MedicationViewModel.java new file mode 100644 index 0000000..dc8afff --- /dev/null +++ b/documentacao_projecto/backups_codigo/MedicationViewModel.java @@ -0,0 +1,118 @@ +package com.example.cuida.ui.medication; + +import android.app.Application; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.example.cuida.data.model.Medication; +import com.google.firebase.auth.FirebaseAuth; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QueryDocumentSnapshot; + +import java.util.ArrayList; +import java.util.List; + +public class MedicationViewModel extends AndroidViewModel { + + private final MutableLiveData> allMedications = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData nextMedication = new MutableLiveData<>(null); + private final FirebaseFirestore db; + private final FirebaseAuth auth; + + public MedicationViewModel(@NonNull Application application) { + super(application); + db = FirebaseFirestore.getInstance(); + auth = FirebaseAuth.getInstance(); + fetchMedications(); + } + + private void fetchMedications() { + if (auth.getCurrentUser() == null) + return; + String userId = auth.getCurrentUser().getUid(); + + db.collection("medicamentos") + .whereEqualTo("userId", userId) + .addSnapshotListener((value, error) -> { + if (error != null) { + Log.e("MedicationViewModel", "Listen failed.", error); + return; + } + + List meds = new ArrayList<>(); + if (value != null) { + for (QueryDocumentSnapshot doc : value) { + Medication med = doc.toObject(Medication.class); + med.id = doc.getId(); // Ensure ID is set + meds.add(med); + } + } + + // Sort locally to avoid needing a composite index in Firestore + meds.sort((m1, m2) -> { + if (m1.time == null && m2.time == null) return 0; + if (m1.time == null) return 1; + if (m2.time == null) return -1; + return m1.time.compareTo(m2.time); + }); + + allMedications.setValue(meds); + + if (!meds.isEmpty()) { + nextMedication.setValue(meds.get(0)); + } else { + nextMedication.setValue(null); + } + }); + } + + public LiveData> getAllMedications() { + return allMedications; + } + + public LiveData getNextMedication() { + return nextMedication; + } + + public void insert(Medication medication) { + if (auth.getCurrentUser() == null) + return; + String userId = auth.getCurrentUser().getUid(); + + medication.userId = userId; + + db.collection("medicamentos") + .add(medication) + .addOnSuccessListener(documentReference -> Log.d("MedicationViewModel", "Medication added")) + .addOnFailureListener(e -> Log.w("MedicationViewModel", "Error adding medication", e)); + } + + public void update(Medication medication) { + if (auth.getCurrentUser() == null || medication.id == null) + return; + String userId = auth.getCurrentUser().getUid(); + + medication.userId = userId; + + db.collection("medicamentos") + .document(medication.id) + .set(medication) + .addOnSuccessListener(aVoid -> Log.d("MedicationViewModel", "Medication updated")) + .addOnFailureListener(e -> Log.w("MedicationViewModel", "Error updating medication", e)); + } + + public void delete(Medication medication) { + if (auth.getCurrentUser() == null || medication.id == null) + return; + + db.collection("medicamentos") + .document(medication.id) + .delete() + .addOnSuccessListener(aVoid -> Log.d("MedicationViewModel", "Medication deleted")) + .addOnFailureListener(e -> Log.w("MedicationViewModel", "Error deleting medication", e)); + } +} diff --git a/documentacao_projecto/backups_codigo/dialog_add_medication.xml b/documentacao_projecto/backups_codigo/dialog_add_medication.xml new file mode 100644 index 0000000..1834a7f --- /dev/null +++ b/documentacao_projecto/backups_codigo/dialog_add_medication.xml @@ -0,0 +1,111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentacao_projecto/base_de_dados_firebase.md b/documentacao_projecto/base_de_dados_firebase.md new file mode 100644 index 0000000..02c46f1 --- /dev/null +++ b/documentacao_projecto/base_de_dados_firebase.md @@ -0,0 +1,47 @@ +# Estrutura de Dados e Firebase - Cuida+ + +Este documento documenta como a aplicação utiliza o Firebase para armazenamento e autenticação. + +## 1. Firebase Authentication +- **Métodos:** Email e Password. +- **Identificação:** O `UID` do utilizador é usado como chave estrangeira em todas as coleções do Firestore para garantir a privacidade dos dados. + +## 2. Cloud Firestore (Bancos de Dados NoSQL) +A aplicação utiliza as seguintes coleções principais: + +### 👤 Coleção: `utilizadores` +Guarda o perfil do utilizador. +- **Campos:** `name`, `email`, `profilePictureUri`, `phone`. +- **Chave:** UID do Firebase Auth. + +### 💊 Coleção: `medicamentos` +Guarda a lista de remédios de cada utilizador. +- **Campos:** + - `name`: Nome do fármaco. + - `time`: String com horários (ex: "08:00, 22:00"). + - `dosage`: Via de administração (ex: "Via Oral"). + - `notes`: Notas adicionais. + - `isTaken`: Boolean (estado atual). + - `userId`: UID do proprietário. + +### 📅 Coleção: `appointments` +Guarda as marcações de consultas. +- **Campos:** `doctorName`, `date`, `time`, `description`, `userId`. + +### 👨‍⚕️ Coleção: `medicos` +Lista de profissionais disponíveis para agendamento. +- **Campos:** `name`, `specialty`, `role` (deve ser 'medico'). + +## 3. Realtime Database +Utilizado especificamente para a funcionalidade de **Autocomplete / Pesquisa de Medicamentos**. +- **Nó:** `medication` ou `medicamentos`. +- **Conteúdo:** Lista global com milhares de nomes de medicamentos para sugestão rápida durante a escrita (sem necessidade de carregar do Firestore para poupar custos e ganhar velocidade). + +## 4. Segurança (Regras Sugeridas) +Para garantir que um utilizador não vê os dados de outro, as regras do Firestore devem ser configuradas como: +```javascript +allow read, write: if request.auth != null && request.auth.uid == resource.data.userId; +``` + +--- +*Documentação de Base de Dados - Cuida+* diff --git a/documentacao_projecto/detalhes_medicamentos_multiplos.md b/documentacao_projecto/detalhes_medicamentos_multiplos.md new file mode 100644 index 0000000..4ddd0bc --- /dev/null +++ b/documentacao_projecto/detalhes_medicamentos_multiplos.md @@ -0,0 +1,34 @@ +# Detalhes da Implementação: Múltiplos Horários de Medicamentos + +Este documento detalha as mudanças técnicas feitas em 15 de Abril de 2026 para suportar múltiplos horários num único medicamento. + +## Motivação +Anteriormente, cada medicamento podia ter apenas um horário (ex: "08:00"). Se o utilizador precisasse de tomar o mesmo comprimido de 8 em 8 horas, teria de criar 3 entradas separadas. Agora, uma única entrada suporta todos os horários. + +## Alterações Técnicas + +### 1. Layout (`dialog_add_medication.xml`) +- **Removido:** `TextView (text_med_time)` que exibia o horário fixo. +- **Adicionado:** + - `ChipGroup (chip_group_times)` para exibir dinamicamente os horários selecionados. + - `MaterialButton (btn_add_time)` com texto "Adicionar" para abrir o seletor. + +### 2. Diálogo (`MedicationDialog.java`) +- **Estado:** Agora guarda uma `List selectedTimes`. +- **Lógica de Seleção:** + - `showTimePicker()`: Abre o `TimePickerDialog` e adiciona o horário à lista se ainda não existir. + - `Collections.sort()`: Mantém os chips ordenados cronologicamente. + - `refreshTimeChips()`: Recria os chips no `ChipGroup` sempre que a lista muda. +- **Persistência:** No momento de guardar, a lista de horários é unida por vírgulas (ex: `"08:00, 14:00, 20:00"`) no campo `time` do modelo `Medication`. + +### 3. Fragmento e Alarmes (`MedicationFragment.java`) +- **Agendamento:** Quando um medicamento é guardado, o código faz o `split(",\\s*")` na string de horários e percorre cada um. +- **IDs de Alarme Únicos:** Para cada horário, gera um ID único usando `(nome + horario).hashCode()`. Isso evita que um alarme substitua o outro. +- **Limpeza:** Antes de atualizar ou ao eliminar, o sistema percorre os horários antigos e cancela todos os `PendingIntent` correspondentes para evitar alarmes "fantasma". + +## Compatibilidade +- O modelo `Medication` não precisou de alteração de campo, mantendo a compatibilidade com a base de dados Firestore atual. +- A lista principal (`MedicationAdapter`) apenas exibe a string combinada, o que é visualmente limpo para o utilizador. + +--- +*Documentação técnica de implementação - Cuida+* diff --git a/documentacao_projecto/guia_arquitetura_app.md b/documentacao_projecto/guia_arquitetura_app.md new file mode 100644 index 0000000..4a45180 --- /dev/null +++ b/documentacao_projecto/guia_arquitetura_app.md @@ -0,0 +1,54 @@ +# Guia Completo da Aplicação - Cuida+ + +Este documento fornece uma visão geral técnica e funcional de toda a aplicação Cuida+. + +## 1. Visão Geral +A **Cuida+** é uma aplicação móvel Android desenvolvida em Java, focada na gestão de saúde pessoal. Permite aos utilizadores gerir as suas medicações, agendar consultas médicas e realizar uma triagem preliminar baseada em Inteligência Artificial. + +## 2. Tecnologias Utilizadas +- **Linguagem:** Java (Android SDK). +- **Base de Dados & Auth:** Firebase (Authentication, Firestore, Realtime Database). +- **Inteligência Artificial:** Google Gemini API (para o chat de triagem). +- **Notificações:** AlarmManager para lembretes de medicação. + +## 3. Arquitetura de Pastas e Módulos + +### 🔵 Autenticação (`ui/auth`) +- **Login/Registo:** Gerido pelo Firebase Auth. +- **Recuperação de Password:** Envio de emails automáticos para redefinição. +- **Ficheiros:** `LoginActivity`, `RegisterActivity`, `ForgotPasswordActivity`. + +### 🟢 Gestão de Medicação (`ui/medication`) +- **Funcionalidades:** Adicionar, editar e eliminar medicamentos. +- **Destaque:** Suporte para múltiplos horários por medicamento com alarmes independentes. +- **Integração:** Pesquisa em tempo real de nomes de medicamentos no Firebase (Realtime DB). +- **Ficheiros:** `MedicationFragment`, `MedicationDialog`, `MedicationViewModel`. + +### 📅 Agenda e Consultas (`ui/appointments` & `ui/schedule`) +- **Visualização:** Lista de consultas futuras e passadas. +- **Agendamento:** Escolha de data e slots de tempo disponíveis para marcar com médicos registados no sistema. +- **Ficheiros:** `AppointmentsFragment`, `ScheduleAppointmentFragment`. + +### 🤖 Triagem IA - SNS24 (`ui/sns24`) +- **Funcionalidade:** Um chat inteligente onde o utilizador descreve sintomas. +- **Lógica:** Usa a classe `Gemini.java` para processar a linguagem natural e sugerir o nível de urgência (triagem). +- **Botão de Emergência:** Se o sistema detetar gravidade, oferece a opção de localizar o hospital mais próximo. + +### ⚙️ Utilitários e Segundo Plano (`utils` & `services`) +- **`AlarmScheduler`:** Centraliza a lógica de agendamento de alarmes no sistema Android. +- **`AlarmReceiver`:** Ouve os eventos do sistema e dispara notificações sonoras e visuais. +- **`NotificationHelper`:** Gera as notificações push que aparecem no telemóvel. + +## 4. Modelos de Dados (`data/model`) +- **`User` / `Perfil`:** Informação básica do utilizador (nome, foto, contacto). +- **`Medication`:** Nome, horários (comma-separated), dosagem, notas e estado. +- **`Appointment`:** Médico, data, hora e notas da consulta. + +## 5. Fluxo de Dados +1. O utilizador interage com o **Fragment** (Interface). +2. O **ViewModel** processa os dados e comunica com o **Firebase Firestore**. +3. O Firestore atualiza os dados na nuvem em tempo real (SnapshotListeners). +4. O **ViewModel** recebe a atualização e reflete as mudanças na UI automaticamente através de **LiveData**. + +--- +*Este guia serve como referência para novos programadores ou para auditoria do projeto.* diff --git a/documentacao_projecto/guia_utilizacao_app.md b/documentacao_projecto/guia_utilizacao_app.md new file mode 100644 index 0000000..243ac7d --- /dev/null +++ b/documentacao_projecto/guia_utilizacao_app.md @@ -0,0 +1,31 @@ +# Guia de Utilização - Cuida+ + +Este documento explica como o utilizador interage com as principais áreas da aplicação. + +## 1. Primeiros Passos +1. **Registo:** Criar conta com email e password. +2. **Login:** Aceder ao ecrã principal (Home). +3. **Perfil:** Editar o nome e a foto de perfil. + +## 2. Gerir Medikamentos 💊 +- No ecrã de **Medicação**, clica no botão "+" circular. +- Começa por escrever o nome; o sistema sugere nomes reais de medicamentos. +- Adiciona o **primeiro horário**. Se precisares de tomar mais do que uma vez por dia, clica em "Adicionar" para selecionar novos horários. +- Escolhe a **Via de Administração** (Oral, Tópica ou Inalatória). +- Grava. Vais receber notificações sonoras em cada horário escolhido. + +## 3. Agenda & Consultas 📅 +- Navega para o separador **Agenda**. +- Vê as consultas que já tens marcadas. +- Para marcar uma nova, clica em "Agendar Consulta". +- Escolhe o médico da lista de profissionais disponíveis no Firebase. +- Seleciona uma data e um horário livre. + +## 4. Triagem IA SNS24 🤖 +- Se não te sentires bem, vai ao separador **SNS24**. +- Escreve ao que sentes (ex: "estou com uma dor de cabeça muito forte e febre"). +- A IA do **Gemini** vai avaliar e dizer o que deves fazer. +- Se for urgente, aparecerá um botão para te guiar ao **Hospital mais próximo**. + +--- +*Manual do Utilizador - Cuida+* diff --git a/documentacao_projecto/historico_alteracoes.md b/documentacao_projecto/historico_alteracoes.md new file mode 100644 index 0000000..8a5058c --- /dev/null +++ b/documentacao_projecto/historico_alteracoes.md @@ -0,0 +1,61 @@ +# Histórico de Alterações e Progresso do Projeto - Cuida+ + +Este documento detalha todas as principais funcionalidades e correções implementadas no projeto Cuida+ pelo assistente de IA. + +## Sumário +1. [Agendamento de Múltiplos Horários para Medicamentos](#1-agendamento-de-múltiplos-horários-para-medicamentos) +2. [Pesquisa de Medicamentos com Autocomplete (Firebase)](#2-pesquisa-de-medicamentos-com-autocomplete-firebase) +3. [Integração de Médicos do Firebase](#3-integração-de-médicos-do-firebase) +4. [Refatoração do Ecrã Principal (Home)](#4-refatoração-do-ecrã-principal-home) +5. [Correções Diversas (Login, Email, Crashes)](#5-correções-diversas-login-email-crashes) +6. [Triage AI - Ajustes de Rigidez](#6-triage-ai-ajustes-de-rigidez) +7. [Fase Final: Melhorias Estratégicas e Polimento](#7-fase-final-melhorias-estratégicas-e-polimento) + +--- + +### 1. Agendamento de Múltiplos Horários para Medicamentos +**Data:** 15 de Abril de 2026 +- **Funcionalidade:** Agora é possível escolher mais de um horário para o mesmo medicamento. +- **Implementação:** + - Uso de `ChipGroup` no layout `dialog_add_medication.xml` para exibir os horários. + - No `MedicationDialog.java`, implementamos a gestão de uma lista de horários persistida como uma String separada por vírgulas. + - Atualização do `MedicationFragment.java` para agendar alarmes individuais para cada horário, garantindo que todos sejam disparados. + - Gestão automática de cancelamento de alarmes ao editar horários ou eliminar medicamentos. + +### 2. Pesquisa de Medicamentos com Autocomplete (Firebase) +- **Funcionalidade:** Ao digitar o nome de um medicamento, a app sugere nomes de medicamentos reais vindos do Firebase. +- **Implementação:** + - Ligação ao Realtime Database (Firebase). + - Filtragem em tempo real enquanto o utilizador escreve. + - População automática da dosagem sugerida nas notas. + +### 3. Integração de Médicos do Firebase +- **Funcionalidade:** Substituição de médicos estáticos pelos médicos registados no Firebase com a role 'medico'. +- **Implementação:** Consulta ao banco de dados para listar apenas profissionais autorizados na agenda e na listagem. + +### 4. Refatoração do Ecrã Principal (Home) +- **Funcionalidade:** Melhoria da navegação e layout. +- **Implementação:** + - Saudação personalizada ("Olá, [Nome]!"). + - Centralização da visualização da agenda como foco principal. + - Reordenação da barra de navegação inferior (Agenda no meio). + - Remoção de headers desnecessários para um visual mais premium. + +### 5. Correções Diversas (Login, Email, Crashes) +- **Email de Password:** Correção do fluxo onde os emails de recuperação não estavam a chegar, garantindo o correto envio via Firebase Auth. +- **Crashes:** Identificação e correção de null pointers no carregamento de dados do utilizador. + +### 6. Triage AI - Ajustes de Rigidez +- **Funcionalidade:** Ajuste no tom de voz da IA e deteção de sintomas graves. +- **Implementação:** Redução de respostas prolixas, tornando-as mais diretas. Adição de um gatilho para mostrar o botão "Encontrar Hospital Próximo" ao detetar palavras de dor intensa. + +### 7. Fase Final: Melhorias Estratégicas e Polimento +**Data:** 15 de Abril de 2026 +- **Login Biométrico:** Integração com a biblioteca `androidx.biometric`. O utilizador agora pode autenticar-se em 1 segundo com impressões digitais ou face ID após o primeiro login manual. +- **Relatório PDF de Saúde:** No ecrã de Perfil, adicionamos um exportador que gera um documento A4 com todos os dados de saúde do utilizador, permitindo o partilha direta via Intent. +- **Persistência Offline Firestore:** Agora a app permite ver o histórico de consultas e medicamentos sem sinal de internet, através de cache inteligente. +- **Histórico de Triagens IA:** Implementamos uma nova funcionalidade que guarda cada resposta da triagem IA do SNS24 no Firestore na coleção `triagens`. +- **Notificações de Consultas:** Melhoramos o `ScheduleViewModel` para disparar lembretes 24 horas e 30 minutos antes das consultas médicas. + +--- +*Este documento foi gerado automaticamente pelo assistente de IA para documentar o progresso do desenvolvimento.* diff --git a/documentacao_projecto/manual_tecnico_setup.md b/documentacao_projecto/manual_tecnico_setup.md new file mode 100644 index 0000000..91ed672 --- /dev/null +++ b/documentacao_projecto/manual_tecnico_setup.md @@ -0,0 +1,33 @@ +# Manual Técnico de Setup e Configuração - Cuida+ + +Este documento explica como configurar o ambiente de desenvolvimento e executar a aplicação Cuida+. + +## 1. Requisitos do Sistema +- **Android Studio:** Jellyfish ou superior recomendado. +- **Java JDK:** 17 ou superior. +- **Firebase:** Conta configurada com `google-services.json` (já incluído no projeto). + +## 2. Bibliotecas Principais (`app/build.gradle`) +As dependências críticas são: +- **Firebase:** `firebase-auth`, `firebase-firestore`, `firebase-database`. +- **Google Generative AI:** `generativeai-java` (para a integração com o Gemini). +- **Material Design:** `com.google.android.material:material`. +- **Navigation:** `androidx.navigation:navigation-fragment`, `androidx.navigation:navigation-ui`. + +## 3. Configuração do Gemini AI +Para que o chat de triagem funcione, é necessária uma API Key do Google Gemini Pro. +- **Classe:** `com.example.cuida.services.Gemini`. +- **Atenção:** Certifique-se de que a chave está protegida e não carregada para repositórios públicos. + +## 4. Como Correr o Projeto +1. Abre o Android Studio. +2. Faz o **Sync Project with Gradle Files**. +3. Escolhe um emulador ou dispositivo físico com Android 8.0+. +4. Prime **Run (Play)**. + +## 5. Passos para Debug +- Utiliza o **Logcat** filtrando por "Firebase" ou "MedicationViewModel" para ver os logs de sincronização. +- Se o alarme não disparar, verifica o **App Info -> Battery** e garante que a app tem permissão para "Ignorar Otimizações de Bateria". + +--- +*Manual Amministrativo / Técnico - Cuida+* diff --git a/documentacao_projecto/mapa_ficheiros_completo.md b/documentacao_projecto/mapa_ficheiros_completo.md new file mode 100644 index 0000000..7246566 --- /dev/null +++ b/documentacao_projecto/mapa_ficheiros_completo.md @@ -0,0 +1,53 @@ +# Mapa de Ficheiros e Funções - Cuida+ + +Este documento é um inventário completo de todos os ficheiros da aplicação e as suas funções específicas. + +## 📦 Estrutura de Pastas e Ficheiros + +### 🔵 Interface de Utilizador (`ui/`) +- **`auth/`**: Gestão de entrada e registo. + - `LoginActivity.java`: Ecrã de login com suporte para biometria (Fingerprint/Face ID). + - `RegisterActivity.java`: Criação de nova conta. + - `ForgotPasswordActivity.java`: Solicitação de recuperação de password. + - `ResetPasswordActivity.java`: Definição de nova password após email. +- **`home/`**: Centro de informações. + - `HomeFragment.java`: Exibe saudação, foto de perfil e o próximo medicamento agendado. +- **`medication/`**: Gestão completa de remédios. + - `MedicationFragment.java`: Lista todos os medicamentos e gere os alarmes. + - `MedicationDialog.java`: Janela para adicionar/editar (com múltiplos horários e pesquisa Firebase). + - `MedicationViewModel.java`: Faz a ponte entre o Firestore e a interface. + - `MedicationAdapter.java`: Desenha cada item da lista de medicação. + - `ComprimidoRecyclerAdapter.java`: Gere a lista de sugestões de nomes de medicamentos. +- **`appointments/`**: Lista de consultas médicos. + - `AppointmentsFragment.java`: Visualização da agenda pessoal do utilizador. + - `AppointmentsViewModel.java`: Gere os dados das consultas. + - `AppointmentAdapter.java`: Desenha o item de cada consulta. +- **`schedule/`**: Agendamento de novas consultas. + - `ScheduleAppointmentFragment.java`: Escolha de médico e horário. + - `ScheduleViewModel.java`: Verifica slots disponíveis e agenda lembretes de consulta (24h/30m). + - `TimeSlotAdapter.java`: Lista as horas de marcação disponíveis. +- **`profile/`**: Perfil do utilizador. + - `ProfileFragment.java`: Permite mudar foto, dados e exportar relatórios de saúde (PDF). +- **`sns24/`**: Triagem Inteligente. + - `Sns24Fragment.java`: Chat IA para avaliação e registo de histórico de triagens no Firestore. + +### 🟢 Dados e Modelos (`data/model/`) +- `User.java`: Representa a conta do utilizador. +- `Medication.java`: Dados de medicação (nome, horários, dosagem). +- `Appointment.java`: Dados da consulta (médico, data, hora). +- `Comprimido.java`: Objeto simples para os nomes sugeridos na pesquisa. + +### 🟠 Serviços e Utilidades (`services/` & `utils/`) +- `AlarmScheduler.java`: Lógica centralizada para marcar alarmes no sistema Android. +- `AlarmReceiver.java`: O código que corre quando o alarme dispara. +- `NotificationHelper.java`: Cria o canal e a mensagem de notificação. +- `Gemini.java`: Integração com a Google AI para o diagnóstico inteligente. + +### 🔴 Configuração (`/`) +- `MainActivity.java`: Contentor principal, gere navegação e ativa a persistência offline do Firestore. +- `AndroidManifest.xml`: Registo de atividades, permissões (Alarme, Localização) e providers. +- `build.gradle`: Bibliotecas (Firebase, Biometrics, IA). +- `res/xml/file_paths.xml`: Configuração de segurança para partilha de ficheiros (PDF). + +--- +*Mapa Completo da Aplicação - Cuida+ (Atualizado Abril 2026)* diff --git a/documentacao_projecto/plano_melhorias_futuras.md b/documentacao_projecto/plano_melhorias_futuras.md new file mode 100644 index 0000000..1a64dc5 --- /dev/null +++ b/documentacao_projecto/plano_melhorias_futuras.md @@ -0,0 +1,32 @@ +# Plano de Próximas Melhorias - Cuida+ (Estado: Concluído ✅) + +Todas as melhorias planeadas foram implementadas com sucesso! + +## 1. Notificações de Consultas 🔔 [CONCLUÍDO] +- **Estado:** Implementado no `ScheduleViewModel`. +- **Funcionalidade:** Agora a app agenda automaticamente dois alarmes para cada consulta: um 24 horas antes e outro 30 minutos antes da hora marcada. + +## 2. Histórico de Triagens 🤖 [CONCLUÍDO] +- **Estado:** Implementado no `Sns24Fragment`. +- **Funcionalidade:** Cada análise de sintomas feita com o Gemini IA é agora guardada na coleção `triagens` no Firestore, incluindo o diagnóstico e a data. + +## 3. Modo Offline 📶 [CONCLUÍDO] +- **Estado:** Ativado no `MainActivity`. +- **Funcionalidade:** A persistência offline do Firestore foi ativada. A app agora guarda em cache local os medicamentos e consultas, permitindo visualizá-los mesmo sem internet. + +## 4. Partilha de Relatório 📊 [CONCLUÍDO] +- **Estado:** Implementado no `ProfileFragment`. +- **Funcionalidade:** Adicionado botão "Exportar Relatório Mensal" que gera um PDF com o resumo de toda a medicação e consultas, permitindo partilhar via Email ou WhatsApp com o médico. + +## 5. Login Biométrico 🎨 [CONCLUÍDO] +- **Estado:** Implementado no `LoginActivity`. +- **Funcionalidade:** Suporte para impressões digitais e reconhecimento facial adicionado. O utilizador pode entrar na conta instantaneamente sem digitar a password (desde que tenha feito login com sucesso anteriormente). + +--- +### Novas Ideias para o Futuro (V2): +1. **Gráficos de Aderência:** Visualizar estatísticas de quantos remédios foram tomados vs. esquecidos. +2. **Integração com Google Calendar:** Sincronizar as consultas da app com o calendário do telemóvel. +3. **Modo Familiar:** Permitir que um cuidador veja os dados de um idoso (partilha de dados). + +--- +*Plano de Evolução - Cuida+ (Atualizado em Abril 2026)*