From 53c58397506a6b3a02ddb8853f383bfffdc63ca7 Mon Sep 17 00:00:00 2001 From: 230402 <230402@epvc.pt> Date: Wed, 18 Mar 2026 10:37:27 +0000 Subject: [PATCH] =?UTF-8?q?Corre=C3=A7=C3=A3o=20de=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deploymentTargetSelector.xml | 8 + app/build.gradle.kts | 7 + app/src/main/AndroidManifest.xml | 48 +- .../finzora/AdicionarTransacaoActivity.java | 241 ++++---- .../java/com/example/finzora/DBHelper.java | 11 +- .../example/finzora/DefinicoesActivity.java | 214 +++++-- .../com/example/finzora/DicasFragment.java | 468 +++++++++++--- .../example/finzora/EditarPerfilActivity.java | 100 ++- .../com/example/finzora/GraficosFragment.java | 257 ++++++-- .../com/example/finzora/LockActivity.java | 84 +++ .../com/example/finzora/LoginActivity.java | 83 ++- .../com/example/finzora/MainActivity.java | 505 +++++++++++++-- .../example/finzora/NovaPasswordActivity.java | 116 ++++ .../java/com/example/finzora/Objetivo.java | 17 + .../com/example/finzora/ObjetivosAdapter.java | 119 ++++ .../example/finzora/ObjetivosFragment.java | 326 ++++++++++ .../example/finzora/OnboardingActivity.java | 18 +- .../com/example/finzora/OrcamentoAdapter.java | 98 ++- .../example/finzora/OrcamentoFragment.java | 288 +++++++-- .../com/example/finzora/RegisterActivity.java | 41 +- .../java/com/example/finzora/Transacao.java | 18 +- .../example/finzora/TransacoesAdapter.java | 173 ++++-- .../example/finzora/TransacoesFragment.java | 209 +++++-- .../com/example/finzora/ViewPagerAdapter.java | 5 +- app/src/main/res/drawable/bg_bottom_sheet.xml | 8 + .../main/res/drawable/bg_coach_input_rect.xml | 10 + app/src/main/res/drawable/bg_coach_rect.xml | 3 + .../res/drawable/bg_dialog_arredondado.xml | 8 + app/src/main/res/drawable/bg_search_bar.xml | 11 + app/src/main/res/drawable/bg_tech_input.xml | 17 + .../main/res/drawable/custom_progress_bar.xml | 20 + app/src/main/res/drawable/ic_alimentacao.xml | 3 + app/src/main/res/drawable/ic_cafe.xml | 3 + app/src/main/res/drawable/ic_carrinho.xml | 3 + app/src/main/res/drawable/ic_casa.xml | 3 + app/src/main/res/drawable/ic_compras.xml | 3 + app/src/main/res/drawable/ic_contas.xml | 3 + app/src/main/res/drawable/ic_educacao.xml | 3 + app/src/main/res/drawable/ic_ginasio.xml | 3 + app/src/main/res/drawable/ic_lazer.xml | 3 + app/src/main/res/drawable/ic_outros.xml | 3 + app/src/main/res/drawable/ic_salario.xml | 3 + app/src/main/res/drawable/ic_saude.xml | 3 + app/src/main/res/drawable/ic_telemovel.xml | 3 + app/src/main/res/drawable/ic_transportes.xml | 3 + .../layout/activity_adicionar_transacao.xml | 61 +- .../main/res/layout/activity_definicoes.xml | 75 ++- .../res/layout/activity_editar_perfil.xml | 63 +- app/src/main/res/layout/activity_lock.xml | 43 ++ app/src/main/res/layout/activity_main.xml | 43 +- .../res/layout/activity_nova_password.xml | 113 ++++ .../main/res/layout/bottom_sheet_detalhe.xml | 136 ++++ app/src/main/res/layout/dialog_categorias.xml | 57 ++ app/src/main/res/layout/dialog_contactar.xml | 11 +- app/src/main/res/layout/dialog_contactos.xml | 141 +++++ app/src/main/res/layout/dialog_eliminar.xml | 78 +++ app/src/main/res/layout/dialog_exportar.xml | 137 +++++ app/src/main/res/layout/dialog_faq.xml | 6 + .../main/res/layout/dialog_novo_objetivo.xml | 73 +++ .../main/res/layout/dialog_tipo_transacao.xml | 161 +++++ app/src/main/res/layout/dialog_tutorial.xml | 8 +- app/src/main/res/layout/fragment_dicas.xml | 582 ++++++++---------- app/src/main/res/layout/fragment_graficos.xml | 199 +++--- .../main/res/layout/fragment_objetivos.xml | 56 ++ .../main/res/layout/fragment_orcamento.xml | 167 +++-- .../main/res/layout/fragment_transacoes.xml | 88 ++- app/src/main/res/layout/item_dropdown.xml | 9 +- app/src/main/res/layout/item_objetivo.xml | 111 ++++ app/src/main/res/layout/item_orcamento.xml | 62 +- app/src/main/res/layout/item_transacao.xml | 7 +- app/src/main/res/raw/anim_dicas_vazio.json | 1 + app/src/main/res/raw/anim_grafico_vazio.json | 1 + app/src/main/res/raw/anim_vazio.json | 1 + app/src/main/res/values-night/colors.xml | 3 +- app/src/main/res/values/colors.xml | 16 +- app/src/main/res/values/refs.xml | 3 - gradle/libs.versions.toml | 2 + 77 files changed, 4921 insertions(+), 1166 deletions(-) create mode 100644 app/src/main/java/com/example/finzora/LockActivity.java create mode 100644 app/src/main/java/com/example/finzora/NovaPasswordActivity.java create mode 100644 app/src/main/java/com/example/finzora/Objetivo.java create mode 100644 app/src/main/java/com/example/finzora/ObjetivosAdapter.java create mode 100644 app/src/main/java/com/example/finzora/ObjetivosFragment.java create mode 100644 app/src/main/res/drawable/bg_bottom_sheet.xml create mode 100644 app/src/main/res/drawable/bg_coach_input_rect.xml create mode 100644 app/src/main/res/drawable/bg_coach_rect.xml create mode 100644 app/src/main/res/drawable/bg_dialog_arredondado.xml create mode 100644 app/src/main/res/drawable/bg_search_bar.xml create mode 100644 app/src/main/res/drawable/bg_tech_input.xml create mode 100644 app/src/main/res/drawable/custom_progress_bar.xml create mode 100644 app/src/main/res/drawable/ic_alimentacao.xml create mode 100644 app/src/main/res/drawable/ic_cafe.xml create mode 100644 app/src/main/res/drawable/ic_carrinho.xml create mode 100644 app/src/main/res/drawable/ic_casa.xml create mode 100644 app/src/main/res/drawable/ic_compras.xml create mode 100644 app/src/main/res/drawable/ic_contas.xml create mode 100644 app/src/main/res/drawable/ic_educacao.xml create mode 100644 app/src/main/res/drawable/ic_ginasio.xml create mode 100644 app/src/main/res/drawable/ic_lazer.xml create mode 100644 app/src/main/res/drawable/ic_outros.xml create mode 100644 app/src/main/res/drawable/ic_salario.xml create mode 100644 app/src/main/res/drawable/ic_saude.xml create mode 100644 app/src/main/res/drawable/ic_telemovel.xml create mode 100644 app/src/main/res/drawable/ic_transportes.xml create mode 100644 app/src/main/res/layout/activity_lock.xml create mode 100644 app/src/main/res/layout/activity_nova_password.xml create mode 100644 app/src/main/res/layout/bottom_sheet_detalhe.xml create mode 100644 app/src/main/res/layout/dialog_categorias.xml create mode 100644 app/src/main/res/layout/dialog_contactos.xml create mode 100644 app/src/main/res/layout/dialog_eliminar.xml create mode 100644 app/src/main/res/layout/dialog_exportar.xml create mode 100644 app/src/main/res/layout/dialog_novo_objetivo.xml create mode 100644 app/src/main/res/layout/dialog_tipo_transacao.xml create mode 100644 app/src/main/res/layout/fragment_objetivos.xml create mode 100644 app/src/main/res/layout/item_objetivo.xml create mode 100644 app/src/main/res/raw/anim_dicas_vazio.json create mode 100644 app/src/main/res/raw/anim_grafico_vazio.json create mode 100644 app/src/main/res/raw/anim_vazio.json delete mode 100644 app/src/main/res/values/refs.xml diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..071a253 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,14 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b621ebf..41447b0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index be00765..e078032 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ package="com.example.finzora"> + + + - - + android:windowSoftInputMode="adjustResize"> + + + + - + + + + + + - - + + - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java b/app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java index 11a7e21..7cf8806 100644 --- a/app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java +++ b/app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java @@ -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 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); } diff --git a/app/src/main/java/com/example/finzora/DBHelper.java b/app/src/main/java/com/example/finzora/DBHelper.java index e1160d0..db5cb11 100644 --- a/app/src/main/java/com/example/finzora/DBHelper.java +++ b/app/src/main/java/com/example/finzora/DBHelper.java @@ -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()); } diff --git a/app/src/main/java/com/example/finzora/DefinicoesActivity.java b/app/src/main/java/com/example/finzora/DefinicoesActivity.java index f7a040a..5d94f09 100644 --- a/app/src/main/java/com/example/finzora/DefinicoesActivity.java +++ b/app/src/main/java/com/example/finzora/DefinicoesActivity.java @@ -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(); } diff --git a/app/src/main/java/com/example/finzora/DicasFragment.java b/app/src/main/java/com/example/finzora/DicasFragment.java index 4b87698..c3b2edf 100644 --- a/app/src/main/java/com/example/finzora/DicasFragment.java +++ b/app/src/main/java/com/example/finzora/DicasFragment.java @@ -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("\\*\\*(.*?)\\*\\*", "$1"); + textoFormatado = textoFormatado.replace("\n", "
"); + 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 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 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 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 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 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 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 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 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 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); } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/EditarPerfilActivity.java b/app/src/main/java/com/example/finzora/EditarPerfilActivity.java index c30b3b8..7579100 100644 --- a/app/src/main/java/com/example/finzora/EditarPerfilActivity.java +++ b/app/src/main/java/com/example/finzora/EditarPerfilActivity.java @@ -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 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; + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/GraficosFragment.java b/app/src/main/java/com/example/finzora/GraficosFragment.java index 4f3bd4c..bd18614 100644 --- a/app/src/main/java/com/example/finzora/GraficosFragment.java +++ b/app/src/main/java/com/example/finzora/GraficosFragment.java @@ -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 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 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 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 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 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 despesas = dbHelper.getDespesasPorCategoria(); ArrayList entradas = new ArrayList<>(); - for (Map.Entry 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 orcamentos, Map gastos) { configurarEstiloBarChart(barChartOrcamento); - Map orcamentos = dbHelper.getOrcamentosDefinidos(); ArrayList gastosEntries = new ArrayList<>(); ArrayList orcamentoEntries = new ArrayList<>(); ArrayList categorias = new ArrayList<>(); @@ -95,7 +229,7 @@ public class GraficosFragment extends Fragment { for (Map.Entry 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 despesaEntry = new ArrayList<>(); ArrayList 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 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); diff --git a/app/src/main/java/com/example/finzora/LockActivity.java b/app/src/main/java/com/example/finzora/LockActivity.java new file mode 100644 index 0000000..e19cf2a --- /dev/null +++ b/app/src/main/java/com/example/finzora/LockActivity.java @@ -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(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/LoginActivity.java b/app/src/main/java/com/example/finzora/LoginActivity.java index d7c387d..aa0033b 100644 --- a/app/src/main/java/com/example/finzora/LoginActivity.java +++ b/app/src/main/java/com/example/finzora/LoginActivity.java @@ -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(); diff --git a/app/src/main/java/com/example/finzora/MainActivity.java b/app/src/main/java/com/example/finzora/MainActivity.java index 6740c69..9e3e313 100644 --- a/app/src/main/java/com/example/finzora/MainActivity.java +++ b/app/src/main/java/com/example/finzora/MainActivity.java @@ -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(); } } } }); diff --git a/app/src/main/java/com/example/finzora/NovaPasswordActivity.java b/app/src/main/java/com/example/finzora/NovaPasswordActivity.java new file mode 100644 index 0000000..570ff7c --- /dev/null +++ b/app/src/main/java/com/example/finzora/NovaPasswordActivity.java @@ -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"); + } + }); + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/Objetivo.java b/app/src/main/java/com/example/finzora/Objetivo.java new file mode 100644 index 0000000..670787c --- /dev/null +++ b/app/src/main/java/com/example/finzora/Objetivo.java @@ -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; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/ObjetivosAdapter.java b/app/src/main/java/com/example/finzora/ObjetivosAdapter.java new file mode 100644 index 0000000..6b56f12 --- /dev/null +++ b/app/src/main/java/com/example/finzora/ObjetivosAdapter.java @@ -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 { + + private List listaObjetivos; + private ObjetivosFragment fragment; + private float saldoAtual; + + public ObjetivosAdapter(List 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); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/ObjetivosFragment.java b/app/src/main/java/com/example/finzora/ObjetivosFragment.java new file mode 100644 index 0000000..5af6ac3 --- /dev/null +++ b/app/src/main/java/com/example/finzora/ObjetivosFragment.java @@ -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 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(); + } + }); + } + } + }); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/OnboardingActivity.java b/app/src/main/java/com/example/finzora/OnboardingActivity.java index d3942b2..4fa5d3e 100644 --- a/app/src/main/java/com/example/finzora/OnboardingActivity.java +++ b/app/src/main/java/com/example/finzora/OnboardingActivity.java @@ -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 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(); diff --git a/app/src/main/java/com/example/finzora/OrcamentoAdapter.java b/app/src/main/java/com/example/finzora/OrcamentoAdapter.java index 6577704..97b76f5 100644 --- a/app/src/main/java/com/example/finzora/OrcamentoAdapter.java +++ b/app/src/main/java/com/example/finzora/OrcamentoAdapter.java @@ -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 { - // Lista de pares: Chave=Categoria, Valor=Limite - private List> listaOrcamentos; - private DBHelper dbHelper; + public static class OrcamentoItem { + String categoria; + float limite; + float gasto; - public OrcamentoAdapter(List> lista, DBHelper db) { + public OrcamentoItem(String categoria, float limite, float gasto) { + this.categoria = categoria; + this.limite = limite; + this.gasto = gasto; + } + } + + private List listaOrcamentos; + private OrcamentoFragment fragment; + + public OrcamentoAdapter(List lista, OrcamentoFragment fragment) { this.listaOrcamentos = lista; - this.dbHelper = db; + this.fragment = fragment; } @NonNull @@ -32,30 +43,42 @@ public class OrcamentoAdapter extends RecyclerView.Adapter 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 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 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 orcamentosMap = dbHelper.getOrcamentosDefinidos(); - List> 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 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 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 listaFinal = new ArrayList<>(); + for (Map.Entry 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(); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/RegisterActivity.java b/app/src/main/java/com/example/finzora/RegisterActivity.java index 961ba20..1475034 100644 --- a/app/src/main/java/com/example/finzora/RegisterActivity.java +++ b/app/src/main/java/com/example/finzora/RegisterActivity.java @@ -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(); } diff --git a/app/src/main/java/com/example/finzora/Transacao.java b/app/src/main/java/com/example/finzora/Transacao.java index 884a331..8c16c7e 100644 --- a/app/src/main/java/com/example/finzora/Transacao.java +++ b/app/src/main/java/com/example/finzora/Transacao.java @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/TransacoesAdapter.java b/app/src/main/java/com/example/finzora/TransacoesAdapter.java index fedf1a0..145e778 100644 --- a/app/src/main/java/com/example/finzora/TransacoesAdapter.java +++ b/app/src/main/java/com/example/finzora/TransacoesAdapter.java @@ -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 { +public class TransacoesAdapter extends RecyclerView.Adapter { - private List listaTransacoes; - private Context context; - private TransacoesFragment fragment; + private List listaOriginal; + private List listaVisivel; + private OnTransacaoClickListener listener; - // O teu construtor mantém-se igual! - public TransacoesAdapter(List lista, Context context, TransacoesFragment fragment) { - this.listaTransacoes = lista; - this.context = context; - this.fragment = fragment; + public interface OnTransacaoClickListener { + void onTransacaoClick(Transacao t); + } + + public TransacoesAdapter(List 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); } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/TransacoesFragment.java b/app/src/main/java/com/example/finzora/TransacoesFragment.java index 01afa5d..d2042d8 100644 --- a/app/src/main/java/com/example/finzora/TransacoesFragment.java +++ b/app/src/main/java/com/example/finzora/TransacoesFragment.java @@ -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 listaNuvem = new ArrayList<>(); - try { + String jsonResposta = response.body().string(); + List 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(); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/ViewPagerAdapter.java b/app/src/main/java/com/example/finzora/ViewPagerAdapter.java index f579958..665b5fd 100644 --- a/app/src/main/java/com/example/finzora/ViewPagerAdapter.java +++ b/app/src/main/java/com/example/finzora/ViewPagerAdapter.java @@ -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! } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_bottom_sheet.xml b/app/src/main/res/drawable/bg_bottom_sheet.xml new file mode 100644 index 0000000..462f1a1 --- /dev/null +++ b/app/src/main/res/drawable/bg_bottom_sheet.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_coach_input_rect.xml b/app/src/main/res/drawable/bg_coach_input_rect.xml new file mode 100644 index 0000000..e66745b --- /dev/null +++ b/app/src/main/res/drawable/bg_coach_input_rect.xml @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_coach_rect.xml b/app/src/main/res/drawable/bg_coach_rect.xml new file mode 100644 index 0000000..0e37940 --- /dev/null +++ b/app/src/main/res/drawable/bg_coach_rect.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_dialog_arredondado.xml b/app/src/main/res/drawable/bg_dialog_arredondado.xml new file mode 100644 index 0000000..a9849bd --- /dev/null +++ b/app/src/main/res/drawable/bg_dialog_arredondado.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_search_bar.xml b/app/src/main/res/drawable/bg_search_bar.xml new file mode 100644 index 0000000..a5469e6 --- /dev/null +++ b/app/src/main/res/drawable/bg_search_bar.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tech_input.xml b/app/src/main/res/drawable/bg_tech_input.xml new file mode 100644 index 0000000..ad09eac --- /dev/null +++ b/app/src/main/res/drawable/bg_tech_input.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_progress_bar.xml b/app/src/main/res/drawable/custom_progress_bar.xml new file mode 100644 index 0000000..df4dda1 --- /dev/null +++ b/app/src/main/res/drawable/custom_progress_bar.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_alimentacao.xml b/app/src/main/res/drawable/ic_alimentacao.xml new file mode 100644 index 0000000..1bb0270 --- /dev/null +++ b/app/src/main/res/drawable/ic_alimentacao.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cafe.xml b/app/src/main/res/drawable/ic_cafe.xml new file mode 100644 index 0000000..dca7bbb --- /dev/null +++ b/app/src/main/res/drawable/ic_cafe.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_carrinho.xml b/app/src/main/res/drawable/ic_carrinho.xml new file mode 100644 index 0000000..f2fb67d --- /dev/null +++ b/app/src/main/res/drawable/ic_carrinho.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_casa.xml b/app/src/main/res/drawable/ic_casa.xml new file mode 100644 index 0000000..c1ffbfc --- /dev/null +++ b/app/src/main/res/drawable/ic_casa.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_compras.xml b/app/src/main/res/drawable/ic_compras.xml new file mode 100644 index 0000000..e8a53be --- /dev/null +++ b/app/src/main/res/drawable/ic_compras.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_contas.xml b/app/src/main/res/drawable/ic_contas.xml new file mode 100644 index 0000000..876545b --- /dev/null +++ b/app/src/main/res/drawable/ic_contas.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_educacao.xml b/app/src/main/res/drawable/ic_educacao.xml new file mode 100644 index 0000000..716e991 --- /dev/null +++ b/app/src/main/res/drawable/ic_educacao.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_ginasio.xml b/app/src/main/res/drawable/ic_ginasio.xml new file mode 100644 index 0000000..6dcd7b7 --- /dev/null +++ b/app/src/main/res/drawable/ic_ginasio.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lazer.xml b/app/src/main/res/drawable/ic_lazer.xml new file mode 100644 index 0000000..3de073f --- /dev/null +++ b/app/src/main/res/drawable/ic_lazer.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_outros.xml b/app/src/main/res/drawable/ic_outros.xml new file mode 100644 index 0000000..eaf7c3a --- /dev/null +++ b/app/src/main/res/drawable/ic_outros.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_salario.xml b/app/src/main/res/drawable/ic_salario.xml new file mode 100644 index 0000000..778364c --- /dev/null +++ b/app/src/main/res/drawable/ic_salario.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_saude.xml b/app/src/main/res/drawable/ic_saude.xml new file mode 100644 index 0000000..640fad4 --- /dev/null +++ b/app/src/main/res/drawable/ic_saude.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_telemovel.xml b/app/src/main/res/drawable/ic_telemovel.xml new file mode 100644 index 0000000..fd710d4 --- /dev/null +++ b/app/src/main/res/drawable/ic_telemovel.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_transportes.xml b/app/src/main/res/drawable/ic_transportes.xml new file mode 100644 index 0000000..8f3ae4f --- /dev/null +++ b/app/src/main/res/drawable/ic_transportes.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_adicionar_transacao.xml b/app/src/main/res/layout/activity_adicionar_transacao.xml index 5eb3d1a..ba6ca1d 100644 --- a/app/src/main/res/layout/activity_adicionar_transacao.xml +++ b/app/src/main/res/layout/activity_adicionar_transacao.xml @@ -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"> -