ja meti o droplist nos medicamentos

This commit is contained in:
2026-04-15 12:33:39 +01:00
parent cbfb87d052
commit 069562ecf3
34 changed files with 1678 additions and 1704 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -45,6 +45,7 @@ dependencies {
implementation 'androidx.navigation:navigation-ui:2.7.7' implementation 'androidx.navigation:navigation-ui:2.7.7'
// Adiciona a biblioteca para Auth se for do Google ID (credentials) // 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:1.5.0'
implementation 'androidx.credentials:credentials-play-services-auth:1.5.0' implementation 'androidx.credentials:credentials-play-services-auth:1.5.0'
//noinspection UseIdentifyId //noinspection UseIdentifyId

View File

@@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.USE_EXACT_ALARM" /> <uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<application <application
android:name=".CuidaApplication"
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
@@ -58,6 +59,16 @@
<receiver android:name=".services.AlarmReceiver" android:exported="false" /> <receiver android:name=".services.AlarmReceiver" android:exported="false" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application> </application>
</manifest> </manifest>

View File

@@ -13,8 +13,10 @@ import com.example.cuida.ui.auth.LoginActivity;
import android.Manifest; import android.Manifest;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.os.Build; import android.os.Build;
import androidx.core.app.ActivityCompat; import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import com.example.cuida.utils.NotificationHelper; import com.example.cuida.utils.NotificationHelper;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {

View File

@@ -12,6 +12,10 @@ import com.example.cuida.R;
import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.auth.FirebaseAuth;
import com.google.firebase.auth.FirebaseUser; 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 { public class LoginActivity extends AppCompatActivity {
// gvjhbk // gvjhbk
@@ -56,6 +60,69 @@ public class LoginActivity extends AppCompatActivity {
binding.forgotPasswordLink.setOnClickListener(v -> { binding.forgotPasswordLink.setOnClickListener(v -> {
startActivity(new Intent(this, ForgotPasswordActivity.class)); 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() { private void login() {
@@ -109,6 +176,12 @@ public class LoginActivity extends AppCompatActivity {
prefs.edit().putBoolean("is_logged_in", true).apply(); prefs.edit().putBoolean("is_logged_in", true).apply();
prefs.edit().putBoolean("remember_me", rememberMe).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 if (fetchTask.isSuccessful() && fetchTask.getResult() != null
&& fetchTask.getResult().exists()) { && fetchTask.getResult().exists()) {
com.google.firebase.firestore.DocumentSnapshot documentSnapshot = fetchTask com.google.firebase.firestore.DocumentSnapshot documentSnapshot = fetchTask

View File

@@ -31,6 +31,10 @@ import android.text.TextWatcher;
import com.example.cuida.data.model.Comprimido; import com.example.cuida.data.model.Comprimido;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.Toast; 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 { public class MedicationDialog extends DialogFragment {
@@ -43,7 +47,8 @@ public class MedicationDialog extends DialogFragment {
private EditText editNotes; private EditText editNotes;
private android.widget.RadioButton radioOral, radioTopical, radioInhalatory; private android.widget.RadioButton radioOral, radioTopical, radioInhalatory;
private android.widget.RadioGroup radioGroupRoute; private android.widget.RadioGroup radioGroupRoute;
private TextView textTime; private ChipGroup chipGroupTimes;
private List<String> selectedTimes = new ArrayList<>();
private Medication medicationToEdit; private Medication medicationToEdit;
private OnMedicationSaveListener listener; private OnMedicationSaveListener listener;
private OnMedicationDeleteListener deleteListener; private OnMedicationDeleteListener deleteListener;
@@ -78,7 +83,8 @@ public class MedicationDialog extends DialogFragment {
editName = view.findViewById(R.id.edit_med_name); editName = view.findViewById(R.id.edit_med_name);
recyclerResults = view.findViewById(R.id.recycler_search_results); recyclerResults = view.findViewById(R.id.recycler_search_results);
editNotes = view.findViewById(R.id.edit_med_notes); 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); radioGroupRoute = view.findViewById(R.id.radio_group_route);
radioOral = view.findViewById(R.id.radio_oral); radioOral = view.findViewById(R.id.radio_oral);
@@ -124,12 +130,18 @@ public class MedicationDialog extends DialogFragment {
radioInhalatory = view.findViewById(R.id.radio_inhalatory); radioInhalatory = view.findViewById(R.id.radio_inhalatory);
// Set up TimePicker // Set up TimePicker
textTime.setOnClickListener(v -> showTimePicker()); btnAddTime.setOnClickListener(v -> showTimePicker());
if (medicationToEdit != null) { if (medicationToEdit != null) {
editName.setText(medicationToEdit.name); editName.setText(medicationToEdit.name);
editNotes.setText(medicationToEdit.notes); 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; String dosage = medicationToEdit.dosage;
if (dosage != null) { if (dosage != null) {
@@ -146,14 +158,23 @@ public class MedicationDialog extends DialogFragment {
builder.setTitle("Adicionar Medicamento"); builder.setTitle("Adicionar Medicamento");
// Default time to current time // Default time to current time
Calendar cal = Calendar.getInstance(); 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) builder.setView(view)
.setPositiveButton("Guardar", (dialog, id) -> { .setPositiveButton("Guardar", (dialog, id) -> {
String name = editName.getText().toString(); String name = editName.getText().toString();
String notes = editNotes.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(); int selectedId = radioGroupRoute.getCheckedRadioButtonId();
String dosage = "Via não especificada"; String dosage = "Via não especificada";
@@ -227,25 +248,32 @@ public class MedicationDialog extends DialogFragment {
int hour = cal.get(Calendar.HOUR_OF_DAY); int hour = cal.get(Calendar.HOUR_OF_DAY);
int minute = cal.get(Calendar.MINUTE); 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(), 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); hour, minute, true);
timePickerDialog.show(); timePickerDialog.show();
} }
private void updateTimeLabel(int hourOfDay, int minute) { private void refreshTimeChips() {
String time = String.format(Locale.getDefault(), "%02d:%02d", hourOfDay, minute); if (chipGroupTimes == null) return;
textTime.setText(time); 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() { private void fetchAllMedsOnce() {

View File

@@ -58,14 +58,29 @@ public class MedicationFragment extends Fragment {
MedicationDialog dialog = new MedicationDialog(); MedicationDialog dialog = new MedicationDialog();
dialog.setMedicationToEdit(medication); dialog.setMedicationToEdit(medication);
dialog.setListener(medicationToSave -> { 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) { if (medication == null) {
medicationViewModel.insert(medicationToSave); medicationViewModel.insert(medicationToSave);
} else { } else {
medicationViewModel.update(medicationToSave); medicationViewModel.update(medicationToSave);
} }
String[] times = medicationToSave.time.split(",\\s*");
for (String t : times) {
if (t.isEmpty()) continue;
try { try {
String[] timeParts = medicationToSave.time.split(":"); String[] timeParts = t.split(":");
int hour = Integer.parseInt(timeParts[0]); int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]); int minute = Integer.parseInt(timeParts[1]);
@@ -80,15 +95,19 @@ public class MedicationFragment extends Fragment {
String title = "Hora do Medicamento"; String title = "Hora do Medicamento";
String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")"; String msg = "É hora de tomar: " + medicationToSave.name + " (" + medicationToSave.dosage + ")";
int alarmId = (medicationToSave.name + t).hashCode();
com.example.cuida.utils.AlarmScheduler.scheduleAlarm( com.example.cuida.utils.AlarmScheduler.scheduleAlarm(
requireContext(), requireContext(),
calendar.getTimeInMillis(), calendar.getTimeInMillis(),
title, title,
msg, msg,
medicationToSave.name.hashCode()); alarmId);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
}
dialog.dismiss(); dialog.dismiss();
}); });
@@ -96,14 +115,19 @@ public class MedicationFragment extends Fragment {
dialog.setDeleteListener(medicationToDelete -> { dialog.setDeleteListener(medicationToDelete -> {
medicationViewModel.delete(medicationToDelete); medicationViewModel.delete(medicationToDelete);
// Cancel alarm if configured // 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 { try {
com.example.cuida.utils.AlarmScheduler.cancelAlarm( int alarmId = (medicationToDelete.name + t).hashCode();
requireContext(), com.example.cuida.utils.AlarmScheduler.cancelAlarm(requireContext(), alarmId);
medicationToDelete.name.hashCode());
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
}
}
}); });
dialog.show(getParentFragmentManager(), "MedicationDialog"); dialog.show(getParentFragmentManager(), "MedicationDialog");

View File

@@ -11,6 +11,18 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Toast; 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.annotation.NonNull;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
@@ -47,6 +59,8 @@ public class ProfileFragment extends Fragment {
getActivity().finish(); getActivity().finish();
}); });
binding.buttonExportReport.setOnClickListener(v -> exportMonthlyReport());
return binding.getRoot(); return binding.getRoot();
} }
@@ -285,9 +299,119 @@ public class ProfileFragment extends Fragment {
dialog.show(); dialog.show();
} }
@Override private void exportMonthlyReport() {
public void onDestroyView() { if (currentUser == null || auth.getCurrentUser() == null) return;
super.onDestroyView(); String userId = auth.getCurrentUser().getUid();
binding = null;
Toast.makeText(getContext(), "A gerar relatório...", Toast.LENGTH_SHORT).show();
db.collection("medicamentos").whereEqualTo("userId", userId).get()
.addOnSuccessListener(medSnapshots -> {
List<Medication> 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<Appointment> appts = new ArrayList<>();
for (com.google.firebase.firestore.DocumentSnapshot doc : apptSnapshots) {
appts.add(doc.toObject(Appointment.class));
}
generateAndSharePDF(meds, appts);
});
});
}
private void generateAndSharePDF(List<Medication> meds, List<Appointment> 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"));
} }
} }

View File

@@ -199,20 +199,25 @@ public class ScheduleViewModel extends AndroidViewModel {
int hour = Integer.parseInt(timeParts[0]); int hour = Integer.parseInt(timeParts[0]);
int minute = Integer.parseInt(timeParts[1]); int minute = Integer.parseInt(timeParts[1]);
Calendar calendar = Calendar.getInstance(); Calendar baseCal = Calendar.getInstance();
calendar.set(year, month, day, hour, minute, 0); baseCal.set(year, month, day, hour, minute, 0);
// 1 hour before
calendar.add(Calendar.HOUR_OF_DAY, -1);
if (calendar.getTimeInMillis() > System.currentTimeMillis()) { // Schedule 24 hours before
String title = "Lembrete de Consulta"; Calendar cal24h = (Calendar) baseCal.clone();
String msg = "A sua consulta é daqui a 1 hora (" + time + ")."; cal24h.add(Calendar.DAY_OF_YEAR, -1);
AlarmScheduler.scheduleAlarm( if (cal24h.getTimeInMillis() > System.currentTimeMillis()) {
getApplication(), AlarmScheduler.scheduleAlarm(getApplication(), cal24h.getTimeInMillis(),
calendar.getTimeInMillis(), "Lembrete de Consulta", "A sua consulta é amanhã às " + time,
title, (date + time + "24h").hashCode());
msg, }
(date + time).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) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();

View File

@@ -13,6 +13,10 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.example.cuida.databinding.FragmentSns24Binding; 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 { public class Sns24Fragment extends Fragment {
@@ -85,6 +89,9 @@ public class Sns24Fragment extends Fragment {
startActivity(mapIntent); 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<String, Object> 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 @Override
public void onDestroyView() { public void onDestroyView() {
super.onDestroyView(); super.onDestroyView();

View File

@@ -86,7 +86,18 @@
android:text="@string/login_button" android:text="@string/login_button"
android:textSize="16sp" android:textSize="16sp"
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="16dp"/> android:layout_marginBottom="8dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/biometric_button"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Login com Biometria"
android:icon="@android:drawable/ic_dialog_info"
android:layout_marginBottom="16dp"
android:visibility="gone"/>
<TextView <TextView
android:id="@+id/forgot_password_link" android:id="@+id/forgot_password_link"

View File

@@ -29,36 +29,38 @@
android:inputType="textCapWords" /> android:inputType="textCapWords" />
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<TextView <TextView
android:text="Horário" android:text="Horários"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_time"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:textSize="14sp" android:text="Adicionar"
android:layout_marginBottom="4dp"/> android:padding="0dp"
android:minWidth="0dp"
<!-- O resto do conteúdo do diálogo continua aqui em baixo --> android:minHeight="0dp"/>
</LinearLayout> </LinearLayout>
<androidx.recyclerview.widget.RecyclerView <com.google.android.material.chip.ChipGroup
android:id="@+id/recycler_search_results" android:id="@+id/chip_group_times"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_marginTop="60dp"
android:visibility="gone"
android:elevation="8dp"
android:background="@drawable/bg_search_results" />
</FrameLayout>
<TextView
android:id="@+id/text_med_time"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="08:00" android:layout_marginBottom="16dp"
android:textSize="18sp" android:padding="4dp"/>
android:padding="12dp"
android:background="#E0E0E0"
android:gravity="center"
android:layout_marginBottom="16dp"/>
<TextView <TextView
android:text="Via de Administração" android:text="Via de Administração"

View File

@@ -115,6 +115,17 @@
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
android:backgroundTint="@color/secondary_color"/> android:backgroundTint="@color/secondary_color"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/button_export_report"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Exportar Relatório Mensal"
android:layout_marginBottom="16dp"
app:strokeColor="@color/secondary_color"
android:textColor="@color/secondary_color"/>
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/button_logout" android:id="@+id/button_logout"
style="@style/Widget.MaterialComponents.Button.OutlinedButton" style="@style/Widget.MaterialComponents.Button.OutlinedButton"

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="my_cache" path="." />
<external-path name="my_external" path="." />
</paths>

View File

@@ -646,7 +646,7 @@ code + .copy-button {
<script type="text/javascript"> <script type="text/javascript">
function configurationCacheProblems() { return ( function configurationCacheProblems() { return (
// begin-report-data // begin-report-data
{"diagnostics":[{"locations":[{"path":"/Users/230405/Desktop/papcuida/app/build.gradle","line":7}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.3.1/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('namespace = <value>') instead."}]]},{"locations":[{"path":"/Users/230405/Desktop/papcuida/app/build.gradle","line":34}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.3.1/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('viewBinding = <value>') instead."}]]}],"problemsReport":{"totalProblemCount":2,"buildName":"Cuida","requestedTasks":":app:assembleDebug","documentationLink":"https://docs.gradle.org/9.3.1/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}} {"diagnostics":[{"locations":[{"path":"/Users/230405/Desktop/papcuida/app/build.gradle","line":7}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.3.1/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('namespace = <value>') instead."}]]},{"locations":[{"path":"/Users/230405/Desktop/papcuida/app/build.gradle","line":34}],"problem":[{"text":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"severity":"WARNING","problemDetails":[{"text":"This is scheduled to be removed in Gradle 10."}],"contextualLabel":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated.","documentationLink":"https://docs.gradle.org/9.3.1/userguide/upgrading_version_8.html#groovy_space_assignment_syntax","problemId":[{"name":"deprecation","displayName":"Deprecation"},{"name":"properties-should-be-assigned-using-the-propname-value-syntax-setting-a-property-via-the-gradle-generated-propname-value-or-propname-value-syntax-in-groovy-dsl","displayName":"Properties should be assigned using the 'propName = value' syntax. Setting a property via the Gradle-generated 'propName value' or 'propName(value)' syntax in Groovy DSL has been deprecated."}],"solutions":[[{"text":"Use assignment ('viewBinding = <value>') instead."}]]}],"problemsReport":{"totalProblemCount":2,"buildName":"Cuida","requestedTasks":"","documentationLink":"https://docs.gradle.org/9.3.1/userguide/reporting_problems.html","documentationLinkCaption":"Problem report","summaries":[]}}
// end-report-data // end-report-data
);} );}
</script> </script>

View File

@@ -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;
}
}

View File

@@ -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<MedicationAdapter.MedicationViewHolder> {
private List<Medication> 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<Medication> 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));
}
});
}
}
}

View File

@@ -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<Comprimido> searchResults = new ArrayList<>();
private List<Comprimido> 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<String> 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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<List<Medication>> allMedications = new MutableLiveData<>(new ArrayList<>());
private final MutableLiveData<Medication> 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<Medication> 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<List<Medication>> getAllMedications() {
return allMedications;
}
public LiveData<Medication> 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));
}
}

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/layout_med_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:hint="Nome do Medicamento"
android:layout_marginBottom="8dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_med_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapWords" />
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="4dp">
<TextView
android:text="Horários"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_time"
style="@style/Widget.MaterialComponents.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Adicionar"
android:padding="0dp"
android:minWidth="0dp"
android:minHeight="0dp"/>
</LinearLayout>
<com.google.android.material.chip.ChipGroup
android:id="@+id/chip_group_times"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:padding="4dp"/>
<TextView
android:text="Via de Administração"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginBottom="4dp"/>
<RadioGroup
android:id="@+id/radio_group_route"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp">
<RadioButton
android:id="@+id/radio_oral"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Oral (Pela boca)" />
<RadioButton
android:id="@+id/radio_topical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Tópica (Na pele)" />
<RadioButton
android:id="@+id/radio_inhalatory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Via Inalatória (Pelo nariz/boca)" />
</RadioGroup>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Notas (Opcional)"
android:layout_marginBottom="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/edit_med_notes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:minLines="2"/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -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+*

View File

@@ -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<String> 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+*

View File

@@ -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.*

View File

@@ -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+*

View File

@@ -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.*

View File

@@ -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+*

View File

@@ -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)*

View File

@@ -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)*