Correção de bugs

This commit is contained in:
2026-03-18 10:37:27 +00:00
parent 347b916c53
commit 53c5839750
77 changed files with 4921 additions and 1166 deletions

View File

@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-04-28T16:08:13.937232Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=4LCE8PNZJREYPVNR" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>

View File

@@ -32,6 +32,12 @@ android {
}
dependencies {
implementation("androidx.biometric:biometric:1.2.0-alpha05")
implementation("com.airbnb.android:lottie:6.3.0")
implementation("com.github.bumptech.glide:glide:4.15.1")
annotationProcessor("com.github.bumptech.glide:compiler:4.15.1")
implementation("com.google.ai.client.generativeai:generativeai:0.9.0")
implementation("com.google.guava:guava:31.1-android")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.13.0")
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
@@ -40,6 +46,7 @@ dependencies {
implementation("com.google.code.gson:gson:2.10.1")
implementation(libs.activity)
implementation(libs.constraintlayout)
implementation(libs.generativeai)
testImplementation(libs.junit)
androidTestImplementation(libs.ext.junit)
androidTestImplementation(libs.espresso.core)

View File

@@ -4,6 +4,8 @@
package="com.example.finzora">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<application
android:allowBackup="true"
@@ -15,31 +17,49 @@
android:supportsRtl="true"
android:theme="@style/Theme.Finzora"
tools:targetApi="31">
<activity
android:name=".LoginActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize"> <intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="finzora"
android:host="confirmado" />
</intent-filter>
</activity>
<activity android:name=".MainActivity" />
<activity android:name=".RegisterActivity" />
<activity
android:name=".DefinicoesActivity"
android:exported="false" />
<activity
android:name=".EditarPerfilActivity"
android:exported="false" />
<activity android:name=".DefinicoesActivity" android:exported="false" />
<activity android:name=".EditarPerfilActivity" android:exported="false" />
<activity android:name=".RecuperarPasswordActivity" android:exported="false" />
<activity android:name=".OnboardingActivity" android:theme="@style/Theme.AppCompat.NoActionBar"/>
<activity android:name=".AdicionarTransacaoActivity" />
<activity android:name=".ProfileActivity" />
</application>
<activity android:name=".LockActivity" android:exported="false" />
<activity
android:name=".NovaPasswordActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="finzora"
android:host="recuperar" />
</intent-filter>
</activity>
</application>
</manifest>

View File

@@ -1,28 +1,30 @@
package com.example.finzora;
import android.app.AlertDialog;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.ArrayAdapter;
import android.util.TypedValue;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.Spinner;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import androidx.cardview.widget.CardView;
public class AdicionarTransacaoActivity extends AppCompatActivity {
// Declaração dos componentes do ecrã (Sem os RadioButtons!)
private EditText editValor;
private Spinner spinnerCategoria;
private EditText editValor, editDescricao;
private TextView txtCategoria;
private Button btnGuardar;
private ImageView btnVoltar;
private String categoriaSelecionada = "";
private final String[] categorias = {"Alimentação", "Contas", "Transportes", "Compras", "Lazer", "Educação", "Saúde", "Salário", "Mesada", "Prémios", "Outros"};
private String idTransacaoParaEditar = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -31,147 +33,160 @@ public class AdicionarTransacaoActivity extends AppCompatActivity {
inicializarComponentes();
// --- ENCHER O SPINNER COM AS CATEGORIAS ---
String[] categorias = {"Alimentação", "Transportes", "Lazer", "Educação", "Saúde", "Salário", "Mesada", "Prémios", "Outros"};
Intent intent = getIntent();
if (intent.hasExtra("transacao_id")) {
idTransacaoParaEditar = intent.getStringExtra("transacao_id");
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
R.layout.item_dropdown,
categorias
);
adapter.setDropDownViewResource(R.layout.item_dropdown);
spinnerCategoria.setAdapter(adapter);
editValor.setText(String.valueOf(intent.getDoubleExtra("valor", 0.0)));
editDescricao.setText(intent.getStringExtra("descricao"));
categoriaSelecionada = intent.getStringExtra("categoria");
txtCategoria.setText(categoriaSelecionada);
// Botão para voltar para trás
if (btnVoltar != null) {
btnVoltar.setOnClickListener(v -> finish());
btnGuardar.setText("ATUALIZAR TRANSAÇÃO");
}
// AGORA O BOTÃO GUARDAR CHAMA O POP-UP!
txtCategoria.setOnClickListener(v -> mostrarDialogCategorias());
btnVoltar.setOnClickListener(v -> finish());
btnGuardar.setOnClickListener(v -> perguntarTipoTransacao());
}
// ====================================================================
// A MAGIA DO POP-UP
// ====================================================================
private void perguntarTipoTransacao() {
String valorStr = editValor.getText().toString().trim();
if (TextUtils.isEmpty(valorStr)) { editValor.setError("Define o valor"); return; }
if (categoriaSelecionada.isEmpty()) { Toast.makeText(this, "Escolhe a categoria!", Toast.LENGTH_SHORT).show(); return; }
// Primeiro, verifica se ele preencheu o valor antes de perguntar o tipo
if (TextUtils.isEmpty(valorStr)) {
editValor.setError("Preenche o valor primeiro!");
editValor.requestFocus();
return;
if (idTransacaoParaEditar != null) {
int tipoOriginal = getIntent().getIntExtra("tipo", 2);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Confirmar Alteração");
builder.setMessage("Desejas guardar estas alterações?");
builder.setPositiveButton("Confirmar", (dialog, which) -> salvarOuAtualizar(tipoOriginal));
builder.setNegativeButton("Cancelar", null);
builder.show();
} else {
View view = getLayoutInflater().inflate(R.layout.dialog_tipo_transacao, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(view);
AlertDialog dialog = builder.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(0));
}
CardView btnReceita = view.findViewById(R.id.btnTipoReceita);
CardView btnDespesa = view.findViewById(R.id.btnTipoDespesa);
TextView btnCancelar = view.findViewById(R.id.btnCancelarTipo);
btnReceita.setOnClickListener(v -> {
dialog.dismiss();
salvarOuAtualizar(1);
});
btnDespesa.setOnClickListener(v -> {
dialog.dismiss();
salvarOuAtualizar(2);
});
btnCancelar.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
// Criar o Pop-up de escolha
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Tipo de Transação");
builder.setMessage("Esta transação é uma Receita (entrada) ou uma Despesa (saída)?");
// Botão de Receita (Passa o tipo 1 para a base de dados)
builder.setPositiveButton("Receita 📈", (dialog, which) -> {
salvarTransacaoNaBaseDeDados(1);
});
// Botão de Despesa (Passa o tipo 2 para a base de dados)
builder.setNegativeButton("Despesa 📉", (dialog, which) -> {
salvarTransacaoNaBaseDeDados(2);
});
// Botão Cancelar (Caso o utilizador queira fechar e alterar algo)
builder.setNeutralButton("Cancelar", (dialog, which) -> {
dialog.dismiss();
});
// Mostrar o Pop-up no ecrã
AlertDialog dialog = builder.create();
dialog.show();
}
// ====================================================================
// GUARDAR NA BASE DE DADOS APÓS A ESCOLHA
// ====================================================================
private void salvarTransacaoNaBaseDeDados(int tipoEscolhido) {
// Primeiro, vamos buscar o "Carimbo" (ID) de quem está a usar a app
android.content.SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
// 🏆 NOVO MENU DE CATEGORIAS (PREMIUM)
// 🏆 NOVO MENU DE CATEGORIAS (PREMIUM)
private void mostrarDialogCategorias() {
View view = getLayoutInflater().inflate(R.layout.dialog_categorias, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(view);
AlertDialog dialog = builder.create();
if (userId == null) {
android.widget.Toast.makeText(this, "Erro: Utilizador não identificado. Faz login novamente.", android.widget.Toast.LENGTH_LONG).show();
return;
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(0));
}
LinearLayout container = view.findViewById(R.id.containerCategorias);
TextView btnCancelar = view.findViewById(R.id.btnCancelarCategoria);
// Gera a lista de categorias visualmente perfeita
for (String cat : categorias) {
TextView tv = new TextView(this);
tv.setText(cat);
tv.setTextSize(16f); // ⚠️ CORRIGIDO AQUI PARA JAVA!
// Rouba a cor certa ao ecrã (Claro/Escuro) para não falhar
tv.setTextColor(txtCategoria.getCurrentTextColor());
// Muito espaço para ser fácil clicar com o dedo
tv.setPadding(32, 40, 32, 40);
// Efeito de onda ao clicar (Ripple Effect nativo)
android.util.TypedValue outValue = new android.util.TypedValue();
getTheme().resolveAttribute(android.R.attr.selectableItemBackground, outValue, true);
tv.setBackgroundResource(outValue.resourceId);
tv.setClickable(true);
tv.setFocusable(true);
tv.setOnClickListener(v -> {
categoriaSelecionada = cat;
txtCategoria.setText(categoriaSelecionada);
dialog.dismiss();
});
container.addView(tv);
}
btnCancelar.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
private void salvarOuAtualizar(int tipoEscolhido) {
android.content.SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
try {
String valorStr = editValor.getText().toString().trim();
valorStr = valorStr.replace(",", "."); // Evitar crashes com vírgulas
double valor = Double.parseDouble(valorStr);
String categoria = spinnerCategoria.getSelectedItem().toString();
double valor = Double.parseDouble(editValor.getText().toString().replace(",", "."));
String descricao = editDescricao.getText().toString().trim();
String dataStr = new java.text.SimpleDateFormat("dd/MM/yyyy", java.util.Locale.getDefault()).format(new java.util.Date());
// Mudar o texto do botão para o utilizador perceber que está a gravar
btnGuardar.setEnabled(false);
btnGuardar.setText("A GRAVAR NAS NUVENS...");
btnGuardar.setText("A GUARDAR...");
// Preparar o cliente de Internet
okhttp3.OkHttpClient client = new okhttp3.OkHttpClient();
// Construir o JSON que vai viajar até ao Supabase
// AQUI ESTÁ A CORREÇÃO: a coluna chama-se "data" para bater certo com o teu SQL!
String json = "{"
+ "\"user_id\":\"" + userId + "\", "
+ "\"valor\":" + valor + ", "
+ "\"categoria\":\"" + categoria + "\", "
+ "\"tipo\":" + tipoEscolhido + ", "
+ "\"data\":\"" + dataStr + "\""
+ "}";
String json = "{\"user_id\":\"" + userId + "\", \"valor\":" + valor + ", \"categoria\":\"" + categoriaSelecionada + "\", \"tipo\":" + tipoEscolhido + ", \"descricao\":\"" + descricao + "\", \"data\":\"" + dataStr + "\"}";
okhttp3.RequestBody body = okhttp3.RequestBody.create(json, okhttp3.MediaType.parse("application/json; charset=utf-8"));
// Fazer o pedido POST para a tabela "transacoes" do Supabase
String url = SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes";
String metodo = "POST";
if (idTransacaoParaEditar != null) {
url += "?id=eq." + idTransacaoParaEditar;
metodo = "PATCH";
}
okhttp3.Request request = new okhttp3.Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes")
.url(url)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.addHeader("Content-Type", "application/json")
.addHeader("Prefer", "return=minimal") // Diz ao Supabase para não devolver os dados de volta
.post(body)
.method(metodo, body)
.build();
// Executar o envio em segundo plano para não bloquear o ecrã
client.newCall(request).enqueue(new okhttp3.Callback() {
@Override
public void onFailure(@androidx.annotation.NonNull okhttp3.Call call, @androidx.annotation.NonNull java.io.IOException e) {
runOnUiThread(() -> {
btnGuardar.setEnabled(true);
btnGuardar.setText("Guardar Transação");
android.widget.Toast.makeText(AdicionarTransacaoActivity.this, "Erro de net! A transação não foi guardada.", android.widget.Toast.LENGTH_SHORT).show();
});
@Override public void onFailure(okhttp3.Call call, java.io.IOException e) {
runOnUiThread(() -> { btnGuardar.setEnabled(true); btnGuardar.setText("GUARDAR"); });
}
@Override
public void onResponse(@androidx.annotation.NonNull okhttp3.Call call, @androidx.annotation.NonNull okhttp3.Response response) throws java.io.IOException {
runOnUiThread(() -> {
if (response.isSuccessful()) {
android.widget.Toast.makeText(AdicionarTransacaoActivity.this, "Transação guardada com sucesso! 🎉", android.widget.Toast.LENGTH_SHORT).show();
finish(); // Volta ao ecrã principal
} else {
btnGuardar.setEnabled(true);
btnGuardar.setText("Guardar Transação");
android.widget.Toast.makeText(AdicionarTransacaoActivity.this, "Erro no Supabase. Tenta novamente.", android.widget.Toast.LENGTH_LONG).show();
}
});
@Override public void onResponse(okhttp3.Call call, okhttp3.Response response) {
runOnUiThread(() -> { if (response.isSuccessful()) finish(); });
}
});
} catch (Exception e) {
android.widget.Toast.makeText(this, "ERRO LOCAL: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show();
e.printStackTrace();
}
} catch (Exception e) { Toast.makeText(this, "Erro: " + e.getMessage(), Toast.LENGTH_SHORT).show(); }
}
private void inicializarComponentes() {
editValor = findViewById(R.id.editValor);
spinnerCategoria = findViewById(R.id.spinnerCategoria);
txtCategoria = findViewById(R.id.txtCategoriaTransacao);
editDescricao = findViewById(R.id.editDescricaoTransacao);
btnGuardar = findViewById(R.id.btnGuardar);
btnVoltar = findViewById(R.id.btnVoltar);
}

View File

@@ -56,12 +56,13 @@ public class DBHelper extends SQLiteOpenHelper {
if (cursor.moveToFirst()) {
do {
// ⚠️ A JOGADA MÁGICA: Transformar o int antigo numa String!
lista.add(new Transacao(
cursor.getInt(0), // id
cursor.getFloat(1), // valor
cursor.getString(2),// categoria
cursor.getInt(3), // tipo
cursor.getString(4) // data
String.valueOf(cursor.getInt(0)), // id convertido para texto
cursor.getFloat(1), // valor
cursor.getString(2), // categoria
cursor.getInt(3), // tipo
cursor.getString(4) // data
));
} while (cursor.moveToNext());
}

View File

@@ -1,10 +1,13 @@
package com.example.finzora;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.ActivityNotFoundException;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.view.ViewGroup;
import android.widget.Button;
@@ -12,9 +15,20 @@ import android.widget.Switch;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DefinicoesActivity extends AppCompatActivity {
@Override
@@ -22,19 +36,19 @@ public class DefinicoesActivity extends AppCompatActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_definicoes);
// Ligar todos os botões do ecrã ao nosso código Java
TextView btnVoltarDefinicoes = findViewById(R.id.btnVoltarDefinicoes);
TextView btnEditarPerfil = findViewById(R.id.btnEditarPerfil);
Switch switchModoEscuro = findViewById(R.id.switchModoEscuro);
Switch switchNotificacoes = findViewById(R.id.switchNotificacoes);
Switch switchBiometria = findViewById(R.id.switchBiometria);
TextView btnSuporte = findViewById(R.id.btnSuporte);
Button btnTerminarSessao = findViewById(R.id.btnTerminarSessao);
Button btnEliminarConta = findViewById(R.id.btnEliminarConta);
// --- 0. BOTÃO DE VOLTAR ---
btnVoltarDefinicoes.setOnClickListener(v -> finish());
// --- 1. EDITAR PERFIL ---
// Agora já abre o novo ecrã de Edição de Perfil!
btnEditarPerfil.setOnClickListener(v -> {
startActivity(new Intent(DefinicoesActivity.this, EditarPerfilActivity.class));
});
@@ -56,7 +70,22 @@ public class DefinicoesActivity extends AppCompatActivity {
}
});
// --- 3. NOTIFICAÇÕES ---
// --- 3. MAGIA DA BIOMETRIA (SEGURANÇA) ---
SharedPreferences prefsUser = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
boolean usarBiometria = prefsUser.getBoolean("usar_biometria", false);
switchBiometria.setChecked(usarBiometria);
switchBiometria.setOnCheckedChangeListener((buttonView, isChecked) -> {
prefsUser.edit().putBoolean("usar_biometria", isChecked).apply();
if (isChecked) {
Toast.makeText(this, "A app vai pedir o teu dedo ao abrir! 🔒", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "Bloqueio Desativado 🔓", Toast.LENGTH_SHORT).show();
}
});
// --- 4. NOTIFICAÇÕES ---
switchNotificacoes.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (isChecked) {
Toast.makeText(this, "Notificações Ligadas 🔔", Toast.LENGTH_SHORT).show();
@@ -65,11 +94,111 @@ public class DefinicoesActivity extends AppCompatActivity {
}
});
// --- 4. CENTRO DE SUPORTE (POP-UP) ---
// --- 5. CENTRO DE SUPORTE (POP-UP) ---
btnSuporte.setOnClickListener(v -> mostrarDialogSuporte());
// --- 5. TERMINAR SESSÃO ---
// --- 6. TERMINAR SESSÃO ---
btnTerminarSessao.setOnClickListener(v -> terminarSessao());
// --- 7. APAGAR CONTA ---
btnEliminarConta.setOnClickListener(v -> mostrarAvisoEliminar());
}
private void mostrarAvisoEliminar() {
new AlertDialog.Builder(this)
.setTitle("⚠️ Ação Irreversível")
.setMessage("Tens a certeza? Todos os teus orçamentos e transações serão apagados para sempre da nossa nuvem, de acordo com as normas de proteção de dados (RGPD).")
.setPositiveButton("Sim, Apagar Tudo", (dialog, which) -> executarLimpezaDeDados())
.setNegativeButton("Cancelar", null)
.show();
}
private void executarLimpezaDeDados() {
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) {
Toast.makeText(this, "ERRO: Não encontrei o teu ID no telemóvel!", Toast.LENGTH_LONG).show();
return;
}
OkHttpClient client = new OkHttpClient();
Toast.makeText(this, "A apagar transações... 🗑️", Toast.LENGTH_SHORT).show();
Request reqTransacoes = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete()
.build();
client.newCall(reqTransacoes).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(() -> Toast.makeText(DefinicoesActivity.this, "Erro de net a apagar transações!", Toast.LENGTH_SHORT).show());
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
apagarOrcamentos(userId);
}
});
}
private void apagarOrcamentos(String userId) {
OkHttpClient client = new OkHttpClient();
Request reqOrcamentos = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete()
.build();
client.newCall(reqOrcamentos).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) { }
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
String jsonParams = "{\"id_alvo\":\"" + userId + "\"}";
RequestBody body = RequestBody.create(jsonParams, MediaType.parse("application/json; charset=utf-8"));
Request reqApagarConta = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/rpc/apagar_conta_finzora")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.post(body)
.build();
client.newCall(reqApagarConta).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(() -> Toast.makeText(DefinicoesActivity.this, "Erro a apagar a conta final!", Toast.LENGTH_LONG).show());
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
final String respostaSupabase = response.body() != null ? response.body().string() : "";
runOnUiThread(() -> {
if (response.isSuccessful()) {
getSharedPreferences("DadosUtilizador", MODE_PRIVATE).edit().clear().apply();
Toast.makeText(DefinicoesActivity.this, "Conta e dados eliminados para sempre. 🧹", Toast.LENGTH_LONG).show();
Intent intent = new Intent(DefinicoesActivity.this, LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();
} else {
Toast.makeText(DefinicoesActivity.this, "Erro: " + respostaSupabase, Toast.LENGTH_LONG).show();
}
});
}
});
}
});
}
private void mostrarDialogSuporte() {
@@ -78,13 +207,11 @@ public class DefinicoesActivity extends AppCompatActivity {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// O Botão de Fechar o Pop-up principal
dialog.findViewById(R.id.btnFecharSuporte).setOnClickListener(v -> dialog.dismiss());
// Ligar os cliques nos Cartões
dialog.findViewById(R.id.cardFAQ).setOnClickListener(v -> {
dialog.dismiss(); // Fecha este menu
mostrarDialogFAQ(); // Abre o FAQ
dialog.dismiss();
mostrarDialogFAQ();
});
dialog.findViewById(R.id.cardTutorial).setOnClickListener(v -> {
@@ -92,30 +219,62 @@ public class DefinicoesActivity extends AppCompatActivity {
mostrarDialogTutorial();
});
// ✉️ CONTACTAR SUPORTE: Abre a app do Gmail
dialog.findViewById(R.id.cardMensagem).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogContactar();
enviarEmailProfissional();
});
// 📞 CONTACTOS DIRETOS: Abre o nosso Novo Design Premium!
dialog.findViewById(R.id.cardContactos).setOnClickListener(v -> {
Toast.makeText(this, "Email: suporte@finzora.pt\nTel: +351 800 123 456", Toast.LENGTH_LONG).show();
dialog.dismiss();
mostrarDialogContactosInfo();
});
dialog.show();
}
// 🏆 A NOVA FUNÇÃO QUE CHAMA O DESIGN PREMIUM
private void mostrarDialogContactosInfo() {
Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_contactos);
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
}
// Quando clica em Voltar, fechamos este e abrimos o menu de Suporte outra vez!
dialog.findViewById(R.id.btnFecharContactos).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogSuporte();
});
dialog.show();
}
private void enviarEmailProfissional() {
Intent emailIntent = new Intent(Intent.ACTION_SENDTO);
emailIntent.setData(Uri.parse("mailto:suporte@finzora.pt"));
emailIntent.putExtra(Intent.EXTRA_SUBJECT, "Suporte Finzora - Ajuda");
try {
startActivity(Intent.createChooser(emailIntent, "Abrir com..."));
} catch (ActivityNotFoundException ex) {
Toast.makeText(this, "Não tens nenhuma app de e-mail instalada no telemóvel!", Toast.LENGTH_LONG).show();
}
}
private void mostrarDialogFAQ() {
Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_faq);
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Setinha de voltar (Fecha o FAQ e volta a abrir o menu principal)
dialog.findViewById(R.id.btnVoltarFAQ).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogSuporte();
});
dialog.show();
}
@@ -125,39 +284,10 @@ public class DefinicoesActivity extends AppCompatActivity {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Setinha de voltar
dialog.findViewById(R.id.btnVoltarTutorial).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogSuporte();
});
dialog.show();
}
private void mostrarDialogContactar() {
Dialog dialog = new Dialog(this);
dialog.setContentView(R.layout.dialog_contactar);
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
// Setinha de voltar
dialog.findViewById(R.id.btnVoltarContactar).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogSuporte();
});
// Botões do formulário
dialog.findViewById(R.id.btnCancelarContacto).setOnClickListener(v -> {
dialog.dismiss();
mostrarDialogSuporte();
});
dialog.findViewById(R.id.btnEnviarMensagem).setOnClickListener(v -> {
Toast.makeText(this, "Mensagem enviada com sucesso!", Toast.LENGTH_SHORT).show();
dialog.dismiss();
mostrarDialogSuporte();
});
dialog.show();
}

View File

@@ -1,41 +1,66 @@
package com.example.finzora;
import android.content.res.ColorStateList;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.GradientDrawable;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class DicasFragment extends Fragment {
// Componentes de Saúde Financeira
private TextView tvTaxaPoupanca, tvDicasReceitas, tvDicasDespesas;
private ProgressBar progressPoupanca;
// Componentes das Dicas
private TextView tvTituloDica1, tvDescDica1;
private TextView tvTituloDica2, tvDescDica2;
// Distribuição de Gastos
private TextView tvTituloDica1, tvDescDica1, tvTituloDica2, tvDescDica2, tvTituloDica3, tvDescDica3;
private LinearLayout layoutDistribuicao;
private DBHelper dbHelper;
private View layoutConteudoDicas;
private View layoutEstadoVazioDicas;
private TextView tvRespostaAI;
private EditText editPerguntaAI;
private ImageButton btnEnviarAI;
private ProgressBar pbCarregandoAI;
private int corFundoCartao;
private int corTextoDinamico;
private String contextoFinanceiroParaAI = "O utilizador ainda não tem dados financeiros registados.";
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_dicas, container, false);
// Ligar os componentes
tvTaxaPoupanca = view.findViewById(R.id.tvTaxaPoupanca);
tvDicasReceitas = view.findViewById(R.id.tvDicasReceitas);
tvDicasDespesas = view.findViewById(R.id.tvDicasDespesas);
@@ -45,119 +70,386 @@ public class DicasFragment extends Fragment {
tvDescDica1 = view.findViewById(R.id.tvDescDica1);
tvTituloDica2 = view.findViewById(R.id.tvTituloDica2);
tvDescDica2 = view.findViewById(R.id.tvDescDica2);
tvTituloDica3 = view.findViewById(R.id.tvTituloDica3);
tvDescDica3 = view.findViewById(R.id.tvDescDica3);
layoutDistribuicao = view.findViewById(R.id.layoutDistribuicao);
layoutConteudoDicas = view.findViewById(R.id.layoutConteudoDicas);
layoutEstadoVazioDicas = view.findViewById(R.id.layoutEstadoVazioDicas);
dbHelper = new DBHelper(getActivity());
tvRespostaAI = view.findViewById(R.id.tvRespostaAI);
editPerguntaAI = view.findViewById(R.id.editPerguntaAI);
btnEnviarAI = view.findViewById(R.id.btnEnviarAI);
pbCarregandoAI = view.findViewById(R.id.pbCarregandoAI);
if (getContext() != null) {
corFundoCartao = ContextCompat.getColor(getContext(), R.color.fundo_cartao);
corTextoDinamico = ContextCompat.getColor(getContext(), R.color.texto_principal);
}
btnEnviarAI.setOnClickListener(v -> perguntarAoNovoCoach());
return view;
}
private void perguntarAoNovoCoach() {
String pergunta = editPerguntaAI.getText().toString().trim();
if (pergunta.isEmpty()) return;
pbCarregandoAI.setVisibility(View.VISIBLE);
tvRespostaAI.setText("A analisar os dados de forma inteligente...");
editPerguntaAI.setText("");
OkHttpClient client = new OkHttpClient();
JSONObject jsonBody = new JSONObject();
try {
jsonBody.put("model", "llama-3.1-8b-instant");
JSONArray messages = new JSONArray();
String regrasBase = "És o Assistente de IA da Finzora, um consultor financeiro altamente profissional e analítico. " +
"Usa ESTRITAMENTE o Português de Portugal (PT-PT). Trata o utilizador SEMPRE por 'tu'. " +
"Sê natural, claro, focado em literacia financeira e responde com um máximo de 3 ou 4 parágrafos curtos. ";
JSONObject systemMsg = new JSONObject();
systemMsg.put("role", "system");
systemMsg.put("content", regrasBase + contextoFinanceiroParaAI);
messages.put(systemMsg);
JSONObject userMsg = new JSONObject();
userMsg.put("role", "user");
userMsg.put("content", pergunta);
messages.put(userMsg);
jsonBody.put("messages", messages);
} catch (Exception e) { e.printStackTrace(); }
RequestBody body = RequestBody.create(jsonBody.toString(), MediaType.parse("application/json; charset=utf-8"));
String groqApiKey = "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";
Request request = new Request.Builder()
.url("https://api.groq.com/openai/v1/chat/completions")
.addHeader("Authorization", "Bearer " + groqApiKey)
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
pbCarregandoAI.setVisibility(View.GONE);
tvRespostaAI.setText("Erro de ligação ao serviço de Inteligência Artificial.");
});
}
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
final String respBody = response.body() != null ? response.body().string() : "";
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
pbCarregandoAI.setVisibility(View.GONE);
if (response.isSuccessful()) {
try {
JSONObject jsonObject = new JSONObject(respBody);
String respostaIA = jsonObject.getJSONArray("choices")
.getJSONObject(0).getJSONObject("message").getString("content");
String textoFormatado = respostaIA.replaceAll("\\*\\*(.*?)\\*\\*", "<b>$1</b>");
textoFormatado = textoFormatado.replace("\n", "<br>");
tvRespostaAI.setText(android.text.Html.fromHtml(textoFormatado, android.text.Html.FROM_HTML_MODE_LEGACY));
} catch (Exception e) {
tvRespostaAI.setText("Erro a ler os dados da análise: " + e.getMessage());
}
} else {
tvRespostaAI.setText("O Assistente não está disponível neste momento.");
}
});
}
}
});
}
@Override
public void onResume() {
super.onResume();
analisarFinancas();
analisarFinancasDaNuvem();
}
private void analisarFinancas() {
float receitas = dbHelper.getTotalReceitas();
float despesas = dbHelper.getTotalDespesas();
private void analisarFinancasDaNuvem() {
if (getActivity() == null) return;
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
// 1. Atualizar Textos Iniciais
tvDicasReceitas.setText(String.format("€ %.2f", receitas));
tvDicasDespesas.setText(String.format("€ %.2f", despesas));
OkHttpClient client = new OkHttpClient();
// 2. Calcular Taxa de Poupança
float taxaPoupanca = 0;
if (receitas > 0) {
taxaPoupanca = ((receitas - despesas) / receitas) * 100;
}
Request requestTransacoes = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
// Se gastou mais do que ganhou, a taxa é 0
if (taxaPoupanca < 0) taxaPoupanca = 0;
client.newCall(requestTransacoes).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) { }
tvTaxaPoupanca.setText(String.format("%.1f%%", taxaPoupanca));
progressPoupanca.setProgress((int) taxaPoupanca);
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (!response.isSuccessful()) return;
try {
String body = response.body().string();
JSONArray array = new JSONArray(body);
float rec = 0, desp = 0;
HashMap<String, Float> mapaGastos = new HashMap<>();
// Cores consoante a saúde financeira
if (taxaPoupanca >= 20) {
tvTaxaPoupanca.setTextColor(Color.parseColor("#00E676")); // Verde
progressPoupanca.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E676")));
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
float v = (float) obj.getDouble("valor");
if (obj.getInt("tipo") == 1) rec += v;
else {
desp += v;
String cat = obj.getString("categoria");
mapaGastos.put(cat, mapaGastos.getOrDefault(cat, 0f) + v);
}
}
tvTituloDica1.setText("Excelente Taxa de Poupança! \uD83C\uDF1F");
tvTituloDica1.setTextColor(Color.parseColor("#00E676"));
tvDescDica1.setText("Estás a poupar " + String.format("%.1f", taxaPoupanca) + "% dos teus rendimentos. Continua com este ótimo hábito financeiro!");
} else if (taxaPoupanca > 0) {
tvTaxaPoupanca.setTextColor(Color.parseColor("#FFD600")); // Amarelo
progressPoupanca.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#FFD600")));
buscarOrcamentosECriarCerebro(userId, client, rec, desp, mapaGastos);
tvTituloDica1.setText("Atenção à Poupança \uD83D\uDD0D");
tvTituloDica1.setTextColor(Color.parseColor("#FFD600"));
tvDescDica1.setText("Estás a poupar muito pouco. A meta recomendada é guardar pelo menos 20% do que ganhas.");
} catch (Exception e) { e.printStackTrace(); }
}
});
}
private void buscarOrcamentosECriarCerebro(String userId, OkHttpClient client, float rec, float desp, HashMap<String, Float> mapaGastos) {
Request requestOrcamentos = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(requestOrcamentos).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (!response.isSuccessful()) return;
try {
String body = response.body().string();
JSONArray arrayOrcamentos = new JSONArray(body);
// 🧠 RECONSTRUIR O CÉREBRO DA IA COM TODOS OS DETALHES!
StringBuilder cerebro = new StringBuilder();
cerebro.append("DADOS FINANCEIROS ATUAIS DO UTILIZADOR: ");
cerebro.append("Receitas Totais: ").append(rec).append("€. ");
cerebro.append("Despesas Totais: ").append(desp).append("€. ");
// Passar as categorias onde gastaste dinheiro
if (!mapaGastos.isEmpty()) {
cerebro.append("Gastos por categoria: ");
for (Map.Entry<String, Float> entry : mapaGastos.entrySet()) {
cerebro.append(entry.getKey()).append(" (").append(entry.getValue()).append("€), ");
}
}
if (arrayOrcamentos.length() > 0) {
cerebro.append(". ORÇAMENTOS DEFINIDOS: ");
for (int i = 0; i < arrayOrcamentos.length(); i++) {
JSONObject obj = arrayOrcamentos.getJSONObject(i);
String cat = obj.getString("categoria");
float limite = (float) obj.getDouble("valor_limite");
float gasto = mapaGastos.containsKey(cat) ? mapaGastos.get(cat) : 0f;
cerebro.append("[").append(cat).append(": Limite definido ").append(limite).append("€, Gasto atual ").append(gasto).append("€] ");
}
} else {
cerebro.append(". O utilizador não tem orçamentos definidos de momento. ");
}
// ⚠️ A JOGADA QUE FALTAVA: Guardar a memória na variável que a IA vai ler!
contextoFinanceiroParaAI = cerebro.toString();
if (getActivity() != null) {
getActivity().runOnUiThread(() -> aplicarLogicaDeDicas(rec, desp, mapaGastos, arrayOrcamentos));
}
} catch (Exception e) { e.printStackTrace(); }
}
});
}
private void aplicarLogicaDeDicas(float rec, float desp, HashMap<String, Float> mapa, JSONArray arrayOrcamentos) {
if (rec == 0 && desp == 0) {
layoutConteudoDicas.setVisibility(View.GONE);
layoutEstadoVazioDicas.setVisibility(View.VISIBLE);
return;
} else {
tvTaxaPoupanca.setTextColor(Color.parseColor("#FF1744")); // Vermelho
progressPoupanca.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#FF1744")));
tvTituloDica1.setText("Alerta Vermelho! \uD83D\uDEA8");
tvTituloDica1.setTextColor(Color.parseColor("#FF1744"));
tvDescDica1.setText("Os teus gastos superam ou igualam os teus ganhos. Verifica urgentemente para onde está a ir o teu dinheiro!");
layoutConteudoDicas.setVisibility(View.VISIBLE);
layoutEstadoVazioDicas.setVisibility(View.GONE);
}
// 3. Descobrir a Categoria mais gasta
HashMap<String, Float> gastosPorCategoria = dbHelper.getDespesasPorCategoria();
String piorCategoria = "Nenhuma";
float maiorGasto = 0;
// --- TOPO: RESUMO ---
tvDicasReceitas.setText(String.format("€ %.2f", rec));
tvDicasDespesas.setText(String.format("€ %.2f", desp));
float taxa = (rec > 0) ? ((rec - desp) / rec) * 100 : 0;
if (taxa < 0) taxa = 0;
tvTaxaPoupanca.setText(String.format("%.1f%%", taxa));
progressPoupanca.setProgress((int) taxa);
for (Map.Entry<String, Float> entry : gastosPorCategoria.entrySet()) {
if (entry.getValue() > maiorGasto) {
maiorGasto = entry.getValue();
piorCategoria = entry.getKey();
// --- CARTÃO 1: REGRA 50/30/20 ---
float necessidades = 0; float desejos = 0;
for (Map.Entry<String, Float> entry : mapa.entrySet()) {
String cat = entry.getKey().toLowerCase();
float val = entry.getValue();
if (cat.contains("conta") || cat.contains("alimen") || cat.contains("saúd") || cat.contains("educa") || cat.contains("casa") || cat.contains("transp")) {
necessidades += val;
} else {
desejos += val;
}
}
if (maiorGasto > 0) {
float percPiorCategoria = (maiorGasto / despesas) * 100;
tvTituloDica2.setText("Gastos Elevados em " + piorCategoria);
tvTituloDica2.setTextColor(Color.parseColor("#FF1744"));
tvDescDica2.setText(String.format("%.1f%%", percPiorCategoria) + " das tuas despesas são em " + piorCategoria + " (€ " + String.format("%.2f", maiorGasto) + "). Tenta reduzir aqui!");
if (rec > 0) {
float percNecessidades = (necessidades / rec) * 100;
float percDesejos = (desejos / rec) * 100;
if (percNecessidades <= 50 && percDesejos <= 30) {
tvTituloDica1.setText("Balanço Perfeito ⚖️");
tvTituloDica1.setTextColor(Color.parseColor("#00E676"));
tvDescDica1.setText("Estás a cumprir a Regra de Ouro (50/30/20). Os teus gastos essenciais e de lazer estão equilibrados face aos teus rendimentos.");
} else if (percDesejos > 30) {
tvTituloDica1.setText("Atenção aos Gastos Supérfluos 🛍️");
tvTituloDica1.setTextColor(Color.parseColor("#ECC94B"));
tvDescDica1.setText(String.format("A alocação em despesas não essenciais representa %.0f%% do teu orçamento. Recomenda-se reduzir para a margem dos 30%%.", percDesejos));
} else {
tvTituloDica1.setText("Despesas Fixas Elevadas 🏠");
tvTituloDica1.setTextColor(Color.parseColor("#F56565"));
tvDescDica1.setText(String.format("Os teus encargos fixos representam %.0f%% do salário. O indicador ideal para manter a estabilidade financeira é de 50%%.", percNecessidades));
}
} else {
tvTituloDica2.setText("Tudo Controlado ✅");
tvTituloDica2.setTextColor(Color.parseColor("#00E676"));
tvDescDica2.setText("Ainda não tens despesas suficientes para analisarmos. Continua o bom trabalho!");
tvTituloDica1.setText("Regra 50/30/20 ⚖️");
tvTituloDica1.setTextColor(corTextoDinamico);
tvDescDica1.setText("Regista receitas para que possamos calcular a distribuição ideal do teu património.");
}
// 4. Construir as barras de Distribuição de Gastos magicamente
layoutDistribuicao.removeAllViews(); // Limpa as barras antigas
// --- CARTÃO 2: RADAR DE ORÇAMENTOS ---
String alertaOrcamento = "Todos os orçamentos definidos encontram-se dentro dos limites previstos.";
int corAlerta = Color.parseColor("#00E676");
String tituloAlerta = "Orçamentos Controlados ✅";
if (despesas > 0) {
for (Map.Entry<String, Float> entry : gastosPorCategoria.entrySet()) {
float valorCat = entry.getValue();
if (valorCat > 0) {
float percentagem = (valorCat / despesas) * 100;
try {
float maiorRisco = 0;
String catRisco = "";
float faltaParaLimite = 0;
// Criar o título da categoria (Ex: Alimentação - €50.00 (20%))
TextView tvCat = new TextView(getActivity());
tvCat.setText(entry.getKey() + " — € " + String.format("%.2f", valorCat) + " (" + (int) percentagem + "%)");
tvCat.setTextColor(Color.WHITE);
tvCat.setTextSize(14f);
tvCat.setPadding(0, 16, 0, 8); // Margens
for (int i = 0; i < arrayOrcamentos.length(); i++) {
JSONObject obj = arrayOrcamentos.getJSONObject(i);
String cat = obj.getString("categoria");
float limite = (float) obj.getDouble("valor_limite");
float gasto = mapa.containsKey(cat) ? mapa.get(cat) : 0f;
// Criar a barra de progresso horizontal
ProgressBar pb = new ProgressBar(getActivity(), null, android.R.attr.progressBarStyleHorizontal);
pb.setMax(100);
pb.setProgress((int) percentagem);
pb.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E5FF"))); // Azul Tech
// Adicionar ao ecrã
layoutDistribuicao.addView(tvCat);
layoutDistribuicao.addView(pb);
if (limite > 0) {
float risco = gasto / limite;
if (risco > maiorRisco) {
maiorRisco = risco;
catRisco = cat;
faltaParaLimite = limite - gasto;
}
}
}
if (maiorRisco >= 1.0) {
tituloAlerta = "Orçamento Excedido 🚨";
corAlerta = Color.parseColor("#FF1744");
alertaOrcamento = "O limite definido para a categoria '" + catRisco + "' foi ultrapassado. Sugere-se o reajuste das restantes categorias.";
} else if (maiorRisco >= 0.8) {
tituloAlerta = "Aviso de Limite Próximo ⚠️";
corAlerta = Color.parseColor("#ECC94B");
alertaOrcamento = String.format("Atenção: A margem disponível para o orçamento de '%s' é de apenas %.2f€.", catRisco, faltaParaLimite);
} else if (arrayOrcamentos.length() == 0) {
tituloAlerta = "Planeamento Financeiro 🎯";
corAlerta = corTextoDinamico;
alertaOrcamento = "Acede ao separador 'Orçamentos' e estabelece limites para otimizar a tua gestão financeira.";
}
} catch (Exception e) { e.printStackTrace(); }
tvTituloDica2.setText(tituloAlerta);
tvTituloDica2.setTextColor(corAlerta);
tvDescDica2.setText(alertaOrcamento);
// --- CARTÃO 3: PREVISÃO E TENDÊNCIA DIÁRIA ---
Calendar cal = Calendar.getInstance();
int diaAtual = cal.get(Calendar.DAY_OF_MONTH);
int diasNoMes = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
if (desp > 0 && diaAtual > 0) {
float mediaDiaria = desp / diaAtual;
float previsaoFimDoMes = mediaDiaria * diasNoMes;
if (previsaoFimDoMes > rec && rec > 0) {
tvTituloDica3.setText("Projeção Mensal Elevada 📈");
tvTituloDica3.setTextColor(Color.parseColor("#FF1744"));
tvDescDica3.setText(String.format("A média de custos diários situa-se em %.2f€. Mantendo esta tendência, o custo final estimado será de %.2f€ (acima dos rendimentos).", mediaDiaria, previsaoFimDoMes));
} else {
tvTituloDica3.setText("Projeção Mensal Controlada 📉");
tvTituloDica3.setTextColor(Color.parseColor("#00E676"));
tvDescDica3.setText(String.format("A tua média de custos é de %.2f€ diários. A estimativa projetada para o final do mês é de %.2f€.", mediaDiaria, previsaoFimDoMes));
}
} else {
TextView semDespesas = new TextView(getActivity());
semDespesas.setText("Ainda não existem despesas registadas.");
semDespesas.setTextColor(Color.parseColor("#B0BEC5"));
layoutDistribuicao.addView(semDespesas);
tvTituloDica3.setText("Projeção de Despesas 📊");
tvTituloDica3.setTextColor(corTextoDinamico);
tvDescDica3.setText("Regista mais movimentos ao longo do mês para que o sistema possa projetar a tua média diária de despesas.");
}
// --- LISTA DE TOP DESPESAS ---
layoutDistribuicao.removeAllViews();
for (Map.Entry<String, Float> entry : mapa.entrySet()) {
String categoria = entry.getKey();
float valor = entry.getValue();
LinearLayout row = new LinearLayout(getContext());
row.setOrientation(LinearLayout.HORIZONTAL);
row.setPadding(40, 30, 40, 30);
row.setElevation(2f);
GradientDrawable shape = new GradientDrawable();
shape.setCornerRadius(24f);
shape.setColor(corFundoCartao);
row.setBackground(shape);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
params.setMargins(0, 0, 0, 20);
row.setLayoutParams(params);
String emoji = "💰";
String catLower = categoria.toLowerCase();
if (catLower.contains("alimen") || catLower.contains("restaurante")) emoji = "🍔";
else if (catLower.contains("transp") || catLower.contains("carro")) emoji = "🚗";
else if (catLower.contains("lazer") || catLower.contains("divers")) emoji = "🎮";
else if (catLower.contains("saúd") || catLower.contains("farmácia")) emoji = "💊";
else if (catLower.contains("educa")) emoji = "📚";
else if (catLower.contains("casa") || catLower.contains("renda") || catLower.contains("conta")) emoji = "🏠";
else if (catLower.contains("compras") || catLower.contains("roupa")) emoji = "🛍️";
TextView tvCat = new TextView(getContext());
tvCat.setText(emoji + " " + categoria);
tvCat.setTextColor(corTextoDinamico);
tvCat.setTextSize(16f);
tvCat.setTypeface(null, Typeface.BOLD);
tvCat.setLayoutParams(new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f));
TextView tvVal = new TextView(getContext());
tvVal.setText(String.format("€ %.2f", valor));
tvVal.setTextColor(Color.parseColor("#FF1744"));
tvVal.setTextSize(16f);
tvVal.setTypeface(null, Typeface.BOLD);
row.addView(tvCat);
row.addView(tvVal);
layoutDistribuicao.addView(row);
}
}
}

View File

@@ -1,42 +1,90 @@
package com.example.finzora;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.ColorStateList;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
public class EditarPerfilActivity extends AppCompatActivity {
private EditText editNomePerfil;
private EditText editEmailPerfil;
private ImageView imgFotoPerfil;
private String caminhoFotoGuardada = null;
private final ActivityResultLauncher<String> seletorImagens =
registerForActivityResult(new ActivityResultContracts.GetContent(), uri -> {
if (uri != null) {
String novoCaminho = guardarImagemInternamente(uri);
if (novoCaminho != null) {
caminhoFotoGuardada = novoCaminho;
imgFotoPerfil.setPadding(0, 0, 0, 0);
imgFotoPerfil.setImageTintList(null);
Glide.with(this)
.load(new File(caminhoFotoGuardada))
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imgFotoPerfil);
}
}
});
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_editar_perfil);
// Ligar ao XML
TextView btnVoltar = findViewById(R.id.btnVoltarEditarPerfil);
editNomePerfil = findViewById(R.id.editNomePerfil);
editEmailPerfil = findViewById(R.id.editEmailPerfil);
Button btnGuardarPerfil = findViewById(R.id.btnGuardarPerfil);
imgFotoPerfil = findViewById(R.id.imgFotoPerfil);
// Voltar para as definições
btnVoltar.setOnClickListener(v -> finish());
imgFotoPerfil.setOnClickListener(v -> seletorImagens.launch("image/*"));
// 1. CARREGAR OS DADOS ATUAIS DA MEMÓRIA
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String nomeAtual = prefs.getString("nome_usuario", "Investidor");
String emailAtual = prefs.getString("email_usuario", ""); // Pode estar vazio se não guardaste no login
editNomePerfil.setText(prefs.getString("nome_usuario", "Investidor"));
editEmailPerfil.setText(prefs.getString("email_usuario", ""));
editNomePerfil.setText(nomeAtual);
editEmailPerfil.setText(emailAtual);
caminhoFotoGuardada = prefs.getString("foto_usuario_path", null);
if (caminhoFotoGuardada != null) {
File arquivoFoto = new File(caminhoFotoGuardada);
if (arquivoFoto.exists()) {
imgFotoPerfil.setPadding(0, 0, 0, 0);
imgFotoPerfil.setImageTintList(null);
Glide.with(this)
.load(arquivoFoto)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imgFotoPerfil);
}
}
// 2. GUARDAR OS DADOS NOVOS
btnGuardarPerfil.setOnClickListener(v -> {
String novoNome = editNomePerfil.getText().toString().trim();
String novoEmail = editEmailPerfil.getText().toString().trim();
@@ -46,14 +94,46 @@ public class EditarPerfilActivity extends AppCompatActivity {
return;
}
// Grava na memória (SharedPreferences)
SharedPreferences.Editor editor = prefs.edit();
editor.putString("nome_usuario", novoNome);
editor.putString("email_usuario", novoEmail);
if (caminhoFotoGuardada != null) {
editor.putString("foto_usuario_path", caminhoFotoGuardada);
}
editor.apply();
// ⚠️ BACKUP TÁTICO DO NOME: Escrever no disco rígido para sobreviver ao Logout!
try {
FileOutputStream fos = openFileOutput("nome_perfil.txt", MODE_PRIVATE);
fos.write(novoNome.getBytes());
fos.close();
} catch (Exception e) {
e.printStackTrace();
}
Toast.makeText(this, "Perfil atualizado com sucesso! 🎉", Toast.LENGTH_SHORT).show();
finish(); // Fecha o ecrã e volta atrás
finish();
});
}
private String guardarImagemInternamente(Uri uri) {
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
File pasta = getFilesDir();
File arquivoFoto = new File(pasta, "foto_perfil.jpg");
FileOutputStream out = new FileOutputStream(arquivoFoto);
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out);
out.flush();
out.close();
return arquivoFoto.getAbsolutePath();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}

View File

@@ -1,5 +1,7 @@
package com.example.finzora;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.os.Bundle;
import android.view.LayoutInflater;
@@ -7,6 +9,7 @@ import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.github.mikephil.charting.charts.BarChart;
import com.github.mikephil.charting.charts.PieChart;
@@ -18,16 +21,32 @@ import com.github.mikephil.charting.data.PieData;
import com.github.mikephil.charting.data.PieDataSet;
import com.github.mikephil.charting.data.PieEntry;
import com.github.mikephil.charting.formatter.IndexAxisValueFormatter;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class GraficosFragment extends Fragment {
private PieChart pieChartDespesas;
private BarChart barChartOrcamento;
private BarChart barChartTendencia;
private DBHelper dbHelper;
// ⚠️ 1. As Vistas do Estado Vazio
private View scrollviewGraficos;
private View layoutEstadoVazio;
private int corTextoDinamica;
@Nullable
@Override
@@ -37,7 +56,16 @@ public class GraficosFragment extends Fragment {
pieChartDespesas = view.findViewById(R.id.pieChartDespesas);
barChartOrcamento = view.findViewById(R.id.barChartOrcamento);
barChartTendencia = view.findViewById(R.id.barChartTendencia);
dbHelper = new DBHelper(getActivity());
// ⚠️ 2. Ligar ao XML
scrollviewGraficos = view.findViewById(R.id.scrollviewGraficos);
layoutEstadoVazio = view.findViewById(R.id.layoutEstadoVazioGraficos);
if (getContext() != null) {
corTextoDinamica = ContextCompat.getColor(getContext(), R.color.texto_principal);
} else {
corTextoDinamica = Color.BLACK;
}
return view;
}
@@ -45,25 +73,134 @@ public class GraficosFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
carregarPieChart();
carregarBarChartOrcamento();
carregarBarChartTendencia();
carregarDadosDaNuvem();
}
// ==========================================
// 1. GRÁFICO CIRCULAR (Despesas por Categoria)
// ==========================================
private void carregarPieChart() {
private void carregarDadosDaNuvem() {
if (getActivity() == null) return;
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
OkHttpClient client = new OkHttpClient();
Request reqTransacoes = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(reqTransacoes).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (!response.isSuccessful()) return;
try {
String jsonTransacoes = response.body().string();
JSONArray arrTransacoes = new JSONArray(jsonTransacoes);
float somaReceitas = 0;
float somaDespesas = 0;
Map<String, Float> gastosPorCategoria = new HashMap<>();
for (int i = 0; i < arrTransacoes.length(); i++) {
JSONObject obj = arrTransacoes.getJSONObject(i);
int tipo = obj.getInt("tipo");
float valor = (float) obj.getDouble("valor");
String categoria = obj.getString("categoria");
if (tipo == 1) {
somaReceitas += valor;
} else if (tipo == 2) {
somaDespesas += valor;
float atual = gastosPorCategoria.containsKey(categoria) ? gastosPorCategoria.get(categoria) : 0f;
gastosPorCategoria.put(categoria, atual + valor);
}
}
final float totalReceitas = somaReceitas;
final float totalDespesas = somaDespesas;
final Map<String, Float> mapaGastos = gastosPorCategoria;
// ⚠️ 3. A Lógica de Mostrar/Esconder
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
// Se as receitas E as despesas forem 0, é porque não há transações nenhumas!
if (totalReceitas == 0 && totalDespesas == 0) {
scrollviewGraficos.setVisibility(View.GONE);
layoutEstadoVazio.setVisibility(View.VISIBLE);
} else {
scrollviewGraficos.setVisibility(View.VISIBLE);
layoutEstadoVazio.setVisibility(View.GONE);
// O utilizador tem dados! Vamos buscar os orçamentos para cruzar a informação
carregarOrcamentosEDesenhar(userId, mapaGastos, totalReceitas, totalDespesas);
}
});
}
} catch (Exception e) { e.printStackTrace(); }
}
});
}
private void carregarOrcamentosEDesenhar(String userId, Map<String, Float> mapaGastos, float totalReceitas, float totalDespesas) {
OkHttpClient client = new OkHttpClient();
Request reqOrcamentos = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(reqOrcamentos).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response2) throws IOException {
if (!response2.isSuccessful()) return;
try {
String jsonOrcamentos = response2.body().string();
JSONArray arrOrcamentos = new JSONArray(jsonOrcamentos);
Map<String, Float> limitesOrcamento = new HashMap<>();
for (int i = 0; i < arrOrcamentos.length(); i++) {
JSONObject obj = arrOrcamentos.getJSONObject(i);
limitesOrcamento.put(obj.getString("categoria"), (float) obj.getDouble("valor_limite"));
}
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
desenharPieChart(mapaGastos);
desenharBarChartOrcamento(limitesOrcamento, mapaGastos);
desenharBarChartTendencia(totalReceitas, totalDespesas);
});
}
} catch (Exception e) { e.printStackTrace(); }
}
});
}
private void desenharPieChart(Map<String, Float> despesas) {
pieChartDespesas.getDescription().setEnabled(false);
pieChartDespesas.setHoleColor(Color.parseColor("#2C5364"));
pieChartDespesas.getLegend().setTextColor(Color.WHITE);
pieChartDespesas.setHoleColor(Color.TRANSPARENT);
pieChartDespesas.getLegend().setTextColor(corTextoDinamica);
pieChartDespesas.getLegend().setTextSize(12f);
pieChartDespesas.getLegend().setWordWrapEnabled(true);
pieChartDespesas.setCenterText("Despesas");
pieChartDespesas.setCenterTextColor(Color.WHITE);
pieChartDespesas.setEntryLabelColor(Color.WHITE);
pieChartDespesas.setCenterTextColor(corTextoDinamica);
pieChartDespesas.setCenterTextSize(16f);
pieChartDespesas.setDrawEntryLabels(false);
HashMap<String, Float> despesas = dbHelper.getDespesasPorCategoria();
ArrayList<PieEntry> entradas = new ArrayList<>();
for (Map.Entry<String, Float> entry : despesas.entrySet()) {
if (entry.getValue() > 0) entradas.add(new PieEntry(entry.getValue(), entry.getKey()));
}
@@ -72,21 +209,18 @@ public class GraficosFragment extends Fragment {
PieDataSet dataSet = new PieDataSet(entradas, "");
dataSet.setColors(new int[]{Color.parseColor("#7C4DFF"), Color.parseColor("#00E5FF"), Color.parseColor("#FFD600"), Color.parseColor("#FF4081")});
dataSet.setSliceSpace(3f);
PieData data = new PieData(dataSet);
data.setValueTextSize(14f);
data.setValueTextColor(Color.WHITE);
data.setValueTextColor(corTextoDinamica);
pieChartDespesas.setData(data);
pieChartDespesas.animateY(1000);
}
// ==========================================
// 2. GRÁFICO DE BARRAS (Orçamento vs Gastos)
// ==========================================
private void carregarBarChartOrcamento() {
private void desenharBarChartOrcamento(Map<String, Float> orcamentos, Map<String, Float> gastos) {
configurarEstiloBarChart(barChartOrcamento);
Map<String, Float> orcamentos = dbHelper.getOrcamentosDefinidos();
ArrayList<BarEntry> gastosEntries = new ArrayList<>();
ArrayList<BarEntry> orcamentoEntries = new ArrayList<>();
ArrayList<String> categorias = new ArrayList<>();
@@ -95,7 +229,7 @@ public class GraficosFragment extends Fragment {
for (Map.Entry<String, Float> entry : orcamentos.entrySet()) {
String categoria = entry.getKey();
float limite = entry.getValue();
float gasto = dbHelper.getGastoPorCategoria(categoria);
float gasto = gastos.containsKey(categoria) ? gastos.get(categoria) : 0f;
categorias.add(categoria);
gastosEntries.add(new BarEntry(index, gasto));
@@ -106,83 +240,96 @@ public class GraficosFragment extends Fragment {
if (categorias.isEmpty()) { barChartOrcamento.clear(); return; }
BarDataSet setGastos = new BarDataSet(gastosEntries, "Gastos Reais");
setGastos.setColor(Color.parseColor("#FF4081")); // Rosa (Figma)
setGastos.setValueTextColor(Color.WHITE);
setGastos.setColor(Color.parseColor("#FF4081"));
setGastos.setValueTextColor(corTextoDinamica);
BarDataSet setOrcamento = new BarDataSet(orcamentoEntries, "Orçamento");
setOrcamento.setColor(Color.parseColor("#00E5FF")); // Azul (Figma)
setOrcamento.setValueTextColor(Color.WHITE);
setOrcamento.setColor(Color.parseColor("#00E5FF"));
setOrcamento.setValueTextColor(corTextoDinamica);
BarData data = new BarData(setGastos, setOrcamento);
// Lógica de agrupamento (Grouped Bar Chart)
float groupSpace = 0.2f; float barSpace = 0.05f; float barWidth = 0.35f;
// ⚠️ A MATEMÁTICA PERFEITA (tem de somar 1.00)
float barWidth = 0.35f;
float barSpace = 0.05f;
float groupSpace = 0.20f;
data.setBarWidth(barWidth);
barChartOrcamento.setData(data);
barChartOrcamento.groupBars(-0.5f, groupSpace, barSpace);
// Labels no Eixo X
// ⚠️ COMEÇA NO 0 PARA ALINHAR AO CENTRO
barChartOrcamento.groupBars(0f, groupSpace, barSpace);
XAxis xAxis = barChartOrcamento.getXAxis();
xAxis.setValueFormatter(new IndexAxisValueFormatter(categorias));
xAxis.setAxisMinimum(-0.5f);
xAxis.setAxisMaximum(categorias.size() - 0.5f);
// ⚠️ LIMITES DINÂMICOS PARA ENCAIXAR OS GRUPOS TODOS
xAxis.setAxisMinimum(0f);
xAxis.setAxisMaximum(barChartOrcamento.getBarData().getGroupWidth(groupSpace, barSpace) * categorias.size());
// Para não ficar tudo esmagado se tiveres muitas categorias, mete limite visível a 4
barChartOrcamento.setVisibleXRangeMaximum(4);
barChartOrcamento.animateY(1000);
barChartOrcamento.invalidate(); // Refresca o gráfico
}
// ==========================================
// 3. GRÁFICO DE BARRAS (Tendência Mensal Geral)
// ==========================================
private void carregarBarChartTendencia() {
private void desenharBarChartTendencia(float receitas, float despesas) {
configurarEstiloBarChart(barChartTendencia);
float totalReceitas = dbHelper.getTotalReceitas();
float totalDespesas = dbHelper.getTotalDespesas();
ArrayList<BarEntry> despesaEntry = new ArrayList<>();
ArrayList<BarEntry> receitaEntry = new ArrayList<>();
despesaEntry.add(new BarEntry(0, totalDespesas));
receitaEntry.add(new BarEntry(0, totalReceitas));
despesaEntry.add(new BarEntry(0, despesas));
receitaEntry.add(new BarEntry(0, receitas));
BarDataSet setDespesas = new BarDataSet(despesaEntry, "Despesas");
setDespesas.setColor(Color.parseColor("#FF1744")); // Vermelho
setDespesas.setValueTextColor(Color.WHITE);
setDespesas.setColor(Color.parseColor("#FF1744"));
setDespesas.setValueTextColor(corTextoDinamica);
BarDataSet setReceitas = new BarDataSet(receitaEntry, "Receitas");
setReceitas.setColor(Color.parseColor("#00E676")); // Verde
setReceitas.setValueTextColor(Color.WHITE);
setReceitas.setColor(Color.parseColor("#00E676"));
setReceitas.setValueTextColor(corTextoDinamica);
BarData data = new BarData(setDespesas, setReceitas);
float groupSpace = 0.3f; float barSpace = 0.05f; float barWidth = 0.3f;
// ⚠️ MATEMÁTICA PERFEITA PARA A TENDÊNCIA
float groupSpace = 0.3f;
float barSpace = 0.05f;
float barWidth = 0.3f;
data.setBarWidth(barWidth);
barChartTendencia.setData(data);
barChartTendencia.groupBars(-0.5f, groupSpace, barSpace);
// ⚠️ COMEÇA NO 0 TAMBÉM
barChartTendencia.groupBars(0f, groupSpace, barSpace);
ArrayList<String> labelMes = new ArrayList<>();
labelMes.add("Atual");
XAxis xAxis = barChartTendencia.getXAxis();
xAxis.setValueFormatter(new IndexAxisValueFormatter(labelMes));
xAxis.setAxisMinimum(-0.5f);
xAxis.setAxisMaximum(0.5f);
// ⚠️ COMO É SÓ 1 GRUPO, O MÁXIMO É 1
xAxis.setAxisMinimum(0f);
xAxis.setAxisMaximum(1f);
barChartTendencia.animateY(1000);
barChartTendencia.invalidate();
}
// Função de limpeza de design comum aos dois gráficos de barras
private void configurarEstiloBarChart(BarChart chart) {
chart.getDescription().setEnabled(false);
chart.getLegend().setTextColor(Color.WHITE);
chart.getAxisRight().setEnabled(false); // Esconde números à direita
chart.getLegend().setTextColor(corTextoDinamica);
chart.getAxisRight().setEnabled(false);
chart.getAxisLeft().setTextColor(Color.WHITE);
chart.getAxisLeft().setTextColor(corTextoDinamica);
chart.getAxisLeft().setDrawGridLines(true);
chart.getAxisLeft().setGridColor(Color.parseColor("#455A64")); // Linhas de fundo subtis
chart.getAxisLeft().setGridColor(Color.LTGRAY);
XAxis xAxis = chart.getXAxis();
xAxis.setPosition(XAxis.XAxisPosition.BOTTOM);
xAxis.setTextColor(Color.WHITE);
xAxis.setTextColor(corTextoDinamica);
xAxis.setDrawGridLines(false);
xAxis.setGranularity(1f);
xAxis.setCenterAxisLabels(true);

View File

@@ -0,0 +1,84 @@
package com.example.finzora;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import java.util.concurrent.Executor;
public class LockActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_lock);
Button btnDesbloquearApp = findViewById(R.id.btnDesbloquearApp);
btnDesbloquearApp.setOnClickListener(v -> solicitarBiometria());
// ⚠️ CORREÇÃO 1: Esperar meio segundo para o telemóvel real não entrar em pânico
new Handler(Looper.getMainLooper()).postDelayed(() -> {
solicitarBiometria();
}, 500);
}
private void solicitarBiometria() {
BiometricManager biometricManager = BiometricManager.from(this);
// ⚠️ CORREÇÃO 2: Mudamos para BIOMETRIC_WEAK para ser compatível com mais telemóveis reais
int authenticators = BiometricManager.Authenticators.BIOMETRIC_WEAK | BiometricManager.Authenticators.DEVICE_CREDENTIAL;
int canAuthenticate = biometricManager.canAuthenticate(authenticators);
if (canAuthenticate == BiometricManager.BIOMETRIC_SUCCESS) {
Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt biometricPrompt = new BiometricPrompt(LockActivity.this,
executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
// Se der erro 10 (User Canceled), não fazemos nada, ele clica no botão se quiser tentar de novo
if (errorCode != 10) {
Toast.makeText(getApplicationContext(), "Erro: " + errString, Toast.LENGTH_SHORT).show();
}
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
startActivity(new Intent(LockActivity.this, MainActivity.class));
finish();
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
Toast.makeText(getApplicationContext(), "Biometria não reconhecida.", Toast.LENGTH_SHORT).show();
}
});
BiometricPrompt.PromptInfo promptInfo = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Finzora: Escudo de Privacidade")
.setSubtitle("Usa a impressão digital ou o PIN do telemóvel")
.setAllowedAuthenticators(authenticators) // ⚠️ CORREÇÃO 3: Usar os mesmos flags aqui
.build();
biometricPrompt.authenticate(promptInfo);
} else {
// Se cair aqui, o Toast vai dizer o código do erro para investigarmos
Toast.makeText(this, "Escudo Inativo (Erro: " + canAuthenticate + "). A aceder...", Toast.LENGTH_LONG).show();
startActivity(new Intent(LockActivity.this, MainActivity.class));
finish();
}
}
}

View File

@@ -12,6 +12,9 @@ import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import okhttp3.Call;
@@ -36,28 +39,36 @@ public class LoginActivity extends AppCompatActivity {
boolean jaDeuLogin = prefs.getBoolean("is_logged_in", false);
if (jaDeuLogin) {
// Já tem o carimbo! Vai direto para o ecrã principal.
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
return; // Para o código aqui para não desenhar o ecrã de login
}
boolean usarBiometria = prefs.getBoolean("usar_biometria", false);
if (usarBiometria) {
startActivity(new Intent(LoginActivity.this, LockActivity.class));
} else {
startActivity(new Intent(LoginActivity.this, MainActivity.class));
}
finish();
return;
}
setContentView(R.layout.activity_login);
Intent intentDeepLink = getIntent();
if (intentDeepLink != null && Intent.ACTION_VIEW.equals(intentDeepLink.getAction())) {
android.net.Uri uri = intentDeepLink.getData();
if (uri != null && "finzora".equals(uri.getScheme()) && "confirmado".equals(uri.getHost())) {
Toast.makeText(this, "✅ Conta confirmada com sucesso! Já podes entrar.", Toast.LENGTH_LONG).show();
}
}
inicializarComponentes();
// Clique no botão "Entrar" -> Agora faz Login de verdade!
btnEntrar.setOnClickListener(v -> validarDados());
// Clique para ir para Registo
txtRegistrar.setOnClickListener(v -> {
Intent intent = new Intent(LoginActivity.this, RegisterActivity.class);
startActivity(intent);
});
// Clique no "Esqueci-me da palavra-passe" -> Agora abre o novo ecrã!
txtEsqueciPassword.setOnClickListener(v -> {
startActivity(new Intent(LoginActivity.this, RecuperarPasswordActivity.class));
});
@@ -74,7 +85,6 @@ public class LoginActivity extends AppCompatActivity {
editPassword.setError("Introduza a sua palavra-passe");
editPassword.requestFocus();
} else {
// Desativa o botão enquanto pensa
btnEntrar.setEnabled(false);
btnEntrar.setText("A VERIFICAR DADOS...");
fazerLoginNoSupabase(email, password);
@@ -87,7 +97,6 @@ public class LoginActivity extends AppCompatActivity {
String json = "{\"email\":\"" + email + "\", \"password\":\"" + password + "\"}";
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
// URL para fazer login (grant_type=password)
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/auth/v1/token?grant_type=password")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
@@ -114,7 +123,6 @@ public class LoginActivity extends AppCompatActivity {
btnEntrar.setText("INICIAR SESSÃO");
if (response.isSuccessful()) {
// SUCESSO! A palavra-passe estava certa!
try {
JSONObject jsonResponse = new JSONObject(responseData);
String userId = jsonResponse.getJSONObject("user").getString("id");
@@ -122,30 +130,65 @@ public class LoginActivity extends AppCompatActivity {
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("is_logged_in", true); // O nosso carimbo!
editor.putString("user_id", userId); // O ID do Supabase
editor.putString("email_usuario", email);// Guardamos o email para o Perfil
editor.putBoolean("is_logged_in", true);
editor.putString("user_id", userId);
editor.putString("email_usuario", email);
// 🏆 A MAGIA ACONTECE AQUI: Recuperar Nome e Foto!
// 1. Recuperar Nome do Backup Físico
String nomeRecuperado = "Investidor";
try {
FileInputStream fis = openFileInput("nome_perfil.txt");
byte[] bytes = new byte[fis.available()];
fis.read(bytes);
fis.close();
nomeRecuperado = new String(bytes);
} catch (Exception e) {
// Se não tiver backup, tenta ver se a Base de Dados devolveu algum nome
try {
JSONObject meta = jsonResponse.getJSONObject("user").optJSONObject("user_metadata");
if (meta != null && meta.has("nome")) nomeRecuperado = meta.getString("nome");
} catch (Exception ignored) {}
}
editor.putString("nome_usuario", nomeRecuperado);
// 2. Recuperar Caminho da Foto (Ela está sempre guardada na pasta principal)
File arquivoFoto = new File(getFilesDir(), "foto_perfil.jpg");
if (arquivoFoto.exists()) {
editor.putString("foto_usuario_path", arquivoFoto.getAbsolutePath());
}
editor.apply();
Toast.makeText(LoginActivity.this, "Bem-vindo de volta!", Toast.LENGTH_SHORT).show();
irParaDashboard();
irParaSeguranca();
} catch (Exception e) {
Toast.makeText(LoginActivity.this, "Erro a processar os dados.", Toast.LENGTH_SHORT).show();
}
} else {
// ERRO! Palavra-passe errada ou email não existe!
Toast.makeText(LoginActivity.this, "Credenciais incorretas. Tenta novamente!", Toast.LENGTH_LONG).show();
editPassword.setError("Palavra-passe errada");
editPassword.setText(""); // Limpa a password para ele tentar de novo
editPassword.setText("");
}
});
}
});
}
private void irParaDashboard() {
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
private void irParaSeguranca() {
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
boolean usarBiometria = prefs.getBoolean("usar_biometria", false);
Intent intent;
if (usarBiometria) {
intent = new Intent(LoginActivity.this, LockActivity.class);
} else {
intent = new Intent(LoginActivity.this, MainActivity.class);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
startActivity(intent);
finish();

View File

@@ -1,16 +1,31 @@
package com.example.finzora;
import android.app.AlertDialog;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.pdf.PdfDocument;
import android.os.Bundle;
import android.os.Environment;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
import androidx.cardview.widget.CardView;
import androidx.viewpager2.widget.ViewPager2;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
@@ -18,7 +33,12 @@ import com.google.android.material.tabs.TabLayoutMediator;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.Calendar;
import java.util.Date;
import okhttp3.Call;
import okhttp3.Callback;
@@ -35,6 +55,7 @@ public class MainActivity extends AppCompatActivity {
private Button btnSair;
private TextView tvSaldoGeral, tvReceitasGeral, tvDespesasGeral;
private JSONArray listaTransacoesGlobal;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -65,60 +86,480 @@ public class MainActivity extends AppCompatActivity {
String nome = prefs.getString("nome_usuario", "Investidor");
tvNomeUsuario.setText("Olá, " + nome);
btnSair.setOnClickListener(v -> {
prefs.edit().clear().apply();
startActivity(new Intent(this, LoginActivity.class));
finish();
});
btnSair.setOnClickListener(v -> finishAffinity());
fabAdicionar.setOnClickListener(v -> {
startActivity(new Intent(this, AdicionarTransacaoActivity.class));
});
fabAdicionar.setOnClickListener(v -> startActivity(new Intent(this, AdicionarTransacaoActivity.class)));
ImageView btnAbrirDefinicoes = findViewById(R.id.btnAbrirDefinicoes);
if (btnAbrirDefinicoes != null) {
btnAbrirDefinicoes.setOnClickListener(v -> {
startActivity(new Intent(MainActivity.this, DefinicoesActivity.class));
btnAbrirDefinicoes.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, DefinicoesActivity.class)));
}
ImageView imgLogoPerfil = findViewById(R.id.imgLogo);
if (imgLogoPerfil != null) {
imgLogoPerfil.setOnClickListener(v -> startActivity(new Intent(MainActivity.this, EditarPerfilActivity.class)));
}
ImageView btnExportarPDF = findViewById(R.id.btnExportarPDF);
if (btnExportarPDF != null) {
btnExportarPDF.setOnClickListener(v -> {
if (listaTransacoesGlobal != null && listaTransacoesGlobal.length() > 0) {
mostrarDialogoExportacao();
} else {
Toast.makeText(this, "Ainda não tens dados para exportar!", Toast.LENGTH_SHORT).show();
}
});
}
configurarAbas();
atualizarCartoes(); // Chama a nova função ligada à net!
atualizarCartoes();
carregarFotoPerfil();
// 🛡️ TÁTICA DE VISIBILIDADE DO BOTÃO +
viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() {
@Override
public void onPageSelected(int position) {
super.onPageSelected(position);
// Posições: 0 = Transações, 1 = Orçamentos, 2 = Gráficos, 3 = Objetivos, 4 = Dicas
if (position == 0 || position == 1) {
fabAdicionar.show(); // Só mostra nas Transações e Orçamentos
} else {
fabAdicionar.hide(); // Esconde nos Gráficos, Objetivos e Dicas
}
}
});
}
private void mostrarDialogoExportacao() {
View view = getLayoutInflater().inflate(R.layout.dialog_exportar, null);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setView(view);
AlertDialog dialog = builder.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new ColorDrawable(0));
}
CardView btnPdf = view.findViewById(R.id.btnOpcaoPDF);
CardView btnExcel = view.findViewById(R.id.btnOpcaoExcel);
TextView btnCancelar = view.findViewById(R.id.btnCancelarExportacao);
btnPdf.setOnClickListener(v -> {
gerarRelatorioPDF(listaTransacoesGlobal);
dialog.dismiss();
});
btnExcel.setOnClickListener(v -> {
gerarRelatorioCSV(listaTransacoesGlobal);
dialog.dismiss();
});
btnCancelar.setOnClickListener(v -> dialog.dismiss());
dialog.show();
}
private void gerarRelatorioCSV(JSONArray transacoes) {
StringBuilder csvData = new StringBuilder();
csvData.append("Data,Descricao,Categoria,Tipo,Valor(Euros)\n");
try {
for (int i = 0; i < transacoes.length(); i++) {
JSONObject obj = transacoes.getJSONObject(i);
String data = obj.optString("data", "---");
String desc = obj.optString("descricao", "---").replace(",", " ");
String cat = obj.optString("categoria", "---");
int tipo = obj.optInt("tipo");
double valor = obj.optDouble("valor");
String tipoStr = (tipo == 1) ? "Receita" : "Despesa";
csvData.append(data).append(",").append(desc).append(",").append(cat).append(",").append(tipoStr).append(",").append(valor).append("\n");
}
String nomeFicheiro = "Finzora_Export_" + System.currentTimeMillis() + ".csv";
File arquivoCsv = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), nomeFicheiro);
FileOutputStream fos = new FileOutputStream(arquivoCsv);
fos.write(csvData.toString().getBytes());
fos.close();
Toast.makeText(this, "Ficheiro Excel guardado com sucesso! 📊", Toast.LENGTH_LONG).show();
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(this, "Erro ao gerar o ficheiro Excel.", Toast.LENGTH_SHORT).show();
}
}
// 🏆 A JOGADA DE MESTRE: PDF COM DIAGNÓSTICO INTELIGENTE!
private void gerarRelatorioPDF(JSONArray transacoes) {
PdfDocument pdf = new PdfDocument();
int numeroPagina = 1;
PdfDocument.PageInfo pageInfo = new PdfDocument.PageInfo.Builder(595, 842, numeroPagina).create();
PdfDocument.Page pagina = pdf.startPage(pageInfo);
Canvas canvas = pagina.getCanvas();
Paint paint = new Paint();
int totalTransacoes = transacoes.length();
double maiorDespesa = 0, maiorReceita = 0, somaDespesas = 0, somaReceitas = 0;
double necessidades = 0, desejos = 0;
String descMaiorDespesa = "-", descMaiorReceita = "-";
int countDespesas = 0;
try {
for (int i = 0; i < totalTransacoes; i++) {
JSONObject obj = transacoes.getJSONObject(i);
double v = obj.getDouble("valor");
int t = obj.getInt("tipo");
String d = obj.getString("descricao");
String cat = obj.optString("categoria", "").toLowerCase();
if (t == 1) {
somaReceitas += v;
if (v > maiorReceita) { maiorReceita = v; descMaiorReceita = d; }
} else {
somaDespesas += v; countDespesas++;
if (v > maiorDespesa) { maiorDespesa = v; descMaiorDespesa = d; }
if (cat.contains("conta") || cat.contains("alimen") || cat.contains("saúd") || cat.contains("educa") || cat.contains("casa") || cat.contains("transp")) {
necessidades += v;
} else {
desejos += v;
}
}
}
} catch (Exception e) { e.printStackTrace(); }
double mediaDespesa = countDespesas > 0 ? somaDespesas / countDespesas : 0;
// CABEÇALHO DO RELATÓRIO
paint.setColor(Color.parseColor("#00E676"));
canvas.drawRect(0, 0, 595, 120, paint);
paint.setColor(Color.parseColor("#1A202C"));
paint.setTextSize(28f);
paint.setFakeBoldText(true);
canvas.drawText("FINZORA", 40, 65, paint);
paint.setTextSize(12f);
paint.setFakeBoldText(false);
canvas.drawText("RELATÓRIO FINANCEIRO E DIAGNÓSTICO", 40, 90, paint);
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String nome = prefs.getString("nome_usuario", "Investidor");
String email = prefs.getString("email_usuario", "---");
paint.setTextAlign(Paint.Align.RIGHT);
canvas.drawText("Utilizador: " + nome, 555, 60, paint);
canvas.drawText("Email: " + email, 555, 80, paint);
paint.setTextAlign(Paint.Align.LEFT);
paint.setColor(Color.parseColor("#F7FAFC"));
canvas.drawRoundRect(40, 140, 555, 210, 10, 10, paint);
paint.setColor(Color.BLACK);
paint.setTextSize(12f);
paint.setFakeBoldText(true);
canvas.drawText("SALDOS ATUAIS", 60, 165, paint);
paint.setFakeBoldText(false);
paint.setTextSize(11f);
canvas.drawText("SALDO: " + tvSaldoGeral.getText().toString(), 60, 190, paint);
canvas.drawText("RECEITAS: " + tvReceitasGeral.getText().toString(), 250, 190, paint);
canvas.drawText("DESPESAS: " + tvDespesasGeral.getText().toString(), 420, 190, paint);
paint.setColor(Color.parseColor("#F1F5F9"));
canvas.drawRoundRect(40, 225, 555, 305, 10, 10, paint);
paint.setColor(Color.BLACK);
paint.setTextSize(12f);
paint.setFakeBoldText(true);
canvas.drawText("ANÁLISE DE PERFORMANCE", 60, 250, paint);
paint.setFakeBoldText(false);
paint.setTextSize(11f);
canvas.drawText("Maior Despesa: " + descMaiorDespesa + " (" + String.format("%.2f €", maiorDespesa) + ")", 60, 275, paint);
canvas.drawText("Maior Receita: " + descMaiorReceita + " (" + String.format("%.2f €", maiorReceita) + ")", 60, 295, paint);
canvas.drawText("Média por Despesa: " + String.format("%.2f €", mediaDespesa), 350, 275, paint);
canvas.drawText("Total de Movimentos: " + totalTransacoes, 350, 295, paint);
paint.setColor(Color.BLACK);
paint.setTextSize(12f);
paint.setFakeBoldText(true);
canvas.drawText("BALANÇO VISUAL", 40, 345, paint);
double totalDinheiro = somaReceitas + somaDespesas;
if (totalDinheiro > 0) {
RectF areaDoGrafico = new RectF(60, 365, 200, 505);
float anguloReceitas = (float) ((somaReceitas / totalDinheiro) * 360f);
float anguloDespesas = (float) ((somaDespesas / totalDinheiro) * 360f);
paint.setColor(Color.parseColor("#00E676"));
canvas.drawArc(areaDoGrafico, 0, anguloReceitas, true, paint);
paint.setColor(Color.parseColor("#FF1744"));
canvas.drawArc(areaDoGrafico, anguloReceitas, anguloDespesas, true, paint);
paint.setTextSize(11f);
paint.setFakeBoldText(false);
paint.setColor(Color.parseColor("#00E676"));
canvas.drawRect(230, 410, 245, 425, paint);
paint.setColor(Color.BLACK);
canvas.drawText("Receitas (" + String.format("%.0f", (somaReceitas/totalDinheiro)*100) + "%)", 255, 422, paint);
paint.setColor(Color.parseColor("#FF1744"));
canvas.drawRect(230, 440, 245, 455, paint);
paint.setColor(Color.BLACK);
canvas.drawText("Despesas (" + String.format("%.0f", (somaDespesas/totalDinheiro)*100) + "%)", 255, 452, paint);
}
paint.setFakeBoldText(true);
paint.setTextSize(14f);
canvas.drawText("HISTÓRICO DETALHADO", 40, 540, paint);
paint.setColor(Color.parseColor("#EDF2F7"));
canvas.drawRect(40, 555, 555, 580, paint);
paint.setColor(Color.BLACK);
paint.setTextSize(12f);
canvas.drawText("DESCRIÇÃO", 50, 572, paint);
canvas.drawText("VALOR", 350, 572, paint);
canvas.drawText("DATA", 470, 572, paint);
paint.setFakeBoldText(false);
int y = 605;
try {
for (int i = 0; i < transacoes.length(); i++) {
if (y > 760) {
desenharRodape(canvas, paint, numeroPagina);
pdf.finishPage(pagina);
numeroPagina++;
pageInfo = new PdfDocument.PageInfo.Builder(595, 842, numeroPagina).create();
pagina = pdf.startPage(pageInfo);
canvas = pagina.getCanvas();
paint.setColor(Color.parseColor("#00E676"));
canvas.drawRect(0, 0, 595, 40, paint);
paint.setColor(Color.parseColor("#1A202C"));
paint.setTextSize(14f);
paint.setFakeBoldText(true);
canvas.drawText("FINZORA - Continuação do Histórico", 40, 25, paint);
paint.setColor(Color.parseColor("#EDF2F7"));
canvas.drawRect(40, 60, 555, 85, paint);
paint.setColor(Color.BLACK);
paint.setTextSize(12f);
canvas.drawText("DESCRIÇÃO", 50, 77, paint);
canvas.drawText("VALOR", 350, 77, paint);
canvas.drawText("DATA", 470, 77, paint);
y = 110;
paint.setFakeBoldText(false);
}
JSONObject obj = transacoes.getJSONObject(i);
String desc = obj.getString("descricao");
double valor = obj.getDouble("valor");
String data = obj.optString("data", "---");
int tipo = obj.getInt("tipo");
if (i % 2 == 0) {
paint.setColor(Color.parseColor("#F8FAFC"));
canvas.drawRect(40, y - 20, 555, y + 10, paint);
}
paint.setColor(Color.BLACK);
canvas.drawText(desc, 50, y, paint);
if (tipo == 1) {
paint.setColor(Color.parseColor("#2F855A"));
canvas.drawText("+ " + String.format("%.2f €", valor), 350, y, paint);
} else {
paint.setColor(Color.parseColor("#C53030"));
canvas.drawText("- " + String.format("%.2f €", valor), 350, y, paint);
}
paint.setColor(Color.BLACK);
canvas.drawText(data, 470, y, paint);
y += 35;
}
} catch (Exception e) { e.printStackTrace(); }
// 🧠 ----------------- NOVO BLOCO: DIAGNÓSTICO FINANCEIRO COM TEXTO QUEBRADO -----------------
y += 20;
// Aumentei o espaço de controlo para a nova caixa que é mais alta (160px)
if (y + 160 > 800) {
desenharRodape(canvas, paint, numeroPagina);
pdf.finishPage(pagina);
numeroPagina++;
pageInfo = new PdfDocument.PageInfo.Builder(595, 842, numeroPagina).create();
pagina = pdf.startPage(pageInfo);
canvas = pagina.getCanvas();
y = 60;
}
paint.setColor(Color.BLACK);
paint.setFakeBoldText(true);
paint.setTextSize(14f);
canvas.drawText("💡 DIAGNÓSTICO E RECOMENDAÇÕES", 40, y, paint);
y += 15;
// Desenha a caixa azul mais alta
paint.setColor(Color.parseColor("#EBF8FF"));
canvas.drawRoundRect(40, y, 555, y + 150, 10, 10, paint);
y += 25;
paint.setTextSize(11f);
int colunaTitulos = 50;
int colunaTexto = 200; // Puxei o texto um bocadinho mais para a esquerda para caber à vontade
// 1. Regra 50/30/20
double percNecessidades = somaReceitas > 0 ? (necessidades / somaReceitas) * 100 : 0;
double percDesejos = somaReceitas > 0 ? (desejos / somaReceitas) * 100 : 0;
paint.setColor(Color.BLACK);
paint.setFakeBoldText(true);
canvas.drawText("Balanço (Regra 50/30/20):", colunaTitulos, y, paint);
paint.setFakeBoldText(false);
if (somaReceitas > 0) {
if (percNecessidades <= 50 && percDesejos <= 30) {
canvas.drawText("Excelente! Gastos essenciais e de lazer", colunaTexto, y, paint);
canvas.drawText("encontram-se equilibrados.", colunaTexto, y + 15, paint);
} else if (percDesejos > 30) {
canvas.drawText("Alerta: Gastos em Lazer elevados (" + String.format("%.0f", percDesejos) + "%).", colunaTexto, y, paint);
canvas.drawText("Aconselha-se redução para 30%.", colunaTexto, y + 15, paint);
} else {
canvas.drawText("Aviso: Despesas Fixas elevadas", colunaTexto, y, paint);
canvas.drawText("(" + String.format("%.0f", percNecessidades) + "% do rendimento total).", colunaTexto, y + 15, paint);
}
} else {
canvas.drawText("Registe receitas para cálculo fiável.", colunaTexto, y, paint);
}
// 2. Taxa de Poupança
y += 35; // Espaço duplo por causa das duas linhas de cima
double taxaPoupanca = somaReceitas > 0 ? ((somaReceitas - somaDespesas) / somaReceitas) * 100 : 0;
paint.setFakeBoldText(true);
canvas.drawText("Taxa de Poupança Global:", colunaTitulos, y, paint);
paint.setFakeBoldText(false);
canvas.drawText(String.format("%.1f%%", Math.max(taxaPoupanca, 0)) + " do rendimento foi retido.", colunaTexto, y, paint);
// 3. Projeção Mensal
y += 25;
Calendar cal = Calendar.getInstance();
int diaAtual = cal.get(Calendar.DAY_OF_MONTH);
int diasNoMes = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
double mediaDiaria = diaAtual > 0 ? somaDespesas / diaAtual : 0;
double previsao = mediaDiaria * diasNoMes;
paint.setFakeBoldText(true);
canvas.drawText("Projeção (Fim do Mês):", colunaTitulos, y, paint);
paint.setFakeBoldText(false);
canvas.drawText(String.format("Média de %.2f€ diários. Gasto", mediaDiaria), colunaTexto, y, paint);
canvas.drawText(String.format("final estimado: %.2f€", previsao), colunaTexto, y + 15, paint);
// 4. Parecer Final
y += 35;
paint.setFakeBoldText(true);
canvas.drawText("Parecer Final:", colunaTitulos, y, paint);
paint.setFakeBoldText(false);
if (previsao > somaReceitas && somaReceitas > 0) {
paint.setColor(Color.parseColor("#C53030")); // Vermelho
canvas.drawText("ALTO RISCO: Mantendo esta tendência,", colunaTexto, y, paint);
canvas.drawText("irá fechar o mês com saldo negativo.", colunaTexto, y + 15, paint);
} else if (somaReceitas == 0) {
paint.setColor(Color.DKGRAY);
canvas.drawText("A aguardar entrada de receitas para", colunaTexto, y, paint);
canvas.drawText("efetuar um diagnóstico final.", colunaTexto, y + 15, paint);
} else {
paint.setColor(Color.parseColor("#2F855A")); // Verde
canvas.drawText("ESTÁVEL: A sua projeção indica que", colunaTexto, y, paint);
canvas.drawText("fechará o mês com lucro.", colunaTexto, y + 15, paint);
}
// -------------------------------------------------------------------------------
desenharRodape(canvas, paint, numeroPagina);
pdf.finishPage(pagina);
String nomeFicheiro = "Relatorio_Consultoria_Finzora_" + System.currentTimeMillis() + ".pdf";
File arquivoPdf = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), nomeFicheiro);
try {
pdf.writeTo(new FileOutputStream(arquivoPdf));
Toast.makeText(this, "Relatório de Consultoria gerado com sucesso! 📄📈", Toast.LENGTH_LONG).show();
} catch (IOException e) {
e.printStackTrace();
} finally {
pdf.close();
}
}
private void desenharRodape(Canvas canvas, Paint paint, int paginaActual) {
paint.setColor(Color.GRAY);
paint.setTextSize(10f);
paint.setFakeBoldText(false);
canvas.drawLine(40, 800, 555, 800, paint);
String dataGeracao = java.text.DateFormat.getDateTimeInstance().format(new java.util.Date());
canvas.drawText("Emitido por Finzora Consultoria Financeira em: " + dataGeracao, 40, 815, paint);
canvas.drawText("Página " + paginaActual, 510, 815, paint);
}
private void carregarFotoPerfil() {
ImageView imgLogoPerfil = findViewById(R.id.imgLogo);
if (imgLogoPerfil == null) return;
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String caminhoFoto = prefs.getString("foto_usuario_path", null);
if (caminhoFoto != null) {
File arquivoFoto = new File(caminhoFoto);
if (arquivoFoto.exists()) {
imgLogoPerfil.setPadding(0, 0, 0, 0);
imgLogoPerfil.setImageTintList(null);
Glide.with(this)
.load(arquivoFoto)
.skipMemoryCache(true)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.circleCrop()
.into(imgLogoPerfil);
}
}
}
@Override
protected void onResume() {
super.onResume();
atualizarCartoes();
carregarFotoPerfil();
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String nome = prefs.getString("nome_usuario", "Investidor");
if (tvNomeUsuario != null) {
tvNomeUsuario.setText("Olá, " + nome);
}
if (tvNomeUsuario != null) tvNomeUsuario.setText("Olá, " + nome);
}
private void configurarAbas() {
ViewPagerAdapter adapter = new ViewPagerAdapter(this);
viewPager.setAdapter(adapter);
new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> {
switch (position) {
case 0: tab.setText("Transações"); break;
case 1: tab.setText("Orçamentos"); break;
case 2: tab.setText("Gráficos"); break;
case 3: tab.setText("Dicas"); break;
case 3: tab.setText("Objetivos"); break;
case 4: tab.setText("Dicas"); break;
}
}).attach();
}
// ==========================================================
// --- CALCULAR O SALDO DIRETAMENTE DO SUPABASE ---
// ==========================================================
public void atualizarCartoes() {
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
OkHttpClient client = new OkHttpClient();
@@ -138,44 +579,32 @@ public class MainActivity extends AppCompatActivity {
try {
String jsonResposta = response.body().string();
JSONArray jsonArray = new JSONArray(jsonResposta);
listaTransacoesGlobal = jsonArray;
float receitas = 0;
float despesas = 0;
// Percorrer todas as transações da nuvem e somar!
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
float valor = (float) obj.getDouble("valor");
int tipo = obj.getInt("tipo");
if (tipo == 1) {
receitas += valor;
} else if (tipo == 2) {
despesas += valor;
}
if (tipo == 1) receitas += valor;
else if (tipo == 2) despesas += valor;
}
final float totalReceitas = receitas;
final float totalDespesas = despesas;
final float saldo = receitas - despesas;
// Atualizar o design do ecrã
runOnUiThread(() -> {
if (tvReceitasGeral != null) tvReceitasGeral.setText(String.format("€ %.2f", totalReceitas));
if (tvDespesasGeral != null) tvDespesasGeral.setText(String.format("€ %.2f", totalDespesas));
if (tvSaldoGeral != null) {
tvSaldoGeral.setText(String.format("€ %.2f", saldo));
if (saldo < 0) {
tvSaldoGeral.setTextColor(Color.parseColor("#FF1744"));
} else {
tvSaldoGeral.setTextColor(getResources().getColor(R.color.texto_principal));
}
tvSaldoGeral.setTextColor(saldo < 0 ? Color.parseColor("#FF1744") : getResources().getColor(R.color.texto_principal));
}
});
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) { e.printStackTrace(); }
}
}
});

View File

@@ -0,0 +1,116 @@
package com.example.finzora;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.widget.Button;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import com.google.android.material.textfield.TextInputEditText;
import java.io.IOException;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class NovaPasswordActivity extends AppCompatActivity {
private TextInputEditText editNovaPass, editConfirmaNovaPass;
private Button btnGuardar;
private String accessTokenParaRecuperar = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_nova_password);
editNovaPass = findViewById(R.id.editNovaPass);
editConfirmaNovaPass = findViewById(R.id.editConfirmaNovaPass);
btnGuardar = findViewById(R.id.btnGuardarNovaPass);
// ⚠️ O RADAR EM AÇÃO: Tentar ler o link mágico com que a app foi aberta
Uri uri = getIntent().getData();
if (uri != null && uri.getFragment() != null) {
// O Supabase manda o token no "fragment" do link (depois do #)
String fragmento = uri.getFragment();
String[] partes = fragmento.split("&");
for (String parte : partes) {
if (parte.startsWith("access_token=")) {
accessTokenParaRecuperar = parte.substring("access_token=".length());
break;
}
}
}
if (accessTokenParaRecuperar == null) {
Toast.makeText(this, "Erro: Link mágico inválido ou expirado.", Toast.LENGTH_LONG).show();
finish();
}
btnGuardar.setOnClickListener(v -> validarEAtualizarPassword());
}
private void validarEAtualizarPassword() {
String pass1 = editNovaPass.getText().toString().trim();
String pass2 = editConfirmaNovaPass.getText().toString().trim();
if (TextUtils.isEmpty(pass1) || pass1.length() < 6) {
editNovaPass.setError("Mínimo de 6 caracteres.");
return;
}
if (!pass1.equals(pass2)) {
editConfirmaNovaPass.setError("As senhas não coincidem.");
return;
}
btnGuardar.setEnabled(false);
btnGuardar.setText("A GUARDAR...");
// ☁️ Enviar a nova password para a Nuvem
OkHttpClient client = new OkHttpClient();
String json = "{\"password\":\"" + pass1 + "\"}";
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/auth/v1/user")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
// Usamos o token mágico que apanhamos do email para provar quem somos!
.addHeader("Authorization", "Bearer " + accessTokenParaRecuperar)
.put(body) // Para atualizar os dados do utilizador usa-se PUT
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(() -> {
Toast.makeText(NovaPasswordActivity.this, "Erro de rede!", Toast.LENGTH_SHORT).show();
btnGuardar.setEnabled(true);
btnGuardar.setText("GUARDAR PALAVRA-PASSE");
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
runOnUiThread(() -> {
if (response.isSuccessful()) {
Toast.makeText(NovaPasswordActivity.this, "Password atualizada com sucesso! 🎉", Toast.LENGTH_LONG).show();
// Volta para o Ecrã de Login para entrar com a nova pass
startActivity(new Intent(NovaPasswordActivity.this, LoginActivity.class));
finish();
} else {
Toast.makeText(NovaPasswordActivity.this, "Erro ao atualizar. Tenta pedir novo link.", Toast.LENGTH_LONG).show();
btnGuardar.setEnabled(true);
btnGuardar.setText("GUARDAR PALAVRA-PASSE");
}
});
}
});
}
}

View File

@@ -0,0 +1,17 @@
package com.example.finzora;
public class Objetivo {
private String id;
private String nome;
private float valorAlvo;
public Objetivo(String id, String nome, float valorAlvo) {
this.id = id;
this.nome = nome;
this.valorAlvo = valorAlvo;
}
public String getId() { return id; }
public String getNome() { return nome; }
public float getValorAlvo() { return valorAlvo; }
}

View File

@@ -0,0 +1,119 @@
package com.example.finzora;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.cardview.widget.CardView;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
public class ObjetivosAdapter extends RecyclerView.Adapter<ObjetivosAdapter.ObjetivosViewHolder> {
private List<Objetivo> listaObjetivos;
private ObjetivosFragment fragment;
private float saldoAtual;
public ObjetivosAdapter(List<Objetivo> lista, float saldoAtual, ObjetivosFragment fragment) {
this.listaObjetivos = lista;
this.saldoAtual = saldoAtual;
this.fragment = fragment;
}
@NonNull
@Override
public ObjetivosViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_objetivo, parent, false);
return new ObjetivosViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ObjetivosViewHolder holder, int position) {
Objetivo item = listaObjetivos.get(position);
int percentagem = 0;
float valorGuardado = 0;
if (saldoAtual > 0) {
valorGuardado = saldoAtual;
if (valorGuardado >= item.getValorAlvo()) {
valorGuardado = item.getValorAlvo();
percentagem = 100;
} else {
percentagem = (int) ((valorGuardado / item.getValorAlvo()) * 100);
}
}
holder.tvNome.setText(item.getNome());
holder.progress.setProgress(percentagem);
holder.tvPercentagem.setText(percentagem + "%");
// 🏆 A MAGIA DA CELEBRAÇÃO QUANDO CHEGAS AOS 100%
if (percentagem == 100) {
holder.tvPercentagem.setTextColor(Color.parseColor("#00E676"));
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E676")));
holder.imgIcone.setColorFilter(Color.parseColor("#00E676"));
holder.cardObjetivo.setCardBackgroundColor(Color.parseColor("#1A00E676"));
holder.tvValores.setText("🎉 Parabéns! Já podes comprar a tua conquista.");
holder.tvValores.setTextColor(Color.parseColor("#00E676"));
holder.btnEditar.setVisibility(View.GONE);
} else {
holder.tvPercentagem.setTextColor(Color.parseColor("#00B8D4"));
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00B8D4")));
holder.imgIcone.setColorFilter(Color.parseColor("#00B8D4"));
holder.cardObjetivo.setCardBackgroundColor(Color.TRANSPARENT);
holder.tvValores.setText(String.format("Guardado: € %.2f / Alvo: € %.2f", valorGuardado, item.getValorAlvo()));
holder.tvValores.setTextColor(Color.GRAY);
holder.btnEditar.setVisibility(View.VISIBLE);
}
// ✏️ Ação do NOVO Botão Editar AGORA A FUNCIONAR
holder.btnEditar.setOnClickListener(v -> {
if (fragment != null) {
fragment.editarObjetivo(item); // CHAMA A JANELA DE EDIÇÃO!
}
});
// 🗑️ Botão Eliminar
holder.btnEliminar.setOnClickListener(v -> {
if (fragment != null) {
fragment.confirmarExclusaoObjetivo(item);
}
});
}
@Override
public int getItemCount() {
return listaObjetivos.size();
}
static class ObjetivosViewHolder extends RecyclerView.ViewHolder {
CardView cardObjetivo;
TextView tvNome, tvValores, tvPercentagem;
ProgressBar progress;
ImageView btnEliminar, btnEditar, imgIcone;
public ObjetivosViewHolder(@NonNull View itemView) {
super(itemView);
cardObjetivo = itemView.findViewById(R.id.cardObjetivo);
tvNome = itemView.findViewById(R.id.tvNomeObjetivo);
tvValores = itemView.findViewById(R.id.tvValoresObjetivo);
tvPercentagem = itemView.findViewById(R.id.tvPercentagemObjetivo);
progress = itemView.findViewById(R.id.progressObjetivo);
btnEliminar = itemView.findViewById(R.id.btnEliminarObjetivo);
btnEditar = itemView.findViewById(R.id.btnEditarObjetivo);
imgIcone = itemView.findViewById(R.id.imgIconeObjetivo);
}
}
}

View File

@@ -0,0 +1,326 @@
package com.example.finzora;
import android.app.AlertDialog;
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 android.widget.Button;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class ObjetivosFragment extends Fragment {
private RecyclerView recyclerObjetivos;
private ObjetivosAdapter adapter;
private View layoutVazio;
private float saldoAtualCalculado = 0f;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_objetivos, container, false);
recyclerObjetivos = view.findViewById(R.id.recyclerObjetivos);
recyclerObjetivos.setLayoutManager(new LinearLayoutManager(getActivity()));
layoutVazio = view.findViewById(R.id.layoutObjetivosVazios);
FloatingActionButton fabAdicionar = view.findViewById(R.id.fabAdicionarObjetivo);
fabAdicionar.setOnClickListener(v -> mostrarDialogoAdicionar());
return view;
}
@Override
public void onResume() {
super.onResume();
calcularSaldoECarregarObjetivos();
}
// 🧠 PRIMEIRO: Calculamos o Saldo Total (Receitas - Despesas)
private void calcularSaldoECarregarObjetivos() {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
try {
JSONArray jsonArray = new JSONArray(response.body().string());
float receitas = 0, despesas = 0;
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
if (obj.getInt("tipo") == 1) receitas += (float) obj.getDouble("valor");
else despesas += (float) obj.getDouble("valor");
}
saldoAtualCalculado = receitas - despesas;
if(saldoAtualCalculado < 0) saldoAtualCalculado = 0;
carregarObjetivosDoSupabase();
} catch (Exception e) { e.printStackTrace(); }
}
}
});
}
// ☁️ SEGUNDO: Buscar os dados à tabela nova
private void carregarObjetivosDoSupabase() {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/objetivos?user_id=eq." + userId + "&order=created_at.desc")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
try {
List<Objetivo> lista = new ArrayList<>();
JSONArray array = new JSONArray(response.body().string());
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
lista.add(new Objetivo(
obj.getString("id"),
obj.getString("nome_objetivo"),
(float) obj.getDouble("valor_alvo")
));
}
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
adapter = new ObjetivosAdapter(lista, saldoAtualCalculado, ObjetivosFragment.this);
recyclerObjetivos.setAdapter(adapter);
if (lista.isEmpty()) {
recyclerObjetivos.setVisibility(View.GONE);
layoutVazio.setVisibility(View.VISIBLE);
} else {
recyclerObjetivos.setVisibility(View.VISIBLE);
layoutVazio.setVisibility(View.GONE);
}
});
}
} catch (Exception e) { e.printStackTrace(); }
}
}
});
}
// TERCEIRO: O Pop-up (AGORA COM DESIGN PREMIUM!)
private void mostrarDialogoAdicionar() {
View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_novo_objetivo, null);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(view);
AlertDialog dialog = builder.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(0));
}
EditText editNome = view.findViewById(R.id.editNomeObjetivo);
EditText editValor = view.findViewById(R.id.editValorObjetivo);
TextView btnCancelar = view.findViewById(R.id.btnCancelarObjetivo);
Button btnGuardar = view.findViewById(R.id.btnGuardarObjetivo);
btnCancelar.setOnClickListener(v -> dialog.dismiss());
btnGuardar.setOnClickListener(v -> {
String nome = editNome.getText().toString().trim();
String valorStr = editValor.getText().toString().trim();
if (!nome.isEmpty() && !valorStr.isEmpty()) {
guardarNoSupabase(nome, Float.parseFloat(valorStr));
dialog.dismiss();
} else {
Toast.makeText(getActivity(), "Preenche todos os campos!", Toast.LENGTH_SHORT).show();
}
});
dialog.show();
}
private void guardarNoSupabase(String nome, float valorAlvo) {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
try {
JSONObject json = new JSONObject();
json.put("user_id", userId);
json.put("nome_objetivo", nome);
json.put("valor_alvo", valorAlvo);
RequestBody body = RequestBody.create(json.toString(), MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/objetivos")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.addHeader("Content-Type", "application/json")
.addHeader("Prefer", "return=minimal")
.post(body)
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override public void onResponse(@NonNull Call call, @NonNull Response response) {
if (response.isSuccessful() && getActivity() != null) {
getActivity().runOnUiThread(() -> carregarObjetivosDoSupabase());
}
}
});
} catch (Exception e) { e.printStackTrace(); }
}
public void confirmarExclusaoObjetivo(Objetivo obj) {
new AlertDialog.Builder(getActivity())
.setTitle("Eliminar Objetivo")
.setMessage("Queres apagar o objetivo '" + obj.getNome() + "'?")
.setPositiveButton("Sim", (dialog, which) -> {
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/objetivos?id=eq." + obj.getId())
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete()
.build();
new OkHttpClient().newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override public void onResponse(@NonNull Call call, @NonNull Response response) {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> carregarObjetivosDoSupabase());
}
}
});
})
.setNegativeButton("Não", null)
.show();
}
// 🏆 A NOVA JOGADA: FUNÇÃO PARA EDITAR! (Corrigida sem o tvTituloDialogObjetivo)
public void editarObjetivo(Objetivo objetivo) {
if (getActivity() == null) return;
// Reutilizamos o design bonito do dialog de adicionar
View view = LayoutInflater.from(getActivity()).inflate(R.layout.dialog_novo_objetivo, null);
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setView(view);
AlertDialog dialog = builder.create();
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(0));
}
EditText editNome = view.findViewById(R.id.editNomeObjetivo);
EditText editValor = view.findViewById(R.id.editValorObjetivo);
TextView btnCancelar = view.findViewById(R.id.btnCancelarObjetivo);
Button btnGuardar = view.findViewById(R.id.btnGuardarObjetivo);
// Preencher com os dados atuais
editNome.setText(objetivo.getNome());
editValor.setText(String.valueOf(objetivo.getValorAlvo()));
btnGuardar.setText("Atualizar");
btnCancelar.setOnClickListener(v -> dialog.dismiss());
btnGuardar.setOnClickListener(v -> {
String novoNome = editNome.getText().toString().trim();
String valorStr = editValor.getText().toString().trim();
if (!novoNome.isEmpty() && !valorStr.isEmpty()) {
double novoValor = Double.parseDouble(valorStr);
atualizarObjetivoNoSupabase(objetivo.getId(), novoNome, novoValor);
dialog.dismiss();
} else {
Toast.makeText(getActivity(), "Preenche todos os campos!", Toast.LENGTH_SHORT).show();
}
});
dialog.show();
}
// 🚀 ENVIA O UPDATE PARA O SUPABASE
private void atualizarObjetivoNoSupabase(String idObjetivo, String novoNome, double novoValor) {
OkHttpClient client = new OkHttpClient();
String jsonUpdate = "{\"nome_objetivo\":\"" + novoNome + "\", \"valor_alvo\":" + novoValor + "}";
RequestBody body = RequestBody.create(jsonUpdate, MediaType.parse("application/json; charset=utf-8"));
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/objetivos?id=eq." + idObjetivo)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.patch(body) // Usamos PATCH para atualizar
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (getActivity() != null) {
getActivity().runOnUiThread(() ->
Toast.makeText(getActivity(), "Erro de net ao atualizar.", Toast.LENGTH_SHORT).show());
}
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
if (response.isSuccessful()) {
Toast.makeText(getActivity(), "Objetivo atualizado! 🚀", Toast.LENGTH_SHORT).show();
// Recarregar os objetivos na lista!
carregarObjetivosDoSupabase();
} else {
Toast.makeText(getActivity(), "Erro a guardar.", Toast.LENGTH_SHORT).show();
}
});
}
}
});
}
}

View File

@@ -16,7 +16,7 @@ public class OnboardingActivity extends AppCompatActivity {
private OnboardingAdapter onboardingAdapter;
private Button btnProximo;
private TextView btnSaltar; // Atenção: no teu XML o Saltar era um TextView
private TextView btnSaltar;
private TabLayout tabLayoutIndicator;
@Override
@@ -32,21 +32,28 @@ public class OnboardingActivity extends AppCompatActivity {
// --- 2. CRIAR OS SLIDES ---
List<OnboardingItem> lista = new ArrayList<>();
// SLIDE 1: Usamos a variável 'nomeRecuperado' aqui
// SLIDE 1: Boas-vindas
lista.add(new OnboardingItem(
"Olá, " + nomeRecuperado + "! \uD83D\uDC4B",
"Bem-vindo ao Finzora. A tua gestão financeira pessoal, agora com tecnologia de ponta.",
R.drawable.ic_wallet // Confirma se tens este ícone, ou usa ic_launcher_foreground
R.drawable.ic_wallet
));
// SLIDE 2
// SLIDE 2: Transações
lista.add(new OnboardingItem(
"Controlo Total",
"Regista receitas e despesas num piscar de olhos e mantém o teu saldo sempre atualizado.",
R.drawable.ic_chart
));
// SLIDE 3
// ⚠️ SLIDE 3: A NOSSA NOVA CONTRATAÇÃO (OBJETIVOS)
lista.add(new OnboardingItem(
"Atinge os teus Objetivos \uD83C\uDFAF",
"Tem um alvo a atingir? Cria cofres de poupança e vê a magia acontecer com o cálculo automático de progresso!",
R.drawable.ic_lazer // Se tiveres um ícone mais adequado como ic_cofre podes mudar aqui
));
// SLIDE 4: Inteligência Artificial
lista.add(new OnboardingItem(
"Inteligência Artificial",
"Recebe dicas automáticas baseadas nos teus gastos para poupares mais todos os meses.",
@@ -94,7 +101,6 @@ public class OnboardingActivity extends AppCompatActivity {
}
private void finalizarOnboarding() {
// Tem de ir para ProfileActivity.class, e NÃO para MainActivity.class
Intent intent = new Intent(OnboardingActivity.this, ProfileActivity.class);
startActivity(intent);
finish();

View File

@@ -5,22 +5,33 @@ import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;
import java.util.Map;
public class OrcamentoAdapter extends RecyclerView.Adapter<OrcamentoAdapter.OrcamentoViewHolder> {
// Lista de pares: Chave=Categoria, Valor=Limite
private List<Map.Entry<String, Float>> listaOrcamentos;
private DBHelper dbHelper;
public static class OrcamentoItem {
String categoria;
float limite;
float gasto;
public OrcamentoAdapter(List<Map.Entry<String, Float>> lista, DBHelper db) {
public OrcamentoItem(String categoria, float limite, float gasto) {
this.categoria = categoria;
this.limite = limite;
this.gasto = gasto;
}
}
private List<OrcamentoItem> listaOrcamentos;
private OrcamentoFragment fragment;
public OrcamentoAdapter(List<OrcamentoItem> lista, OrcamentoFragment fragment) {
this.listaOrcamentos = lista;
this.dbHelper = db;
this.fragment = fragment;
}
@NonNull
@@ -32,30 +43,42 @@ public class OrcamentoAdapter extends RecyclerView.Adapter<OrcamentoAdapter.Orca
@Override
public void onBindViewHolder(@NonNull OrcamentoViewHolder holder, int position) {
Map.Entry<String, Float> entry = listaOrcamentos.get(position);
String categoria = entry.getKey();
float limite = entry.getValue();
OrcamentoItem item = listaOrcamentos.get(position);
// Calcular quanto já gastou
float gasto = dbHelper.getGastoPorCategoria(categoria);
float restante = limite - gasto;
int percentagem = (int) ((gasto / limite) * 100);
int percentagem = (int) ((item.gasto / item.limite) * 100);
if (percentagem > 100) percentagem = 100;
// Atualizar Textos
holder.tvCategoria.setText(categoria);
holder.tvValores.setText(String.format("€ %.2f / € %.2f", gasto, limite));
holder.tvCategoria.setText(item.categoria);
holder.tvValores.setText(String.format("Gasto: € %.2f / Limite: € %.2f", item.gasto, item.limite));
holder.progress.setProgress(percentagem);
if (restante >= 0) {
holder.tvRestante.setText(String.format("Restam € %.2f (%d%%)", restante, 100 - percentagem));
holder.tvRestante.setTextColor(Color.parseColor("#90A4AE")); // Cinzento
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E676"))); // Verde
// ⚠️ A TÁTICA NOVA: Mudar a imagem conforme o texto!
holder.imgIcone.setImageResource(obterIconeCategoria(item.categoria));
// Dica do Mister: Se quiseres que os ícones fiquem todos verdes para combinar com o design,
// podes descomentar a linha abaixo tirando as duas barras (//):
// holder.imgIcone.setColorFilter(Color.parseColor("#00E676"));
// A TÁTICA DO SEMÁFORO
if (percentagem < 80) {
holder.tvPercentagem.setText(percentagem + "%");
holder.tvPercentagem.setTextColor(Color.parseColor("#00E676"));
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E676")));
} else if (percentagem >= 80 && percentagem < 100) {
holder.tvPercentagem.setText(percentagem + "%");
holder.tvPercentagem.setTextColor(Color.parseColor("#ECC94B"));
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#ECC94B")));
} else {
holder.tvRestante.setText(String.format("Ultrapassado por € %.2f!", Math.abs(restante)));
holder.tvRestante.setTextColor(Color.parseColor("#FF1744")); // Vermelho
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#FF1744"))); // Vermelho
holder.tvPercentagem.setText("100%+");
holder.tvPercentagem.setTextColor(Color.parseColor("#FF1744"));
holder.progress.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#FF1744")));
}
holder.btnEliminar.setOnClickListener(v -> {
if (fragment != null) {
fragment.confirmarExclusaoOrcamento(item);
}
});
}
@Override
@@ -63,16 +86,39 @@ public class OrcamentoAdapter extends RecyclerView.Adapter<OrcamentoAdapter.Orca
return listaOrcamentos.size();
}
// ⚠️ O NOSSO RADAR DE CATEGORIAS COMPLETO!
private int obterIconeCategoria(String cat) {
if (cat == null) return R.drawable.ic_outros;
switch (cat.toLowerCase()) {
case "alimentação": return R.drawable.ic_alimentacao;
case "contas": return R.drawable.ic_contas;
case "transportes": return R.drawable.ic_transportes;
case "compras": return R.drawable.ic_compras;
case "lazer": return R.drawable.ic_lazer;
case "educação": return R.drawable.ic_educacao;
case "saúde": return R.drawable.ic_saude;
default: return R.drawable.ic_outros;
}
}
static class OrcamentoViewHolder extends RecyclerView.ViewHolder {
TextView tvCategoria, tvValores, tvRestante;
TextView tvCategoria, tvValores, tvPercentagem;
ProgressBar progress;
ImageView btnEliminar;
ImageView imgIcone;
public OrcamentoViewHolder(@NonNull View itemView) {
super(itemView);
tvCategoria = itemView.findViewById(R.id.tvCatOrcamento);
tvCategoria = itemView.findViewById(R.id.tvCategoriaOrcamento);
tvValores = itemView.findViewById(R.id.tvValoresOrcamento);
tvRestante = itemView.findViewById(R.id.tvRestante);
tvPercentagem = itemView.findViewById(R.id.tvPercentagemOrcamento);
progress = itemView.findViewById(R.id.progressOrcamento);
btnEliminar = itemView.findViewById(R.id.btnEliminarOrcamento);
// ⚠️ ATENÇÃO CAPITÃO: Verifica se "imgIconeOrcamento" é mesmo o ID da imagem
// no teu ficheiro R.layout.item_orcamento! Se der erro, é só corrigir o nome aqui.
imgIcone = itemView.findViewById(R.id.imgIconeOrcamento);
}
}
}

View File

@@ -1,58 +1,69 @@
package com.example.finzora;
import android.app.AlertDialog;
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 android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
public class OrcamentoFragment extends Fragment {
private Spinner spinnerCategoria;
private TextView txtCategoria;
private String categoriaSelecionada = "";
// ⚠️ COMPRAS ADICIONADAS AO PLANTEL!
private final String[] categorias = {"Alimentação", "Contas", "Transportes", "Compras", "Lazer", "Educação", "Saúde", "Salário", "Mesada", "Prémios", "Outros"};
private EditText editLimite;
private Button btnSalvar;
private RecyclerView recyclerOrcamentos;
private DBHelper dbHelper;
private View layoutEstadoVazio;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
// Carrega o layout XML correto
View view = inflater.inflate(R.layout.fragment_orcamento, container, false);
// Liga os componentes do Java aos IDs do XML
// SE AQUI FICAR VERMELHO, É PORQUE O XML NÃO FOI GUARDADO
spinnerCategoria = view.findViewById(R.id.spinnerOrcamento);
editLimite = view.findViewById(R.id.editLimite);
txtCategoria = view.findViewById(R.id.txtCategoriaOrcamento);
editLimite = view.findViewById(R.id.editLimiteOrcamento);
btnSalvar = view.findViewById(R.id.btnDefinirOrcamento);
recyclerOrcamentos = view.findViewById(R.id.recyclerOrcamentos);
layoutEstadoVazio = view.findViewById(R.id.layoutEstadoVazioOrcamento);
// Configura a lista
recyclerOrcamentos.setLayoutManager(new LinearLayoutManager(getActivity()));
// Inicia a base de dados
dbHelper = new DBHelper(getActivity());
txtCategoria.setOnClickListener(v -> mostrarDialogCategorias());
// Configurações iniciais
configurarSpinner();
btnSalvar.setOnClickListener(v -> salvarOrcamentoNaNuvem());
// Ação do botão
btnSalvar.setOnClickListener(v -> salvarOrcamento());
// Mostrar dados
carregarOrcamentos();
carregarOrcamentosDaNuvem();
return view;
}
@@ -60,39 +71,242 @@ public class OrcamentoFragment extends Fragment {
@Override
public void onResume() {
super.onResume();
carregarOrcamentos();
carregarOrcamentosDaNuvem();
}
// Método corrigido (havia um duplicado antes)
private void configurarSpinner() {
String[] categorias = {"Alimentação", "Transporte", "Salário", "Lazer", "Contas", "Saúde", "Outros"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(getActivity(), R.layout.item_dropdown, categorias);
spinnerCategoria.setAdapter(adapter);
private void mostrarDialogCategorias() {
// 1. Criar um Dialog "em branco"
android.app.Dialog dialog = new android.app.Dialog(getActivity());
// 2. MAGIA: Dizer-lhe para usar o TEU design premium!
dialog.setContentView(R.layout.dialog_categorias);
// 3. Fazer o fundo padrão ficar transparente para o teu CardView arredondado brilhar
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
dialog.getWindow().setLayout(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
}
// 4. Ligar o botão de Cancelar do teu XML
TextView btnCancelar = dialog.findViewById(R.id.btnCancelarCategoria);
btnCancelar.setOnClickListener(v -> dialog.dismiss());
// 5. Injetar as categorias no teu LinearLayout vazio (dentro do ScrollView)
android.widget.LinearLayout container = dialog.findViewById(R.id.containerCategorias);
// 🛑 A CORREÇÃO DA TINTA: Usar a nossa própria cor texto_principal!
int corTexto = androidx.core.content.ContextCompat.getColor(getActivity(), R.color.texto_principal);
android.util.TypedValue clickEffect = new android.util.TypedValue();
getActivity().getTheme().resolveAttribute(android.R.attr.selectableItemBackground, clickEffect, true);
for (String categoria : categorias) {
TextView tvCat = new TextView(getActivity());
tvCat.setText(categoria);
tvCat.setTextSize(18f);
tvCat.setPadding(32, 32, 32, 32); // Espaçamento elegante
tvCat.setTextColor(corTexto);
// Efeito visual seguro sem fechar a app!
tvCat.setBackgroundResource(clickEffect.resourceId);
tvCat.setClickable(true);
// O que acontece quando clicas numa categoria
tvCat.setOnClickListener(v -> {
categoriaSelecionada = categoria;
txtCategoria.setText(categoriaSelecionada);
dialog.dismiss();
});
container.addView(tvCat);
}
dialog.show();
}
private void salvarOrcamento() {
String limiteStr = editLimite.getText().toString();
private void salvarOrcamentoNaNuvem() {
String limiteStr = editLimite.getText().toString().replace(",", ".");
if (categoriaSelecionada.isEmpty()) {
Toast.makeText(getActivity(), "Por favor, escolhe uma categoria!", Toast.LENGTH_SHORT).show();
return;
}
if (limiteStr.isEmpty()) {
editLimite.setError("Define um valor");
return;
}
String categoria = spinnerCategoria.getSelectedItem().toString();
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
float limite = Float.parseFloat(limiteStr);
dbHelper.salvarOrcamento(categoria, limite);
btnSalvar.setEnabled(false);
btnSalvar.setText("A GRAVAR...");
Toast.makeText(getActivity(), "Orçamento definido!", Toast.LENGTH_SHORT).show();
editLimite.setText(""); // Limpar campo
OkHttpClient client = new OkHttpClient();
String json = "{\"user_id\":\"" + userId + "\", \"categoria\":\"" + categoriaSelecionada + "\", \"valor_limite\":" + limite + "}";
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
carregarOrcamentos(); // Atualizar lista
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?on_conflict=user_id,categoria")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.addHeader("Content-Type", "application/json")
.addHeader("Prefer", "resolution=merge-duplicates")
.post(body)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if(getActivity() != null) getActivity().runOnUiThread(() -> {
btnSalvar.setEnabled(true); btnSalvar.setText("Definir Orçamento");
Toast.makeText(getActivity(), "Erro de internet!", Toast.LENGTH_SHORT).show();
});
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if(getActivity() != null) getActivity().runOnUiThread(() -> {
btnSalvar.setEnabled(true); btnSalvar.setText("Definir Orçamento");
if (response.isSuccessful()) {
Toast.makeText(getActivity(), "Orçamento guardado nas nuvens! ☁️", Toast.LENGTH_SHORT).show();
editLimite.setText("");
categoriaSelecionada = "";
txtCategoria.setText("Selecionar Categoria...");
carregarOrcamentosDaNuvem();
}
});
}
});
}
private void carregarOrcamentos() {
Map<String, Float> orcamentosMap = dbHelper.getOrcamentosDefinidos();
List<Map.Entry<String, Float>> lista = new ArrayList<>(orcamentosMap.entrySet());
private void carregarOrcamentosDaNuvem() {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return;
OrcamentoAdapter adapter = new OrcamentoAdapter(lista, dbHelper);
recyclerOrcamentos.setAdapter(adapter);
OkHttpClient client = new OkHttpClient();
Request reqOrcamentos = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?user_id=eq." + userId)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(reqOrcamentos).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (!response.isSuccessful()) return;
try {
String jsonOrcamentos = response.body().string();
JSONArray arrOrcamentos = new JSONArray(jsonOrcamentos);
Map<String, Float> mapaLimites = new HashMap<>();
for (int i = 0; i < arrOrcamentos.length(); i++) {
JSONObject obj = arrOrcamentos.getJSONObject(i);
mapaLimites.put(obj.getString("categoria"), (float) obj.getDouble("valor_limite"));
}
Request reqDespesas = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId + "&tipo=eq.2")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
client.newCall(reqDespesas).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response2) throws IOException {
if (!response2.isSuccessful()) return;
try {
String jsonDespesas = response2.body().string();
JSONArray arrDespesas = new JSONArray(jsonDespesas);
Map<String, Float> mapaGastos = new HashMap<>();
for (int i = 0; i < arrDespesas.length(); i++) {
JSONObject obj = arrDespesas.getJSONObject(i);
String cat = obj.getString("categoria");
float valor = (float) obj.getDouble("valor");
float atual = mapaGastos.containsKey(cat) ? mapaGastos.get(cat) : 0f;
mapaGastos.put(cat, atual + valor);
}
List<OrcamentoAdapter.OrcamentoItem> listaFinal = new ArrayList<>();
for (Map.Entry<String, Float> entry : mapaLimites.entrySet()) {
String categoria = entry.getKey();
float limite = entry.getValue();
float gasto = mapaGastos.containsKey(categoria) ? mapaGastos.get(categoria) : 0f;
listaFinal.add(new OrcamentoAdapter.OrcamentoItem(categoria, limite, gasto));
}
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
OrcamentoAdapter adapter = new OrcamentoAdapter(listaFinal, OrcamentoFragment.this);
recyclerOrcamentos.setAdapter(adapter);
if (listaFinal.isEmpty()) {
recyclerOrcamentos.setVisibility(View.GONE);
layoutEstadoVazio.setVisibility(View.VISIBLE);
} else {
recyclerOrcamentos.setVisibility(View.VISIBLE);
layoutEstadoVazio.setVisibility(View.GONE);
}
});
}
} catch (Exception e) { e.printStackTrace(); }
}
});
} catch (Exception e) { e.printStackTrace(); }
}
});
}
public void confirmarExclusaoOrcamento(OrcamentoAdapter.OrcamentoItem item) {
new AlertDialog.Builder(getActivity())
.setTitle("Eliminar Orçamento")
.setMessage("Queres apagar o limite de orçamento para " + item.categoria + "?")
.setPositiveButton("Sim", (dialog, which) -> {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", "");
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/orcamentos?user_id=eq." + userId + "&categoria=eq." + item.categoria)
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete()
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(getActivity(), "Orçamento apagado!", Toast.LENGTH_SHORT).show();
carregarOrcamentosDaNuvem();
});
}
}
});
})
.setNegativeButton("Não", null)
.show();
}
}

View File

@@ -35,10 +35,7 @@ public class RegisterActivity extends AppCompatActivity {
inicializarComponentes();
// Voltar para o Login
txtLogin.setOnClickListener(v -> finish());
// Clicar em Criar Conta
btnCriarConta.setOnClickListener(v -> validarDados());
}
@@ -78,7 +75,6 @@ public class RegisterActivity extends AppCompatActivity {
private void registarNoSupabase(String nome, String email, String password) {
OkHttpClient client = new OkHttpClient();
// JSON com os dados para o Supabase Auth
String json = "{\"email\":\"" + email + "\", \"password\":\"" + password + "\", \"data\": {\"nome\": \"" + nome + "\"}}";
RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8"));
@@ -93,7 +89,7 @@ public class RegisterActivity extends AppCompatActivity {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
runOnUiThread(() -> {
Toast.makeText(RegisterActivity.this, "Erro de rede! Verifica a internet.", Toast.LENGTH_SHORT).show();
Toast.makeText(RegisterActivity.this, "Erro de rede!", Toast.LENGTH_SHORT).show();
resetBotao();
});
}
@@ -105,34 +101,49 @@ public class RegisterActivity extends AppCompatActivity {
runOnUiThread(() -> {
if (response.isSuccessful()) {
try {
// Extrair o ID do utilizador da resposta JSON
JSONObject jsonResponse = new JSONObject(responseData);
String userId = jsonResponse.getString("id");
// Guardar Nome e ID localmente para usar nas transações
// ⚠️ JOGADA DE MESTRE: Tentar ler o ID de forma mais flexível
String userId = "";
if (jsonResponse.has("user")) {
userId = jsonResponse.getJSONObject("user").getString("id");
} else if (jsonResponse.has("id")) {
userId = jsonResponse.getString("id");
}
// Guardar dados e marcar como logado
SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putString("nome_usuario", nome);
editor.putString("user_id", userId);
editor.putString("email_usuario", email);
editor.putBoolean("is_logged_in", true);
editor.apply();
Toast.makeText(RegisterActivity.this, "Sucesso! Bem-vindo à nuvem.", Toast.LENGTH_SHORT).show();
Toast.makeText(RegisterActivity.this, "Bem-vindo à Finzora! Verifica o teu email! 📩", Toast.LENGTH_LONG).show();
// Avançar para o Onboarding
// Avançar sem olhar para trás
startActivity(new Intent(RegisterActivity.this, OnboardingActivity.class));
finish();
} catch (Exception e) {
Toast.makeText(RegisterActivity.this, "Erro ao ler dados da nuvem", Toast.LENGTH_SHORT).show();
// Se chegámos aqui, a conta foi criada (isSuccessful), mas o JSON era estranho
// Avançamos na mesma para não prender o utilizador
startActivity(new Intent(RegisterActivity.this, OnboardingActivity.class));
finish();
}
} else {
// LER O ERRO REAL DO SUPABASE
try {
JSONObject erroObj = new JSONObject(responseData);
String mensagemErroReal = erroObj.getString("msg");
Toast.makeText(RegisterActivity.this, "ERRO: " + mensagemErroReal, Toast.LENGTH_LONG).show();
String msg = erroObj.optString("msg", "Erro no registo");
if (msg.contains("already registered")) {
Toast.makeText(RegisterActivity.this, "Este email já tem conta!", Toast.LENGTH_LONG).show();
} else {
Toast.makeText(RegisterActivity.this, "Erro: " + msg, Toast.LENGTH_LONG).show();
}
} catch (Exception e) {
Toast.makeText(RegisterActivity.this, "Erro desconhecido: " + responseData, Toast.LENGTH_LONG).show();
Toast.makeText(RegisterActivity.this, "Erro na nuvem", Toast.LENGTH_LONG).show();
}
resetBotao();
}

View File

@@ -1,14 +1,16 @@
package com.example.finzora;
public class Transacao {
private int id;
private String id;
private float valor;
private String categoria;
private int tipo; // 1 = Receita, 2 = Despesa
private String data;
// ⚠️ NOVA JOGADA: Adicionada a descrição!
private String descricao;
// --- CONSTRUTOR 1: Para ler da Base de Dados (TEM ID) ---
public Transacao(int id, float valor, String categoria, int tipo, String data) {
public Transacao(String id, float valor, String categoria, int tipo, String data) {
this.id = id;
this.valor = valor;
this.categoria = categoria;
@@ -24,15 +26,19 @@ public class Transacao {
this.data = data;
}
// --- GETTERS (Para os outros lerem) ---
public int getId() { return id; }
// --- GETTERS ---
public String getId() { return id; }
public float getValor() { return valor; }
public String getCategoria() { return categoria; }
public int getTipo() { return tipo; }
public String getData() { return data; }
public String getDescricao() { return descricao; } // Getter da descrição
// --- SETTERS (Opcional, mas evita erros se algum código antigo os chamar) ---
public void setId(int id) { this.id = id; }
// --- SETTERS ---
public void setId(String id) { this.id = id; }
public void setValor(float valor) { this.valor = valor; }
public void setCategoria(String categoria) { this.categoria = categoria; }
public void setTipo(int tipo) { this.tipo = tipo; }
public void setData(String data) { this.data = data; }
public void setDescricao(String descricao) { this.descricao = descricao; } // Setter da descrição
}

View File

@@ -1,6 +1,5 @@
package com.example.finzora;
import android.content.Context;
import android.graphics.Color;
import android.view.LayoutInflater;
import android.view.View;
@@ -9,71 +8,165 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;
public class TransacoesAdapter extends RecyclerView.Adapter<TransacoesAdapter.MyViewHolder> {
public class TransacoesAdapter extends RecyclerView.Adapter<TransacoesAdapter.TransacaoViewHolder> {
private List<Transacao> listaTransacoes;
private Context context;
private TransacoesFragment fragment;
private List<Transacao> listaOriginal;
private List<Transacao> listaVisivel;
private OnTransacaoClickListener listener;
// O teu construtor mantém-se igual!
public TransacoesAdapter(List<Transacao> lista, Context context, TransacoesFragment fragment) {
this.listaTransacoes = lista;
this.context = context;
this.fragment = fragment;
public interface OnTransacaoClickListener {
void onTransacaoClick(Transacao t);
}
public TransacoesAdapter(List<Transacao> lista, OnTransacaoClickListener listener) {
this.listaOriginal = new ArrayList<>(lista);
this.listaVisivel = lista;
this.listener = listener;
}
@NonNull
@Override
public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View itemLista = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transacao, parent, false);
return new MyViewHolder(itemLista);
public TransacaoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_transacao, parent, false);
return new TransacaoViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
Transacao transacao = listaTransacoes.get(position);
public void onBindViewHolder(@NonNull TransacaoViewHolder holder, int position) {
Transacao t = listaVisivel.get(position);
// --- ATUALIZADO PARA OS NOVOS IDs DO XML ---
holder.tvDescricao.setText(transacao.getCategoria());
holder.tvData.setText(transacao.getData());
// Cores e Ícones
if (transacao.getTipo() == 1) { // Receita
holder.tvValor.setTextColor(Color.parseColor("#388E3C")); // Verde
holder.tvValor.setText("+ € " + String.format("%.2f", transacao.getValor()));
if(holder.imgIcone != null) holder.imgIcone.setImageResource(android.R.drawable.arrow_up_float);
} else { // Despesa
holder.tvValor.setTextColor(Color.parseColor("#D32F2F")); // Vermelho
holder.tvValor.setText("- € " + String.format("%.2f", transacao.getValor()));
if(holder.imgIcone != null) holder.imgIcone.setImageResource(android.R.drawable.arrow_down_float);
String desc = t.getDescricao();
if (desc != null && !desc.trim().isEmpty() && !desc.equalsIgnoreCase("Sem descrição")) {
holder.tvDescricao.setText(desc);
} else {
holder.tvDescricao.setText(t.getCategoria());
}
// O botão de apagar agora chama-se btnEliminar no XML
holder.btnEliminar.setOnClickListener(v -> {
if (fragment != null) fragment.confirmarExclusao(transacao);
});
holder.tvData.setText(t.getData());
if (t.getTipo() == 1) { // Receita
holder.tvValor.setText(String.format("+ %.2f €", t.getValor()));
holder.tvValor.setTextColor(Color.parseColor("#00E676"));
holder.imgIcone.setImageResource(R.drawable.ic_wallet);
} else { // Despesa
holder.tvValor.setText(String.format("- %.2f €", t.getValor()));
holder.tvValor.setTextColor(Color.parseColor("#FF1744"));
holder.imgIcone.setImageResource(obterIconeInteligente(t.getCategoria(), t.getDescricao()));
}
holder.itemView.setOnClickListener(v -> listener.onTransacaoClick(t));
}
@Override
public int getItemCount() {
return listaTransacoes.size();
return listaVisivel.size();
}
public class MyViewHolder extends RecyclerView.ViewHolder {
// --- AQUI ESTÃO OS NOVOS NOMES DO XML ---
TextView tvDescricao, tvValor, tvData;
ImageView imgIcone, btnEliminar;
// 🧠 FUNÇÃO INTELIGENTE PARA REMOVER ACENTOS (ex: transformar "água" em "agua")
private String removerAcentos(String str) {
if (str == null) return "";
CharSequence unaccented = java.text.Normalizer.normalize(str, java.text.Normalizer.Form.NFD);
return unaccented.toString().replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
}
public MyViewHolder(@NonNull View itemView) {
// 🚀 O NOVO MOTOR DE BUSCA TECNOLÓGICO
public void filtrarLista(String textoProcura) {
listaVisivel.clear();
if (textoProcura == null || textoProcura.trim().isEmpty()) {
listaVisivel.addAll(listaOriginal);
} else {
// Limpamos o texto: sem espaços extra, em minúsculas e sem acentos!
String queryLimpa = removerAcentos(textoProcura.toLowerCase().trim());
for (Transacao t : listaOriginal) {
String descricao = removerAcentos(t.getDescricao() != null ? t.getDescricao().toLowerCase() : "");
String categoria = removerAcentos(t.getCategoria() != null ? t.getCategoria().toLowerCase() : "");
// Transforma o valor e a data em texto para também podermos pesquisar por eles!
String valorStr = String.valueOf(t.getValor());
String dataStr = t.getData() != null ? t.getData() : "";
// 1. Pesquisa por Palavras (Ignora letras no meio das palavras)
boolean matchDescricao = descricao.startsWith(queryLimpa) || descricao.contains(" " + queryLimpa);
boolean matchCategoria = categoria.startsWith(queryLimpa) || categoria.contains(" " + queryLimpa);
// 2. Pesquisa de Matemática e Calendário
boolean matchValor = valorStr.contains(queryLimpa);
boolean matchData = dataStr.contains(queryLimpa);
// Se bater certo com a Descrição, Categoria, Valor OU Data... a transação é mostrada!
if (matchDescricao || matchCategoria || matchValor || matchData) {
listaVisivel.add(t);
}
}
}
notifyDataSetChanged();
}
public int obterIconeInteligente(String cat, String desc) {
String frase = (cat + " " + (desc != null ? desc : "")).toLowerCase();
// --- EXCEÇÕES PREMIUM (Ícones Específicos que criaste) ---
if (frase.matches(".*(café|snack).*")) return R.drawable.ic_cafe;
if (frase.matches(".*(supermercado|pingo doce|continente|lidl).*")) return R.drawable.ic_carrinho;
if (frase.matches(".*(renda|casa|condomínio).*")) return R.drawable.ic_casa;
if (frase.matches(".*(internet|meo|nos|vodafone|telemóvel).*")) return R.drawable.ic_telemovel;
if (frase.matches(".*(ginásio|fitness|yoga).*")) return R.drawable.ic_ginasio;
if (frase.matches(".*(compras|roupa|sapatilhas|zara|shein|amazon|aliexpress|worten|fnac|shopping|loja|perfume).*")) {
return R.drawable.ic_compras;
}
// --- GRUPOS GERAIS DA TUA LISTA ---
if (frase.matches(".*(restaurante|almoço|jantar|pizza|burger|sushi).*")) {
return R.drawable.ic_alimentacao;
}
if (frase.matches(".*(luz|edp|água|gás|seguro).*")) {
return R.drawable.ic_contas;
}
if (frase.matches(".*(uber|bolt|gota|gasolina|diesel|repsol|galp|autocarro|comboio|cp|metro|oficina|estacionamento).*")) {
return R.drawable.ic_transportes;
}
if (frase.matches(".*(jogo|steam|ps5|xbox|netflix|disney|spotify|cinema|concerto|bar|festa|estádio|futebol).*")) {
return R.drawable.ic_lazer;
}
if (frase.matches(".*(farmácia|médico|hospital|dentista|exames).*")) {
return R.drawable.ic_saude;
}
if (frase.matches(".*(escola|faculdade|curso|livro|udemy|propina|papelaria).*")) {
return R.drawable.ic_educacao;
}
switch (cat.toLowerCase()) {
case "alimentação": return R.drawable.ic_alimentacao;
case "contas": return R.drawable.ic_contas;
case "transportes": return R.drawable.ic_transportes;
case "compras": return R.drawable.ic_compras;
case "lazer": return R.drawable.ic_lazer;
case "educação": return R.drawable.ic_educacao;
case "saúde": return R.drawable.ic_saude;
case "salário":
case "mesada":
case "prémios": return R.drawable.ic_wallet;
default: return R.drawable.ic_outros;
}
}
static class TransacaoViewHolder extends RecyclerView.ViewHolder {
TextView tvDescricao, tvData, tvValor;
ImageView imgIcone;
public TransacaoViewHolder(@NonNull View itemView) {
super(itemView);
// Agora liga perfeitamente ao layout tech!
tvDescricao = itemView.findViewById(R.id.tvDescricao);
tvData = itemView.findViewById(R.id.tvData);
tvValor = itemView.findViewById(R.id.tvValor);
imgIcone = itemView.findViewById(R.id.imgIcone);
btnEliminar = itemView.findViewById(R.id.btnEliminar);
}
}
}

View File

@@ -2,11 +2,18 @@ package com.example.finzora;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -14,6 +21,8 @@ import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import org.json.JSONArray;
import org.json.JSONObject;
@@ -31,13 +40,30 @@ public class TransacoesFragment extends Fragment {
private RecyclerView recyclerTransacoes;
private TransacoesAdapter adapter;
private View layoutEstadoVazio;
private EditText editPesquisar;
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_transacoes, container, false);
recyclerTransacoes = view.findViewById(R.id.recyclerTransacoes);
recyclerTransacoes.setLayoutManager(new LinearLayoutManager(getActivity()));
layoutEstadoVazio = view.findViewById(R.id.layoutEstadoVazio);
editPesquisar = view.findViewById(R.id.editPesquisar);
editPesquisar.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) {
if (adapter != null) { adapter.filtrarLista(s.toString()); }
}
@Override
public void afterTextChanged(Editable s) {}
});
return view;
}
@@ -47,20 +73,15 @@ public class TransacoesFragment extends Fragment {
carregarDadosDoSupabase();
}
// ====================================================================
// BUSCAR AS TRANSAÇÕES À NUVEM (SUPABASE)
// ====================================================================
public void carregarDadosDoSupabase() {
SharedPreferences prefs = getActivity().getSharedPreferences("DadosUtilizador", Context.MODE_PRIVATE);
String userId = prefs.getString("user_id", null);
if (userId == null) return; // Se não houver utilizador, não faz nada
if (userId == null) return;
OkHttpClient client = new OkHttpClient();
// O URL pede ao Supabase: "Dá-me as transações onde o user_id seja igual ao meu!"
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId + "&order=id.desc")
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?user_id=eq." + userId + "&order=created_at.desc")
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.build();
@@ -69,90 +90,158 @@ public class TransacoesFragment extends Fragment {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (getActivity() != null) {
getActivity().runOnUiThread(() ->
Toast.makeText(getActivity(), "Erro de internet ao carregar transações.", Toast.LENGTH_SHORT).show()
);
getActivity().runOnUiThread(() -> Toast.makeText(getActivity(), "Erro de internet.", Toast.LENGTH_SHORT).show());
}
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (response.isSuccessful()) {
String jsonResposta = response.body().string();
List<Transacao> listaNuvem = new ArrayList<>();
try {
String jsonResposta = response.body().string();
List<Transacao> listaNuvem = new ArrayList<>();
JSONArray jsonArray = new JSONArray(jsonResposta);
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject obj = jsonArray.getJSONObject(i);
// Traduzir do JSON do Supabase para o teu Java
int id = obj.getInt("id");
float valor = (float) obj.getDouble("valor");
String categoria = obj.getString("categoria");
int tipo = obj.getInt("tipo");
String data = obj.getString("data");
String id = obj.optString("id", "");
float valor = (float) obj.optDouble("valor", 0.0);
String categoria = obj.isNull("categoria") ? "Desconhecido" : obj.optString("categoria");
int tipo = obj.optInt("tipo", 1);
String data = obj.isNull("data") ? "" : obj.optString("data");
String descricao = obj.isNull("descricao") ? "Sem descrição" : obj.optString("descricao");
Transacao t = new Transacao(valor, categoria, tipo, data);
t.setId(id); // Guarda o ID verdadeiro da nuvem para podermos apagar depois!
t.setId(id);
t.setDescricao(descricao);
listaNuvem.add(t);
}
// Atualizar o ecrã tem de ser sempre na Thread Principal (runOnUiThread)
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
adapter = new TransacoesAdapter(listaNuvem, getActivity(), TransacoesFragment.this);
adapter = new TransacoesAdapter(listaNuvem, transacao -> abrirCartaoDeslizante(transacao));
recyclerTransacoes.setAdapter(adapter);
if (listaNuvem.isEmpty()) {
recyclerTransacoes.setVisibility(View.GONE);
layoutEstadoVazio.setVisibility(View.VISIBLE);
editPesquisar.setVisibility(View.GONE);
} else {
recyclerTransacoes.setVisibility(View.VISIBLE);
layoutEstadoVazio.setVisibility(View.GONE);
editPesquisar.setVisibility(View.VISIBLE);
}
});
}
} catch (Exception e) {
e.printStackTrace();
}
} catch (Exception e) { e.printStackTrace(); }
}
}
});
}
// ====================================================================
// APAGAR TRANSAÇÃO NA NUVEM
// ====================================================================
private void abrirCartaoDeslizante(Transacao t) {
if (getActivity() == null) return;
BottomSheetDialog dialog = new BottomSheetDialog(getActivity());
View view = getLayoutInflater().inflate(R.layout.bottom_sheet_detalhe, null);
ImageView imgIcone = view.findViewById(R.id.imgDetalheIcone);
TextView tvTitulo = view.findViewById(R.id.tvDetalheTitulo);
TextView tvValor = view.findViewById(R.id.tvDetalheValor);
TextView tvDataHora = view.findViewById(R.id.tvDetalheDataHora);
TextView tvCategoria = view.findViewById(R.id.tvDetalheCategoria);
TextView tvDescricao = view.findViewById(R.id.tvDetalheDescricao);
imgIcone.setImageResource(adapter.obterIconeInteligente(t.getCategoria(), t.getDescricao()));
String desc = t.getDescricao();
if (desc != null && !desc.trim().isEmpty() && !desc.equalsIgnoreCase("Sem descrição") && !desc.equals("null")) {
tvTitulo.setText(desc);
} else {
tvTitulo.setText(t.getCategoria());
}
tvDataHora.setText(t.getData());
tvCategoria.setText(t.getCategoria());
tvDescricao.setText(desc != null && !desc.trim().isEmpty() && !desc.equals("null") ? desc : "Sem descrição");
if (t.getTipo() == 1) {
tvValor.setText(String.format("+ %.2f €", t.getValor()));
tvValor.setTextColor(android.graphics.Color.parseColor("#00E676"));
imgIcone.setColorFilter(android.graphics.Color.parseColor("#00E676"));
} else {
tvValor.setText(String.format("- %.2f €", t.getValor()));
tvValor.setTextColor(android.graphics.Color.parseColor("#FF1744"));
imgIcone.setColorFilter(android.graphics.Color.parseColor("#FF1744"));
}
view.findViewById(R.id.btnFecharDetalhe).setOnClickListener(v -> dialog.dismiss());
view.findViewById(R.id.btnEditarTransacao).setOnClickListener(v -> {
dialog.dismiss();
Intent intent = new Intent(getActivity(), AdicionarTransacaoActivity.class);
intent.putExtra("transacao_id", t.getId());
intent.putExtra("valor", (double) t.getValor());
intent.putExtra("descricao", t.getDescricao());
intent.putExtra("categoria", t.getCategoria());
intent.putExtra("tipo", t.getTipo());
startActivity(intent);
});
view.findViewById(R.id.btnApagarTransacao).setOnClickListener(v -> {
dialog.dismiss();
confirmarExclusao(t);
});
dialog.setContentView(view);
dialog.show();
}
// 🏆 NOVA JOGADA: O Pop-up Premium para Eliminar Transações!
public void confirmarExclusao(Transacao transacao) {
new AlertDialog.Builder(getActivity())
.setTitle("Eliminar Transação")
.setMessage("Apagar " + transacao.getCategoria() + "?")
.setPositiveButton("Sim", (dialog, which) -> {
if (getActivity() == null) return;
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?id=eq." + transacao.getId())
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete() // O comando mágico para apagar!
.build();
android.app.Dialog dialog = new android.app.Dialog(getActivity());
dialog.setContentView(R.layout.dialog_eliminar);
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {}
if (dialog.getWindow() != null) {
dialog.getWindow().setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
dialog.getWindow().setLayout(android.view.ViewGroup.LayoutParams.MATCH_PARENT, android.view.ViewGroup.LayoutParams.WRAP_CONTENT);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(getActivity(), "Eliminado das nuvens!", Toast.LENGTH_SHORT).show();
carregarDadosDoSupabase(); // Recarrega a lista
// Se clicar em cancelar, fecha o pop-up
dialog.findViewById(R.id.btnCancelarEliminar).setOnClickListener(v -> dialog.dismiss());
// Atualiza os cartões na MainActivity
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).atualizarCartoes();
}
});
// Se clicar no botão vermelho "ELIMINAR", faz a magia no Supabase!
dialog.findViewById(R.id.btnConfirmarEliminar).setOnClickListener(v -> {
dialog.dismiss();
OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder()
.url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes?id=eq." + transacao.getId())
.addHeader("apikey", SupabaseConfig.SUPABASE_KEY)
.addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY)
.delete()
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(@NonNull Call call, @NonNull IOException e) {}
@Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException {
if (getActivity() != null) {
getActivity().runOnUiThread(() -> {
Toast.makeText(getActivity(), "Eliminado com sucesso!", Toast.LENGTH_SHORT).show();
carregarDadosDoSupabase();
editPesquisar.setText("");
if (getActivity() instanceof MainActivity) {
((MainActivity) getActivity()).atualizarCartoes();
}
}
});
});
}
}
});
});
})
.setNegativeButton("Não", null)
.show();
dialog.show();
}
}

View File

@@ -18,13 +18,14 @@ public class ViewPagerAdapter extends FragmentStateAdapter {
case 0: return new TransacoesFragment();
case 1: return new OrcamentoFragment();
case 2: return new GraficosFragment();
case 3: return new DicasFragment();
case 3: return new ObjetivosFragment(); // ⚠️ O NOSSO NOVO JOGADOR ESTÁ AQUI
case 4: return new DicasFragment(); // ⚠️ Dicas passou para a posição 4
default: return new TransacoesFragment();
}
}
@Override
public int getItemCount() {
return 4; // Total de 4 abas
return 5; // ⚠️ Total atualizado para 5 abas!
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/bg_dinamico" />
<corners
android:topLeftRadius="24dp"
android:topRightRadius="24dp" />
</shape>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Usamos a mesma tática do fundo adaptável para o claro/escuro -->
<solid android:color="#1A888888" />
<corners android:radius="8dp" />
<!-- Borda fina adaptável -->
<stroke
android:width="1dp"
android:color="?android:attr/textColorTertiary" />
</shape>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="#2D3748" /> <corners android:radius="12dp" /> </shape>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<solid android:color="@color/fundo_cartao" />
<corners android:radius="20dp" />
</shape>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Usamos uma cor adaptável com ligeira transparência em vez de preto fixo -->
<solid android:color="#1A888888" />
<corners android:radius="12dp" />
<padding
android:bottom="8dp"
android:left="12dp"
android:right="12dp"
android:top="8dp" />
</shape>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_focused="true">
<shape android:shape="rectangle">
<solid android:color="#0D1117" />
<stroke android:width="2dp" android:color="@color/tech_accent_cyan" />
<corners android:radius="12dp" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="#0D1117" />
<stroke android:width="1dp" android:color="#30363D" />
<corners android:radius="12dp" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background">
<shape>
<corners android:radius="8dp"/>
<solid android:color="#33A0AEC0"/>
</shape>
</item>
<item android:id="@android:id/progress">
<clip>
<shape>
<corners android:radius="8dp"/>
<solid android:color="@color/tech_accent_cyan"/>
</shape>
</clip>
</item>
</layer-list>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M11,9H9V2H7v7H5V2H3v7c0,2.12 1.66,3.84 3.75,3.97V22h2.5v-9.03C11.34,12.84 13,11.12 13,9V2h-2V9zM16,6v8h2.5v8H21V2c-2.76,0 -5,2.24 -5,4z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M20,3H4v10c0,2.21 1.79,4 4,4h6c2.21,0 4,-1.79 4,-4v-3h2c1.11,0 2,-0.9 2,-2V5C22,3.9 21.11,3 20,3zM20,8h-2V5h2V8zM4,19h16v2H4V19z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M7,18c-1.1,0 -1.99,0.9 -1.99,2S5.9,22 7,22s2,-0.9 2,-2S8.1,18 7,18zM17,18c-1.1,0 -1.99,0.9 -1.99,2s0.89,2 1.99,2s2,-0.9 2,-2S18.1,18 17,18zM8.1,13h7.45c0.75,0 1.41,-0.41 1.75,-1.03l3.58,-6.49c0.22,-0.4 0.22,-0.88 -0.01,-1.28C20.64,3.8 20.21,3.56 19.74,3.56H5.21l-0.94,-2H1v2h2l3.6,7.59l-1.35,2.44C4.52,15.37 5.48,17 7,17h12v-2H7l1.1,-2z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M10,20v-6h4v6h5v-8h3L12,3L2,12h3v8H10z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M18,6h-2c0,-2.21 -1.79,-4 -4,-4S8,3.79 8,6H6C4.9,6 4,6.9 4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8C20,6.9 19.1,6 18,6zM12,4c1.1,0 2,0.9 2,2h-4C10,4.9 10.9,4 12,4zM18,20H6V8h12V20z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M18,17H6v-2h12V17zM18,13H6v-2h12V13zM18,9H6V7h12V9zM3,22l1.5,-1.5L6,22l1.5,-1.5L9,22l1.5,-1.5L12,22l1.5,-1.5L15,22l1.5,-1.5L18,22l1.5,-1.5L21,22V2l-1.5,1.5L18,2l-1.5,1.5L15,2l-1.5,1.5L12,2l-1.5,1.5L9,2L7.5,3.5L6,2L4.5,3.5L3,2V22z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M12,3L1,9l4,2.18v6L12,21l7,-3.82v-6l2.12,-1.15V17h2V9L12,3zM12,18.72l-5,-2.73v-5.09l5,2.73l5,-2.73v5.09L12,18.72zM12,10.28L5.34,6.64L12,3l6.66,3.64L12,10.28z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M20.57,14.86L22,13.43L20.57,12L17,15.57L8.43,7L12,3.43L10.57,2L9.14,3.43L7.71,2L5.57,4.14L4.14,2.71L2.71,4.14L4.14,5.57L2,7.71L3.43,9.14L2,10.57L3.43,12L7,8.43L15.57,17L12,20.57L13.43,22L14.86,20.57L16.29,22L18.43,19.86L19.86,21.29L21.29,19.86L19.86,18.43L22,16.29L20.57,14.86z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M21.58,16.09l-1.09,-7.66C20.18,6.27 18.4,5 16.25,5H7.75C5.6,5 3.82,6.27 3.51,8.43l-1.09,7.66C2.2,17.63 3.39,19 4.94,19c0.68,0 1.32,-0.27 1.8,-0.75L9,16h6l2.25,2.25c0.48,0.48 1.13,0.75 1.8,0.75C20.61,19 21.8,17.63 21.58,16.09zM19.59,16.16l-2.02,-2.02C17.18,13.76 16.65,13.5 16.1,13.5H7.9c-0.55,0 -1.08,0.26 -1.47,0.64L4.41,16.16C4.16,16.41 3.75,16.28 3.84,15.93l1.09,-7.66C5.09,7.18 5.98,6.5 7.06,6.5h9.88c1.08,0 1.97,0.68 2.14,1.77l1.09,7.66C20.25,16.28 19.84,16.41 19.59,16.16zM9,11.5c0.83,0 1.5,-0.67 1.5,-1.5S9.83,8.5 9,8.5S7.5,9.17 7.5,10S8.17,11.5 9,11.5zM15,11.5c0.83,0 1.5,-0.67 1.5,-1.5S15.83,8.5 15,8.5S13.5,9.17 13.5,10S14.17,11.5 15,11.5z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M12,2l-5.5,9h11L12,2zM12,5.84L13.93,9h-3.87L12,5.84zM3,13.5h6v6H3V13.5zM5,15.5v2h2v-2H5zM17.5,13c-1.93,0 -3.5,1.57 -3.5,3.5s1.57,3.5 3.5,3.5s3.5,-1.57 3.5,-3.5S19.43,13 17.5,13zM17.5,18c-0.83,0 -1.5,-0.67 -1.5,-1.5s0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5S18.33,18 17.5,18z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M11.8,10.9c-2.27,-0.59 -3,-1.2 -3,-2.15 0,-1.09 1.01,-1.85 2.7,-1.85 1.78,0 2.44,0.85 2.5,2.1h2.21c-0.07,-1.72 -1.12,-3.3 -3.21,-3.81V3h-3v2.16c-1.94,0.42 -3.5,1.68 -3.5,3.61 0,2.31 1.91,3.46 4.7,4.13 2.5,0.6 3,1.48 3,2.41 0,0.69 -0.49,1.79 -2.7,1.79 -2.06,0 -2.87,-0.92 -2.98,-2.1h-2.2c0.12,2.19 1.76,3.42 3.68,3.83V21h3v-2.15c1.95,-0.37 3.5,-1.5 3.5,-3.55 0,-2.84 -2.43,-3.81 -4.7,-4.4z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M19,3H5C3.9,3 3.01,3.9 3.01,5L3,19c0,1.1 0.89,2 1.99,2H19c1.1,0 2,-0.9 2,-2V5C21,3.9 20.1,3 19,3zM19,19H5V5h14V19zM10.5,17h3v-3.5H17v-3h-3.5V7h-3v3.5H7v3h3.5V17z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M17,1.01L7,1C5.9,1 5,1.9 5,3v18c0,1.1 0.9,2 2,2h10c1.1,0 2,-0.9 2,-2V3C19,1.9 18.1,1.01 17,1.01zM17,19H7V5h10V19z"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#FFFFFF" android:pathData="M18.92,6.01C18.72,5.42 18.16,5 17.5,5h-11C5.84,5 5.28,5.42 5.08,6.01L3,12v8c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-1h12v1c0,0.55 0.45,1 1,1h1c0.55,0 1,-0.45 1,-1v-8L18.92,6.01zM6.85,7h10.29l1.08,3.11H5.77L6.85,7zM19,17H5v-5h14V17zM7.5,16c0.83,0 1.5,-0.67 1.5,-1.5S8.33,13 7.5,13S6,13.67 6,14.5S6.67,16 7.5,16zM16.5,16c0.83,0 1.5,-0.67 1.5,-1.5s-0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5S15.67,16 16.5,16z"/>
</vector>

View File

@@ -4,7 +4,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/bg_tech_gradient"
android:background="@color/bg_dinamico"
android:padding="24dp">
<ImageView
@@ -12,7 +12,7 @@
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
app:tint="@color/white"
app:tint="@color/texto_dinamico"
android:layout_marginBottom="24dp" />
<TextView
@@ -21,7 +21,7 @@
android:text="Nova Transação"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="@color/white"
android:textColor="@color/texto_dinamico"
android:layout_marginBottom="32dp"/>
<EditText
@@ -30,7 +30,7 @@
android:layout_height="wrap_content"
android:hint="Valor (€)"
android:textColorHint="#B0BEC5"
android:textColor="@color/white"
android:textColor="@color/texto_dinamico"
android:inputType="numberDecimal"
android:backgroundTint="@color/tech_accent_cyan"
android:textSize="20sp"
@@ -40,22 +40,51 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Categoria"
android:textColor="#B0BEC5"
android:textColor="@color/texto_dinamico"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
<Spinner
android:id="@+id/spinnerCategoria"
<TextView
android:id="@+id/txtCategoriaTransacao"
android:layout_width="match_parent"
android:layout_height="50dp"
android:text="Selecionar Categoria..."
android:textColor="@color/texto_dinamico"
android:textSize="16sp"
android:gravity="center_vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:background="@drawable/bg_card_tech"
android:layout_marginBottom="40dp"/> <Button
android:id="@+id/btnGuardar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Guardar Transação"
android:textColor="@color/black"
android:padding="12dp"
android:textStyle="bold"
app:backgroundTint="@color/tech_accent_cyan"/>
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Descrição (Opcional)"
android:textColor="@color/texto_dinamico"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
<EditText
android:id="@+id/editDescricaoTransacao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/bg_card_tech"
android:textColor="@color/texto_dinamico"
android:padding="16dp"
android:hint="Ex: Jantar, Uber, Conta da luz..."
android:textColorHint="?android:attr/textColorSecondary"
android:inputType="textCapSentences"
android:layout_marginBottom="40dp"/>
<Button
android:id="@+id/btnGuardar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Guardar Transação"
android:textColor="@color/black"
android:padding="12dp"
android:textStyle="bold"
app:backgroundTint="@color/tech_accent_cyan"/>
</LinearLayout>

View File

@@ -5,32 +5,34 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:background="@color/fundo_app"> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="32dp">
android:background="@color/fundo_app">
<TextView
android:id="@+id/btnVoltarDefinicoes"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="←"
android:textColor="@color/texto_principal"
android:textSize="32sp"
android:textStyle="bold"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="32dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Definições"
android:textColor="@color/texto_principal"
android:textSize="28sp"
android:textStyle="bold"/>
</LinearLayout>
<TextView
android:id="@+id/btnVoltarDefinicoes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="←"
android:textColor="@color/texto_principal"
android:textSize="32sp"
android:textStyle="bold"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Definições"
android:textColor="@color/texto_principal"
android:textSize="28sp"
android:textStyle="bold"/>
</LinearLayout>
<TextView
android:id="@+id/btnEditarPerfil"
@@ -55,6 +57,17 @@
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/linha_separadora" />
<Switch
android:id="@+id/switchBiometria"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Bloqueio por Biometria"
android:textColor="@color/texto_principal"
android:textSize="18sp"
android:paddingVertical="16dp" />
<View android:layout_width="match_parent" android:layout_height="1dp" android:background="@color/linha_separadora" />
<Switch
android:id="@+id/switchNotificacoes"
android:layout_width="match_parent"
@@ -87,4 +100,18 @@
android:textStyle="bold"
android:padding="16dp"
app:cornerRadius="8dp"
android:backgroundTint="#FF1744" /> </LinearLayout>
android:backgroundTint="#424242" />
<Button
android:id="@+id/btnEliminarConta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Apagar Conta e Dados"
android:textColor="#FFFFFF"
android:textStyle="bold"
android:padding="16dp"
android:layout_marginTop="12dp"
app:cornerRadius="8dp"
android:backgroundTint="#FF1744" />
</LinearLayout>

View File

@@ -5,48 +5,50 @@
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="24dp"
android:background="#1A202C"> <LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="40dp">
android:background="@color/bg_dinamico">
<TextView
android:id="@+id/btnVoltarEditarPerfil"
android:layout_width="wrap_content"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="←"
android:textColor="#FFFFFF"
android:textSize="32sp"
android:textStyle="bold"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="40dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Editar Perfil"
android:textColor="#FFFFFF"
android:textSize="28sp"
android:textStyle="bold"/>
</LinearLayout>
<TextView
android:id="@+id/btnVoltarEditarPerfil"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="←"
android:textColor="@color/texto_dinamico"
android:textSize="32sp"
android:textStyle="bold"
android:paddingEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Editar Perfil"
android:textColor="@color/texto_dinamico"
android:textSize="28sp"
android:textStyle="bold"/>
</LinearLayout>
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:id="@+id/imgFotoPerfil"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_circle_icon"
android:src="@android:drawable/ic_menu_camera"
app:tint="#FFFFFF"
android:padding="24dp"
android:scaleType="centerCrop"
android:layout_marginBottom="32dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Nome"
android:textColor="#B0BEC5"
android:textColor="@color/texto_dinamico"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
@@ -65,7 +67,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email"
android:textColor="#B0BEC5"
android:textColor="@color/texto_dinamico"
android:textSize="14sp"
android:layout_marginBottom="8dp"/>
@@ -90,4 +92,5 @@
android:textStyle="bold"
android:padding="16dp"
app:cornerRadius="8dp"
android:backgroundTint="#00E676" /> </LinearLayout>
android:backgroundTint="#00E676" />
</LinearLayout>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:background="@color/bg_dinamico"
android:padding="32dp">
<ImageView
android:layout_width="120dp"
android:layout_height="120dp"
android:src="@drawable/ic_carteira_tech"
android:layout_marginBottom="24dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Finzora"
android:textColor="@color/texto_principal"
android:textSize="32sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="O teu gestor financeiro seguro."
android:textColor="?android:attr/textColorSecondary"
android:textSize="16sp"
android:layout_marginBottom="48dp"/>
<Button
android:id="@+id/btnDesbloquearApp"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="Desbloquear"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#1A202C"
android:backgroundTint="#00E676"
app:cornerRadius="12dp"/>
</LinearLayout>

View File

@@ -18,14 +18,15 @@
<ImageView
android:id="@+id/imgLogo"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_width="45dp"
android:layout_height="45dp"
android:layout_alignParentStart="true"
android:layout_centerVertical="true"
android:background="@drawable/bg_circle_icon"
android:padding="8dp"
android:src="@android:drawable/ic_menu_gallery"
app:tint="@color/white" />
app:tint="@color/white"
android:scaleType="centerCrop" />
<LinearLayout
android:layout_width="wrap_content"
@@ -52,6 +53,30 @@
android:textSize="14sp"/>
</LinearLayout>
<ImageView
android:id="@+id/btnAbrirDefinicoes"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_toStartOf="@id/btnExportarPDF"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@drawable/ic_settings_pap"
app:tint="@color/tech_accent_cyan"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Definições" />
<ImageView
android:id="@+id/btnExportarPDF"
android:layout_width="35dp"
android:layout_height="35dp"
android:layout_toStartOf="@id/btnSair"
android:layout_centerVertical="true"
android:layout_marginEnd="15dp"
android:src="@android:drawable/ic_menu_save"
app:tint="@color/tech_accent_cyan"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Exportar Relatório" />
<Button
android:id="@+id/btnSair"
android:layout_width="wrap_content"
@@ -110,16 +135,4 @@
app:tint="@color/black"
app:elevation="6dp"/>
<ImageView
android:id="@+id/btnAbrirDefinicoes"
android:layout_width="56dp"
android:layout_height="56dp"
android:layout_gravity="bottom|start"
android:layout_margin="24dp"
android:padding="12dp"
android:src="@drawable/ic_settings_pap"
app:tint="@color/texto_principal"
android:elevation="6dp"
android:background="?attr/selectableItemBackgroundBorderless"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/fundo_app"
android:padding="24dp"
tools:context=".NovaPasswordActivity">
<ImageView
android:id="@+id/imgTechLock"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="40dp"
android:padding="8dp"
android:src="@android:drawable/ic_secure"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/tech_accent_cyan" />
<TextView
android:id="@+id/tvTechTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="NOVA PALAVRA-PASSE"
android:textColor="@color/white"
android:textSize="26sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/imgTechLock" />
<TextView
android:id="@+id/tvTechSubtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Verificação de segurança ativa.\nDefine a tua nova senha de acesso."
android:textAlignment="center"
android:textColor="#B0BEC5"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTechTitle" />
<LinearLayout
android:id="@+id/linearLayoutInputs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tvTechSubtitle">
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editNovaPass"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/bg_tech_input"
android:hint="Nova palavra-passe"
android:textColorHint="#546E7A"
android:inputType="textPassword"
android:padding="16dp"
android:textColor="@color/white"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:hintEnabled="false">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/editConfirmaNovaPass"
android:layout_width="match_parent"
android:layout_height="60dp"
android:background="@drawable/bg_tech_input"
android:hint="Confirmar palavra-passe"
android:textColorHint="#546E7A"
android:inputType="textPassword"
android:padding="16dp"
android:textColor="@color/white"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<Button
android:id="@+id/btnGuardarNovaPass"
android:layout_width="0dp"
android:layout_height="60dp"
android:layout_marginTop="16dp"
android:backgroundTint="@color/tech_accent_cyan"
android:text="GUARDAR PALAVRA-PASSE"
android:textColor="@color/black"
android:textSize="16sp"
android:textStyle="bold"
app:cornerRadius="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/linearLayoutInputs" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@color/bg_dinamico"
android:padding="24dp">
<ImageView
android:id="@+id/btnFecharDetalhe"
android:layout_width="32dp"
android:layout_height="32dp"
android:src="@android:drawable/ic_menu_close_clear_cancel"
app:tint="@color/texto_dinamico"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_marginBottom="16dp"/>
<ImageView
android:id="@+id/imgDetalheIcone"
android:layout_width="64dp"
android:layout_height="64dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/bg_circle_icon"
android:src="@android:drawable/ic_menu_agenda"
app:tint="#FFFFFF"
android:padding="16dp"
android:layout_marginBottom="12dp"/>
<TextView
android:id="@+id/tvDetalheTitulo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="Título da Transação"
android:textColor="@color/texto_dinamico"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvDetalheValor"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="- 13,38 €"
android:textColor="@color/texto_dinamico"
android:textSize="36sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<TextView
android:id="@+id/tvDetalheDataHora"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="12/04, 10:14"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_marginBottom="32dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="#2D3748"
android:padding="16dp"
android:layout_marginBottom="24dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Categoria"
android:textColor="#A0AEC0"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvDetalheCategoria"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Entretenimento"
android:textColor="#00E676"
android:textStyle="bold"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Descrição"
android:textColor="#A0AEC0"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvDetalheDescricao"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Subscrição Mensal"
android:textColor="#FFFFFF"
android:textAlignment="viewEnd"
android:maxWidth="200dp"/>
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btnEditarTransacao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Editar Transação"
android:textColor="#FFFFFF"
android:backgroundTint="#00B8D4"
android:textStyle="bold"
android:padding="12dp"
app:cornerRadius="8dp"
android:layout_marginBottom="8dp"/>
<Button
android:id="@+id/btnApagarTransacao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Eliminar Transação"
android:textColor="#FFFFFF"
android:backgroundTint="#F56565"
android:textStyle="bold"
android:padding="12dp"
app:cornerRadius="8dp"/>
</LinearLayout>

View File

@@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardBackgroundColor="@color/fundo_cartao"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="24dp"
android:paddingBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="24dp"
android:layout_marginBottom="16dp"
android:text="Selecionar Categoria"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
android:textStyle="bold" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="350dp"
android:scrollbars="none">
<LinearLayout
android:id="@+id/containerCategorias"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp" />
</ScrollView>
<TextView
android:id="@+id/btnCancelarCategoria"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="24dp"
android:layout_marginTop="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="end"
android:padding="12dp"
android:text="CANCELAR"
android:textColor="#F56565"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -24,9 +24,14 @@
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Contactar Suporte" android:textSize="18sp" android:textStyle="bold" android:textColor="#1A202C"/>
</LinearLayout>
<EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="O seu email" android:background="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp"/>
<EditText android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Assunto" android:background="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp"/>
<EditText android:layout_width="match_parent" android:layout_height="120dp" android:hint="Descreva o seu problema..." android:background="#F7FAFC" android:padding="16dp" android:gravity="top|start" android:layout_marginBottom="24dp"/>
<EditText android:id="@+id/editEmailSuporte"
android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="O seu email" android:background="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp"/>
<EditText android:id="@+id/editAssunto"
android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Assunto" android:background="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp"/>
<EditText android:id="@+id/editProblema"
android:layout_width="match_parent" android:layout_height="120dp" android:hint="Descreva o seu problema..." android:background="#F7FAFC" android:padding="16dp" android:gravity="top|start" android:layout_marginBottom="24dp"/>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:gravity="end">
<Button android:id="@+id/btnCancelarContacto" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Cancelar" android:backgroundTint="#FFFFFF" android:textColor="#2D3748" android:layout_marginEnd="8dp"/>

View File

@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Contactos Finzora"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:text="Precisas de ajuda urgente? Fala connosco diretamente."
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<!-- 📧 Zona do Email -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📧"
android:textSize="28sp"
android:layout_marginEnd="16dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Email de Suporte"
android:textSize="15sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="suporte@finzora.pt"
android:textSize="15sp"
android:textColor="#00E676"/>
</LinearLayout>
</LinearLayout>
<!-- 📞 Zona do Telefone -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="📞"
android:textSize="28sp"
android:layout_marginEnd="16dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Linha de Apoio"
android:textSize="15sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="+351 800 123 456"
android:textSize="15sp"
android:textColor="#00E676"/>
</LinearLayout>
</LinearLayout>
<!-- 🕒 Zona do Horário (com fundo suave) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="24dp"
android:background="#1A888888"
android:padding="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🕒"
android:layout_marginEnd="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dias úteis: 09:00 - 18:00"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"/>
</LinearLayout>
<!-- Botão Fechar -->
<TextView
android:id="@+id/btnFecharContactos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="VOLTAR"
android:gravity="end"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="8dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp"
android:gravity="center">
<!-- Ícone de Lixo gigante no topo -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="🗑️"
android:textSize="40sp"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Eliminar Transação"
android:textSize="20sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="24dp"
android:gravity="center"
android:text="Tens a certeza que queres apagar esta transação? Esta ação não pode ser desfeita."
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="end">
<TextView
android:id="@+id/btnCancelarEliminar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="CANCELAR"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorSecondary"
android:padding="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:layout_marginEnd="16dp"/>
<TextView
android:id="@+id/btnConfirmarEliminar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ELIMINAR"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="#FF1744"
android:padding="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"/>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="📥 Exportar Dados"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
android:textStyle="bold" />
<androidx.cardview.widget.CardView
android:id="@+id/btnOpcaoPDF"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="#1A888888"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="📄"
android:textSize="28sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Relatório PDF"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ideal para ler e imprimir"
android:textColor="?android:attr/textColorSecondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:id="@+id/btnOpcaoExcel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="#1A888888"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:text="📊"
android:textSize="28sp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ficheiro Excel (.CSV)"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ideal para tabelas e gráficos"
android:textColor="?android:attr/textColorSecondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/btnCancelarExportacao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="12dp"
android:text="CANCELAR"
android:textColor="#F56565"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -42,6 +42,12 @@
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Aceda ao separador 'Gráficos' para ver representações visuais dos seus gastos organizados por categoria e período." android:textColor="#718096" android:textSize="13sp"/>
</LinearLayout>
<!-- ⚠️ A NOSSA NOVA FAQ SOBRE OBJETIVOS -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@drawable/bg_tech_gradient" android:backgroundTint="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Como funcionam os Objetivos de Poupança?" android:textStyle="bold" android:textColor="#2D3748" android:layout_marginBottom="8dp"/>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="No separador 'Objetivos', clique no botão azul para criar um alvo (ex: Comprar PS5). A barra de progresso preenche-se automaticamente com base no seu Saldo Total disponível (Receitas - Despesas)." android:textColor="#718096" android:textSize="13sp"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@drawable/bg_tech_gradient" android:backgroundTint="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="O que são as dicas financeiras?" android:textStyle="bold" android:textColor="#2D3748" android:layout_marginBottom="8dp"/>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="No separador 'Dicas', encontrará recomendações personalizadas baseadas nos seus padrões de gastos para melhorar a sua saúde financeira." android:textColor="#718096" android:textSize="13sp"/>

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="20dp"
android:text="🎯 Novo Objetivo"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
android:textStyle="bold" />
<EditText
android:id="@+id/editNomeObjetivo"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="16dp"
android:hint="Ex: Comprar PS5"
android:textColor="?android:attr/textColorPrimary"
android:textColorHint="?android:attr/textColorSecondary" />
<EditText
android:id="@+id/editValorObjetivo"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_marginBottom="24dp"
android:hint="Valor Alvo (Ex: 500.00)"
android:inputType="numberDecimal"
android:textColor="?android:attr/textColorPrimary"
android:textColorHint="?android:attr/textColorSecondary" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal">
<TextView
android:id="@+id/btnCancelarObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="12dp"
android:text="CANCELAR"
android:textColor="?android:attr/textColorSecondary"
android:textStyle="bold" />
<Button
android:id="@+id/btnGuardarObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:backgroundTint="#00B8D4"
android:text="GUARDAR"
android:textColor="#FFFFFF"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -0,0 +1,161 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="24dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:text="Tipo de Transação"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
android:textStyle="bold" />
<!-- CARTÃO RECEITA -->
<androidx.cardview.widget.CardView
android:id="@+id/btnTipoReceita"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="#1A888888"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<!-- Ícone Verde Redondo -->
<androidx.cardview.widget.CardView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="#2F855A"
app:cardCornerRadius="20dp"
app:cardElevation="0dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="+"
android:textColor="#FFFFFF"
android:textSize="24sp"
android:textStyle="bold" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Receita"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Entrada de dinheiro (Ex: Salário)"
android:textColor="?android:attr/textColorSecondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- CARTÃO DESPESA -->
<androidx.cardview.widget.CardView
android:id="@+id/btnTipoDespesa"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:clickable="true"
android:focusable="true"
android:foreground="?attr/selectableItemBackground"
app:cardBackgroundColor="#1A888888"
app:cardCornerRadius="12dp"
app:cardElevation="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="16dp">
<!-- Ícone Vermelho Redondo -->
<androidx.cardview.widget.CardView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginEnd="16dp"
app:cardBackgroundColor="#C53030"
app:cardCornerRadius="20dp"
app:cardElevation="0dp">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="-"
android:textColor="#FFFFFF"
android:textSize="26sp"
android:textStyle="bold" />
</androidx.cardview.widget.CardView>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Despesa"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Saída de dinheiro (Ex: Compras)"
android:textColor="?android:attr/textColorSecondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:id="@+id/btnCancelarTipo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:padding="12dp"
android:text="CANCELAR"
android:textColor="#F56565"
android:textStyle="bold" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -42,8 +42,14 @@
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Use os gráficos para visualizar para onde vai o seu dinheiro." android:textColor="#718096" android:textSize="13sp"/>
</LinearLayout>
<!-- ⚠️ A NOSSA NOVA ABA AQUI COMO PASSO 4 -->
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@drawable/bg_tech_gradient" android:backgroundTint="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="4. Siga as dicas personalizadas" android:textStyle="bold" android:textColor="#2D3748" android:layout_marginBottom="8dp"/>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="4. Crie Objetivos de Poupança" android:textStyle="bold" android:textColor="#2D3748" android:layout_marginBottom="8dp"/>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Tem um alvo a atingir? Registe-o no separador 'Objetivos'. A Finzora calcula automaticamente o seu progresso com base no seu Saldo Total!" android:textColor="#718096" android:textSize="13sp"/>
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="@drawable/bg_tech_gradient" android:backgroundTint="#F7FAFC" android:padding="16dp" android:layout_marginBottom="12dp">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="5. Siga as dicas personalizadas" android:textStyle="bold" android:textColor="#2D3748" android:layout_marginBottom="8dp"/>
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="A Finzora analisa os seus padrões e fornece recomendações úteis." android:textColor="#718096" android:textSize="13sp"/>
</LinearLayout>

View File

@@ -1,395 +1,313 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_tech_gradient"
android:fillViewport="true">
android:background="?android:attr/windowBackground">
<LinearLayout
android:id="@+id/layoutEstadoVazioDicas"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="16dp">
android:visibility="gone">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Resumo da Sua Saúde Financeira"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
android:text="📊"
android:textSize="60sp" />
<androidx.cardview.widget.CardView
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Sem dados para analisar"
android:textColor="?android:attr/textColorPrimary"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center"
android:paddingHorizontal="32dp"
android:text="Adiciona transações para receberes conselhos da nossa Inteligência Artificial."
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp" />
</LinearLayout>
<ScrollView
android:id="@+id/layoutConteudoDicas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:padding="16dp"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="4dp">
android:orientation="vertical">
<LinearLayout
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="20dp"
app:cardElevation="8dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
android:orientation="vertical"
android:padding="20dp">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Taxa de Poupança"
android:textColor="@color/white"
android:textStyle="bold"/>
<TextView
android:id="@+id/tvTaxaPoupanca"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="0.0%"
android:textColor="#00E676"
android:textStyle="bold"/>
</LinearLayout>
android:text="🤖 Finzora AI Coach"
android:textColor="@color/tech_accent_cyan"
android:textSize="18sp"
android:textStyle="bold" />
<ProgressBar
android:id="@+id/progressPoupanca"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:max="100"
android:progress="0"
android:progressTint="#00E676"
android:layout_marginBottom="16dp"/>
<TextView
android:id="@+id/tvRespostaAI"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="Olá! Já analisei os teus dados. Pergunta-me qualquer coisa sobre como poupar ou investir."
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/bg_search_bar"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingHorizontal="12dp"
android:paddingVertical="4dp">
<EditText
android:id="@+id/editPerguntaAI"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@null"
android:hint="Pede uma dica tática..."
android:textColor="?android:attr/textColorPrimary"
android:textColorHint="?android:attr/textColorSecondary"
android:textSize="14sp" />
<ImageButton
android:id="@+id/btnEnviarAI"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_send"
app:tint="@color/tech_accent_cyan" />
</LinearLayout>
<ProgressBar
android:id="@+id/pbCarregandoAI"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="8dp"
android:visibility="gone" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:background="#1AFFFFFF"
android:padding="12dp"
android:layout_marginEnd="8dp">
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Taxa de Poupança"
android:textColor="?android:attr/textColorPrimary"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTaxaPoupanca"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Receitas"
android:textColor="#B0BEC5"
android:textSize="12sp"/>
android:text="0%"
android:textColor="#00E676"
android:textStyle="bold" />
</LinearLayout>
<ProgressBar
android:id="@+id/progressPoupanca"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:layout_marginTop="8dp"
android:max="100"
android:progress="0"
android:progressTint="#00E676" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvDicasReceitas"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="€ 0.00"
android:textColor="#00E676"
android:textStyle="bold"
android:textSize="16sp"/>
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:background="#1AFFFFFF"
android:padding="12dp"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Despesas"
android:textColor="#B0BEC5"
android:textSize="12sp"/>
android:textStyle="bold" />
<TextView
android:id="@+id/tvDicasDespesas"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="€ 0.00"
android:textColor="#FF1744"
android:textStyle="bold"
android:textSize="16sp"/>
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dicas Personalizadas"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<androidx.cardview.widget.CardView
android:id="@+id/cardDica1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="2dp">
<LinearLayout
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloDica1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A calcular..."
android:textColor="#00E676"
android:textStyle="bold"
android:textSize="16sp"/>
<TextView
android:id="@+id/tvDescDica1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="..."
android:textColor="#B0BEC5"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<androidx.cardview.widget.CardView
android:id="@+id/cardDica2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="2dp">
<LinearLayout
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloDica1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A Regra 50/30/20 ⚖️"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDescDica1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="A calcular a tua distribuição de riqueza..."
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloDica2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="A analisar gastos..."
android:textColor="#FF1744"
android:textStyle="bold"
android:textSize="16sp"/>
<TextView
android:id="@+id/tvDescDica2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="..."
android:textColor="#B0BEC5"
android:layout_marginTop="4dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
android:layout_marginBottom="12dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Distribuição de Gastos"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="2dp">
<TextView
android:id="@+id/tvTituloDica2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Radar de Orçamentos 🎯"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDescDica2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="A verificar limites..."
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTituloDica3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ritmo de Gastos 🏃‍♂️"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDescDica3"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:text="A calcular a tua velocidade de gastos..."
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:text="Top Despesas"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:textStyle="bold" />
<LinearLayout
android:id="@+id/layoutDistribuicao"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
</LinearLayout>
</androidx.cardview.widget.CardView>
android:orientation="vertical" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:layout_marginBottom="12dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:src="@drawable/ic_idea"
app:tint="@color/white"
android:layout_marginEnd="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dicas Rápidas de Economia"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"/>
</LinearLayout>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="32dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/checkbox_on_background"
app:tint="#00E676"
android:layout_marginTop="2dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Prepare refeições em casa"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Pode poupar até €200/mês reduzindo refeições fora"
android:textColor="#B0BEC5"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/checkbox_on_background"
app:tint="#00E676"
android:layout_marginTop="2dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Compare preços antes de comprar"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Use apps de comparação para encontrar melhores ofertas"
android:textColor="#B0BEC5"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="16dp">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/checkbox_on_background"
app:tint="#00E676"
android:layout_marginTop="2dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Cancele subscrições não utilizadas"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Reveja streamings, ginásios e apps que paga mas não usa"
android:textColor="#B0BEC5"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
</LinearLayout>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/checkbox_on_background"
app:tint="#00E676"
android:layout_marginTop="2dp"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginStart="12dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Use transportes públicos"
android:textColor="@color/white"
android:textStyle="bold"
android:textSize="14sp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Economize em combustível, estacionamento e manutenção"
android:textColor="#B0BEC5"
android:textSize="12sp"
android:layout_marginTop="2dp"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
</ScrollView>
</FrameLayout>

View File

@@ -1,88 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_tech_gradient"
android:fillViewport="true">
android:background="@color/bg_dinamico">
<ScrollView
android:id="@+id/scrollviewGraficos"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/fundo_cartao"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Distribuição de Despesas"
android:textColor="@color/texto_principal"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/pieChartDespesas"
android:layout_width="match_parent"
android:layout_height="300dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/fundo_cartao"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Orçamento vs Gasto"
android:textColor="@color/texto_principal"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChartOrcamento"
android:layout_width="match_parent"
android:layout_height="300dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardBackgroundColor="@color/fundo_cartao"
app:cardCornerRadius="16dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tendência de Gastos"
android:textColor="@color/texto_principal"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"/>
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChartTendencia"
android:layout_width="match_parent"
android:layout_height="300dp"/>
</LinearLayout>
</androidx.cardview.widget.CardView>
</LinearLayout>
</ScrollView>
<LinearLayout
android:id="@+id/layoutEstadoVazioGraficos"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
android:gravity="center"
android:visibility="gone"
android:padding="32dp">
<com.airbnb.lottie.LottieAnimationView
android:layout_width="250dp"
android:layout_height="250dp"
app:lottie_rawRes="@raw/anim_grafico_vazio"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Despesas por Categoria"
android:textColor="@color/white"
android:textSize="18sp"
android:text="À espera de dados! 📊"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="4dp">
<com.github.mikephil.charting.charts.PieChart
android:id="@+id/pieChartDespesas"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="12dp"/>
</androidx.cardview.widget.CardView>
android:textColor="@color/texto_principal"
android:layout_marginTop="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Orçamento vs Gastos Reais"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="4dp">
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChartOrcamento"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="12dp"/>
</androidx.cardview.widget.CardView>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Tendência Mensal (Geral)"
android:textColor="@color/white"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_marginBottom="24dp"
app:cardCornerRadius="16dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="4dp">
<com.github.mikephil.charting.charts.BarChart
android:id="@+id/barChartTendencia"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="12dp"/>
</androidx.cardview.widget.CardView>
android:text="Regista as tuas primeiras transações para veres a magia dos gráficos acontecer."
android:textSize="15sp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="8dp"/>
</LinearLayout>
</ScrollView>
</FrameLayout>

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/bg_dinamico">
<!-- Lista onde vão aparecer os Cofres -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerObjetivos"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:clipToPadding="false"
android:paddingBottom="80dp"/>
<!-- Mensagem quando não há objetivos -->
<LinearLayout
android:id="@+id/layoutObjetivosVazios"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone">
<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_lazer"
app:tint="#A0AEC0"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sem objetivos definidos."
android:textColor="#A0AEC0"
android:textSize="16sp"
android:textStyle="bold"/>
</LinearLayout>
<!-- Botão de Adicionar Objetivo -->
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fabAdicionarObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_margin="24dp"
android:src="@android:drawable/ic_input_add"
app:tint="#FFFFFF"
app:backgroundTint="#00B8D4"
app:elevation="6dp"/>
</RelativeLayout>

View File

@@ -1,29 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/bg_tech_gradient"
android:padding="16dp"
tools:ignore="HardcodedText">
android:background="?android:attr/windowBackground"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Planeamento de Orçamento"
android:textColor="@color/white"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:layout_marginBottom="8dp"/>
android:textStyle="bold"
android:layout_marginBottom="12dp"/>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="@color/white">
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
android:layout_marginBottom="16dp">
<LinearLayout
android:layout_width="match_parent"
@@ -31,48 +30,79 @@
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Categoria"
android:textColor="#333333"
android:textStyle="bold"
android:textSize="12sp"/>
<Spinner
android:id="@+id/spinnerOrcamento"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:background="#F5F5F5"
android:layout_marginBottom="12dp"/>
android:orientation="horizontal"
android:baselineAligned="false">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Limite Mensal (€)"
android:textColor="#333333"
android:textStyle="bold"
android:textSize="12sp"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginEnd="8dp">
<EditText
android:id="@+id/editLimite"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#F5F5F5"
android:padding="8dp"
android:inputType="numberDecimal"
android:hint="0.00"
android:textColor="#000000"
android:layout_marginBottom="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Categoria"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<TextView
android:id="@+id/txtCategoriaOrcamento"
android:layout_width="match_parent"
android:layout_height="45dp"
android:text="Selecionar..."
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:background="@drawable/bg_coach_input_rect" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical"
android:layout_marginStart="8dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Limite (€)"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
android:layout_marginBottom="4dp"/>
<EditText
android:id="@+id/editLimiteOrcamento"
android:layout_width="match_parent"
android:layout_height="45dp"
android:hint="0.00"
android:textColor="?android:attr/textColorPrimary"
android:textColorHint="?android:attr/textColorSecondary"
android:inputType="numberDecimal"
android:backgroundTint="@color/tech_accent_cyan"
android:paddingStart="4dp"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/btnDefinirOrcamento"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="45dp"
android:text="Definir Orçamento"
android:backgroundTint="#0F2027"
android:textColor="@color/white"/>
android:textColor="#1A202C"
android:textStyle="bold"
android:layout_marginTop="16dp"
app:cornerRadius="8dp"
app:backgroundTint="@color/tech_accent_cyan" />
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -80,14 +110,55 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Orçamentos Ativos"
android:textColor="@color/white"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:textStyle="bold"
android:layout_marginBottom="8dp"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerOrcamentos"
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent"/>
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerOrcamentos"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/layoutEstadoVazioOrcamento"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone"
android:padding="16dp">
<com.airbnb.lottie.LottieAnimationView
android:layout_width="150dp"
android:layout_height="150dp"
app:lottie_rawRes="@raw/anim_vazio"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sem limites definidos!"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginTop="8dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Usa o cartão acima para definir objetivos."
android:textSize="14sp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="4dp"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -1,14 +1,88 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@drawable/bg_tech_gradient"
android:padding="8dp">
android:background="@color/bg_dinamico">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerTransacoes"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingBottom="80dp"/> </LinearLayout>
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:background="@drawable/bg_search_bar"
android:orientation="horizontal"
android:padding="12dp"
android:gravity="center_vertical">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@android:drawable/ic_menu_search"
app:tint="#A0AEC0" />
<EditText
android:id="@+id/editPesquisar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@android:color/transparent"
android:hint="Pesquisar transação..."
android:textColorHint="#718096"
android:textColor="@color/texto_principal"
android:textSize="14sp"
android:inputType="text"
android:maxLines="1"/>
</LinearLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerTransacoes"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="20dp"
android:clipToPadding="false"/>
<LinearLayout
android:id="@+id/layoutEstadoVazio"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:visibility="gone"
android:padding="32dp">
<com.airbnb.lottie.LottieAnimationView
android:layout_width="200dp"
android:layout_height="200dp"
app:lottie_rawRes="@raw/anim_vazio"
app:lottie_autoPlay="true"
app:lottie_loop="true"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Ainda não tens transações!"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="@color/texto_principal"
android:layout_marginTop="16dp"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Clica no botão + para começares a registar os teus movimentos."
android:textSize="14sp"
android:textAlignment="center"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="8dp"/>
</LinearLayout>
</FrameLayout>
</LinearLayout>

View File

@@ -3,10 +3,7 @@
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="12dp"
android:padding="16dp"
android:textSize="16sp"
android:textColor="#FFFFFF"
android:background="#1A202C"
android:ellipsize="end"
android:singleLine="true"/>
android:textColor="@color/texto_principal"
android:background="@color/bg_dinamico" />

View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/cardObjetivo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:cardCornerRadius="16dp"
app:cardElevation="2dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imgIconeObjetivo"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_centerVertical="true"
android:src="@drawable/ic_lazer"
app:tint="#00B8D4" />
<TextView
android:id="@+id/tvNomeObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_toEndOf="@id/imgIconeObjetivo"
android:layout_toStartOf="@id/layoutBotoesAcao"
android:text="Nome do Objetivo"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:textStyle="bold" />
<!-- Zona dos Botões de Ação -->
<LinearLayout
android:id="@+id/layoutBotoesAcao"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_centerVertical="true"
android:orientation="horizontal">
<!-- ✏️ Novo Botão Editar -->
<ImageView
android:id="@+id/btnEditarObjetivo"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_edit"
app:tint="#ECC94B" />
<!-- 🗑️ Botão Eliminar -->
<ImageView
android:id="@+id/btnEliminarObjetivo"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_delete"
app:tint="#F56565" />
</LinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/tvValoresObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="Guardado: € 0.00 / Alvo: € 0.00"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<ProgressBar
android:id="@+id/progressObjetivo"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_weight="1"
android:max="100"
android:progress="0"
android:progressTint="#00B8D4" />
<TextView
android:id="@+id/tvPercentagemObjetivo"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="0%"
android:textColor="#00B8D4"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -3,10 +3,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="12dp"
app:cardCornerRadius="12dp"
app:cardBackgroundColor="#2C5364"
app:cardElevation="2dp">
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:cardBackgroundColor="?android:attr/colorBackgroundFloating"
app:cardCornerRadius="16dp"
app:cardElevation="4dp">
<LinearLayout
android:layout_width="match_parent"
@@ -18,25 +19,42 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="8dp">
android:gravity="center_vertical">
<ImageView
android:id="@+id/imgIconeOrcamento"
android:layout_width="40dp"
android:layout_height="40dp"
android:src="@drawable/ic_alimentacao"
app:tint="@color/tech_accent_cyan" />
<TextView
android:id="@+id/tvCatOrcamento"
android:id="@+id/tvCategoriaOrcamento"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="12dp"
android:text="Alimentação"
android:textStyle="bold"
android:textColor="@color/white"
android:textSize="16sp"/>
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
android:textStyle="bold" />
<ImageView
android:id="@+id/btnEliminarOrcamento"
android:layout_width="28dp"
android:layout_height="28dp"
android:layout_marginEnd="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@android:drawable/ic_menu_delete"
app:tint="#F56565" />
<TextView
android:id="@+id/tvValoresOrcamento"
android:id="@+id/tvPercentagemOrcamento"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="€85.50 / €300.00"
android:textColor="#B0BEC5"
android:textSize="14sp"/>
android:text="75%"
android:textColor="@color/tech_accent_cyan"
android:textStyle="bold" />
</LinearLayout>
<ProgressBar
@@ -44,18 +62,18 @@
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="8dp"
android:progressDrawable="@drawable/progress_savings"
android:max="100"
android:progress="50"/>
android:layout_marginTop="16dp"
android:layout_marginBottom="8dp"
android:progress="75"
android:progressDrawable="@drawable/custom_progress_bar" />
<TextView
android:id="@+id/tvRestante"
android:layout_width="wrap_content"
android:id="@+id/tvValoresOrcamento"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restam €214.50 (72%)"
android:textColor="#90A4AE"
android:text="Gasto: € 150.00 / Limite: € 200.00"
android:textColor="?android:attr/textColorSecondary"
android:textSize="12sp"
android:layout_marginTop="6dp"/>
android:textAlignment="viewEnd" />
</LinearLayout>
</androidx.cardview.widget.CardView>

View File

@@ -5,10 +5,9 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="16dp"
android:layout_marginVertical="8dp"
app:cardBackgroundColor="#FFFFFF"
app:cardBackgroundColor="@color/fundo_cartao"
app:cardCornerRadius="12dp"
app:cardElevation="2dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@@ -36,7 +35,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Descrição"
android:textColor="#1A202C"
android:textColor="@color/texto_principal"
android:textSize="16sp"
android:textStyle="bold" />
@@ -45,7 +44,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Data"
android:textColor="#718096"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
android:layout_marginTop="4dp"/>
</LinearLayout>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -4,4 +4,5 @@
<color name="texto_principal">#FFFFFF</color>
<color name="fundo_cartao">#2D3748</color>
<color name="linha_separadora">#2D3748</color>
</resources>
<color name="bg_dinamico">#1A202C</color> <color name="texto_dinamico">#FFFFFF</color> </resources>

View File

@@ -3,14 +3,16 @@
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
<color name="tech_accent_cyan">#00E676</color>
<color name="tech_card_bg">#2D3748</color>
<color name="text_secondary">#A0AEC0</color>
<color name="tech_accent_green">#48BB78</color>
<color name="tech_accent_yellow">#ECC94B</color> <color name="tech_accent_red">#F56565</color>
<color name="tech_bg_dark">#1A202C</color>
<color name="tech_accent_yellow">#ECC94B</color>
<color name="tech_accent_red">#F56565</color>
<color name="fundo_app">#FFFFFF</color>
<color name="texto_principal">#1A202C</color>
<color name="fundo_cartao">#F7FAFC</color>
<color name="fundo_app">#F3F4F6</color>
<color name="fundo_cartao">#FFFFFF</color> <color name="tech_card_bg">#FFFFFF</color>
<color name="tech_bg_dark">#F8F9FA</color>
<color name="texto_principal">#1A202C</color> <color name="text_secondary">#718096</color>
<color name="linha_separadora">#E2E8F0</color>
<color name="bg_dinamico">#F3F4F6</color>
<color name="texto_dinamico">#1A202C</color>
</resources>

View File

@@ -1,3 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -7,6 +7,7 @@ appcompat = "1.7.1"
material = "1.13.0"
activity = "1.12.2"
constraintlayout = "2.2.1"
generativeai = "0.9.0"
[libraries]
junit = { group = "junit", name = "junit", version.ref = "junit" }
@@ -16,6 +17,7 @@ appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "a
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
activity = { group = "androidx.activity", name = "activity", version.ref = "activity" }
constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
generativeai = { group = "com.google.ai.client.generativeai", name = "generativeai", version.ref = "generativeai" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }