From 347b916c53deb411e03bce21c57716fc1c428f4c Mon Sep 17 00:00:00 2001 From: 230402 <230402@epvc.pt> Date: Tue, 10 Mar 2026 15:42:51 +0000 Subject: [PATCH] =?UTF-8?q?Melhoramentos=20nos=20designs=20todos=20e=20cor?= =?UTF-8?q?re=C3=A7=C3=A3o=20de=20erros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/deviceManager.xml | 13 + app/build.gradle.kts | 12 +- app/src/main/AndroidManifest.xml | 35 +- .../finzora/AdicionarTransacaoActivity.java | 178 ++++++++ .../java/com/example/finzora/DBHelper.java | 154 +++++++ .../example/finzora/DefinicoesActivity.java | 175 ++++++++ .../com/example/finzora/DicasFragment.java | 159 ++++++- .../example/finzora/EditarPerfilActivity.java | 59 +++ .../com/example/finzora/GraficosFragment.java | 186 ++++++++- .../com/example/finzora/LoginActivity.java | 116 ++++- .../com/example/finzora/MainActivity.java | 174 ++++++-- .../example/finzora/OnboardingActivity.java | 102 +++++ .../example/finzora/OnboardingAdapter.java | 55 +++ .../com/example/finzora/OnboardingItem.java | 16 + .../com/example/finzora/OrcamentoAdapter.java | 78 ++++ .../example/finzora/OrcamentoFragment.java | 98 +++++ .../com/example/finzora/ProfileActivity.java | 151 +++++++ .../finzora/RecuperarPasswordActivity.java | 90 ++++ .../com/example/finzora/RegisterActivity.java | 141 +++++-- .../com/example/finzora/SupabaseConfig.java | 8 + .../java/com/example/finzora/Transacao.java | 38 ++ .../example/finzora/TransacoesAdapter.java | 79 ++++ .../example/finzora/TransacoesFragment.java | 154 ++++++- .../com/example/finzora/ViewPagerAdapter.java | 12 +- app/src/main/res/drawable/bg_card_tech.xml | 6 + app/src/main/res/drawable/bg_circle_icon.xml | 6 + .../main/res/drawable/bg_icone_transacao.xml | 6 + .../main/res/drawable/bg_tech_gradient.xml | 10 + app/src/main/res/drawable/eliminar.png | Bin 0 -> 7842 bytes app/src/main/res/drawable/ic_arrow_back.xml | 9 + .../res/drawable/ic_baseline_delete_24.xml | 4 + .../main/res/drawable/ic_carteira_tech.xml | 26 ++ app/src/main/res/drawable/ic_chart.xml | 10 + app/src/main/res/drawable/ic_idea.xml | 10 + app/src/main/res/drawable/ic_pulse.xml | 10 + app/src/main/res/drawable/ic_settings_pap.xml | 9 + app/src/main/res/drawable/ic_wallet.xml | 14 +- app/src/main/res/drawable/progress_neon.xml | 18 + .../main/res/drawable/progress_savings.xml | 17 + app/src/main/res/drawable/wallet.png | Bin 0 -> 3371 bytes .../layout/activity_adicionar_transacao.xml | 61 +++ .../main/res/layout/activity_definicoes.xml | 90 ++++ .../res/layout/activity_editar_perfil.xml | 93 +++++ app/src/main/res/layout/activity_login.xml | 192 +++++---- app/src/main/res/layout/activity_main.xml | 191 +++++---- .../main/res/layout/activity_onboarding.xml | 68 +++ app/src/main/res/layout/activity_profile.xml | 132 ++++++ .../layout/activity_recuperar_password.xml | 83 ++++ app/src/main/res/layout/activity_register.xml | 231 +++++----- app/src/main/res/layout/card_despesas.xml | 40 +- app/src/main/res/layout/card_receitas.xml | 40 +- app/src/main/res/layout/card_saldo_total.xml | 53 ++- app/src/main/res/layout/dialog_contactar.xml | 38 ++ app/src/main/res/layout/dialog_faq.xml | 64 +++ app/src/main/res/layout/dialog_suporte.xml | 130 ++++++ app/src/main/res/layout/dialog_tutorial.xml | 59 +++ app/src/main/res/layout/fragment_dicas.xml | 395 ++++++++++++++++++ app/src/main/res/layout/fragment_graficos.xml | 88 ++++ .../main/res/layout/fragment_orcamento.xml | 93 +++++ .../main/res/layout/fragment_transacoes.xml | 14 + app/src/main/res/layout/item_dropdown.xml | 12 + app/src/main/res/layout/item_onboarding.xml | 54 +++ app/src/main/res/layout/item_orcamento.xml | 61 +++ app/src/main/res/layout/item_transacao.xml | 79 ++++ app/src/main/res/values-night/colors.xml | 7 + app/src/main/res/values/colors.xml | 11 + app/src/main/res/values/ids.xml | 22 + app/src/main/res/values/themes.xml | 25 +- gradle.properties | 12 +- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle.kts | 2 +- 72 files changed, 4467 insertions(+), 415 deletions(-) create mode 100644 .idea/deviceManager.xml create mode 100644 app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java create mode 100644 app/src/main/java/com/example/finzora/DBHelper.java create mode 100644 app/src/main/java/com/example/finzora/DefinicoesActivity.java create mode 100644 app/src/main/java/com/example/finzora/EditarPerfilActivity.java create mode 100644 app/src/main/java/com/example/finzora/OnboardingActivity.java create mode 100644 app/src/main/java/com/example/finzora/OnboardingAdapter.java create mode 100644 app/src/main/java/com/example/finzora/OnboardingItem.java create mode 100644 app/src/main/java/com/example/finzora/OrcamentoAdapter.java create mode 100644 app/src/main/java/com/example/finzora/OrcamentoFragment.java create mode 100644 app/src/main/java/com/example/finzora/ProfileActivity.java create mode 100644 app/src/main/java/com/example/finzora/RecuperarPasswordActivity.java create mode 100644 app/src/main/java/com/example/finzora/SupabaseConfig.java create mode 100644 app/src/main/java/com/example/finzora/Transacao.java create mode 100644 app/src/main/java/com/example/finzora/TransacoesAdapter.java create mode 100644 app/src/main/res/drawable/bg_card_tech.xml create mode 100644 app/src/main/res/drawable/bg_circle_icon.xml create mode 100644 app/src/main/res/drawable/bg_icone_transacao.xml create mode 100644 app/src/main/res/drawable/bg_tech_gradient.xml create mode 100644 app/src/main/res/drawable/eliminar.png create mode 100644 app/src/main/res/drawable/ic_arrow_back.xml create mode 100644 app/src/main/res/drawable/ic_baseline_delete_24.xml create mode 100644 app/src/main/res/drawable/ic_carteira_tech.xml create mode 100644 app/src/main/res/drawable/ic_chart.xml create mode 100644 app/src/main/res/drawable/ic_idea.xml create mode 100644 app/src/main/res/drawable/ic_pulse.xml create mode 100644 app/src/main/res/drawable/ic_settings_pap.xml create mode 100644 app/src/main/res/drawable/progress_neon.xml create mode 100644 app/src/main/res/drawable/progress_savings.xml create mode 100644 app/src/main/res/drawable/wallet.png create mode 100644 app/src/main/res/layout/activity_adicionar_transacao.xml create mode 100644 app/src/main/res/layout/activity_definicoes.xml create mode 100644 app/src/main/res/layout/activity_editar_perfil.xml create mode 100644 app/src/main/res/layout/activity_onboarding.xml create mode 100644 app/src/main/res/layout/activity_profile.xml create mode 100644 app/src/main/res/layout/activity_recuperar_password.xml create mode 100644 app/src/main/res/layout/dialog_contactar.xml create mode 100644 app/src/main/res/layout/dialog_faq.xml create mode 100644 app/src/main/res/layout/dialog_suporte.xml create mode 100644 app/src/main/res/layout/dialog_tutorial.xml create mode 100644 app/src/main/res/layout/fragment_dicas.xml create mode 100644 app/src/main/res/layout/fragment_graficos.xml create mode 100644 app/src/main/res/layout/fragment_orcamento.xml create mode 100644 app/src/main/res/layout/fragment_transacoes.xml create mode 100644 app/src/main/res/layout/item_dropdown.xml create mode 100644 app/src/main/res/layout/item_onboarding.xml create mode 100644 app/src/main/res/layout/item_orcamento.xml create mode 100644 app/src/main/res/layout/item_transacao.xml create mode 100644 app/src/main/res/values-night/colors.xml diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a3dcf35..b621ebf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -4,9 +4,7 @@ plugins { android { namespace = "com.example.finzora" - compileSdk { - version = release(36) - } + compileSdk = 36 defaultConfig { applicationId = "com.example.finzora" @@ -34,8 +32,12 @@ android { } dependencies { - implementation(libs.appcompat) - implementation(libs.material) + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("com.google.android.material:material:1.13.0") + implementation("com.github.PhilJay:MPAndroidChart:v3.1.0") + implementation("androidx.viewpager2:viewpager2:1.1.0") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + implementation("com.google.code.gson:gson:2.10.1") implementation(libs.activity) implementation(libs.constraintlayout) testImplementation(libs.junit) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 55134a1..be00765 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,9 @@ + xmlns:tools="http://schemas.android.com/tools" + package="com.example.finzora"> + + + android:theme="@style/Theme.Finzora" + tools:targetApi="31"> - - - - + android:exported="true" + 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 new file mode 100644 index 0000000..11a7e21 --- /dev/null +++ b/app/src/main/java/com/example/finzora/AdicionarTransacaoActivity.java @@ -0,0 +1,178 @@ +package com.example.finzora; + +import android.app.AlertDialog; +import android.os.Bundle; +import android.text.TextUtils; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class AdicionarTransacaoActivity extends AppCompatActivity { + + // Declaração dos componentes do ecrã (Sem os RadioButtons!) + private EditText editValor; + private Spinner spinnerCategoria; + private Button btnGuardar; + private ImageView btnVoltar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_adicionar_transacao); + + inicializarComponentes(); + + // --- ENCHER O SPINNER COM AS CATEGORIAS --- + String[] categorias = {"Alimentação", "Transportes", "Lazer", "Educação", "Saúde", "Salário", "Mesada", "Prémios", "Outros"}; + + ArrayAdapter adapter = new ArrayAdapter<>( + this, + R.layout.item_dropdown, + categorias + ); + adapter.setDropDownViewResource(R.layout.item_dropdown); + spinnerCategoria.setAdapter(adapter); + + // Botão para voltar para trás + if (btnVoltar != null) { + btnVoltar.setOnClickListener(v -> finish()); + } + + // AGORA O BOTÃO GUARDAR CHAMA O POP-UP! + btnGuardar.setOnClickListener(v -> perguntarTipoTransacao()); + } + + // ==================================================================== + // A MAGIA DO POP-UP + // ==================================================================== + private void perguntarTipoTransacao() { + String valorStr = editValor.getText().toString().trim(); + + // 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; + } + + // 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); + + if (userId == null) { + android.widget.Toast.makeText(this, "Erro: Utilizador não identificado. Faz login novamente.", android.widget.Toast.LENGTH_LONG).show(); + 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(); + 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..."); + + // 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 + "\"" + + "}"; + + 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 + okhttp3.Request request = new okhttp3.Request.Builder() + .url(SupabaseConfig.SUPABASE_URL + "/rest/v1/transacoes") + .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) + .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 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(); + } + }); + } + }); + + } catch (Exception e) { + android.widget.Toast.makeText(this, "ERRO LOCAL: " + e.getMessage(), android.widget.Toast.LENGTH_LONG).show(); + e.printStackTrace(); + } + } + private void inicializarComponentes() { + editValor = findViewById(R.id.editValor); + spinnerCategoria = findViewById(R.id.spinnerCategoria); + btnGuardar = findViewById(R.id.btnGuardar); + btnVoltar = findViewById(R.id.btnVoltar); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/DBHelper.java b/app/src/main/java/com/example/finzora/DBHelper.java new file mode 100644 index 0000000..e1160d0 --- /dev/null +++ b/app/src/main/java/com/example/finzora/DBHelper.java @@ -0,0 +1,154 @@ +package com.example.finzora; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class DBHelper extends SQLiteOpenHelper { + + private static final String DATABASE_NAME = "finzora.db"; + private static final int DATABASE_VERSION = 2; // Versão 2 para incluir Orçamentos + + public DBHelper(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + // Criação da Tabela de Transações + db.execSQL("CREATE TABLE transacoes (id INTEGER PRIMARY KEY AUTOINCREMENT, valor REAL, categoria TEXT, tipo INTEGER, data TEXT)"); + + // Criação da Tabela de Orçamentos + db.execSQL("CREATE TABLE orcamentos (categoria TEXT PRIMARY KEY, valor_limite REAL)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + db.execSQL("DROP TABLE IF EXISTS transacoes"); + db.execSQL("DROP TABLE IF EXISTS orcamentos"); + onCreate(db); + } + + // ========================================== + // MÉTODOS PARA AS TRANSAÇÕES + // ========================================== + + public void adicionarTransacao(Transacao transacao) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("valor", transacao.getValor()); + values.put("categoria", transacao.getCategoria()); + values.put("tipo", transacao.getTipo()); + values.put("data", transacao.getData()); + db.insert("transacoes", null, values); + } + + public List listarTransacoes() { + List lista = new ArrayList<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT * FROM transacoes ORDER BY id DESC", null); + + if (cursor.moveToFirst()) { + do { + lista.add(new Transacao( + cursor.getInt(0), // id + cursor.getFloat(1), // valor + cursor.getString(2),// categoria + cursor.getInt(3), // tipo + cursor.getString(4) // data + )); + } while (cursor.moveToNext()); + } + cursor.close(); + return lista; + } + + public void eliminarTransacao(int id) { + SQLiteDatabase db = this.getWritableDatabase(); + db.delete("transacoes", "id=?", new String[]{String.valueOf(id)}); + } + + // ========================================== + // MÉTODOS PARA OS ORÇAMENTOS + // ========================================== + + public void salvarOrcamento(String categoria, float limite) { + SQLiteDatabase db = this.getWritableDatabase(); + ContentValues values = new ContentValues(); + values.put("categoria", categoria); + values.put("valor_limite", limite); + // CONFLICT_REPLACE garante que se a categoria já existir, ele só atualiza o limite + db.insertWithOnConflict("orcamentos", null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + + public Map getOrcamentosDefinidos() { + Map orcamentos = new HashMap<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT * FROM orcamentos", null); + + if (cursor.moveToFirst()) { + do { + orcamentos.put(cursor.getString(0), cursor.getFloat(1)); + } while (cursor.moveToNext()); + } + cursor.close(); + return orcamentos; + } + + public float getGastoPorCategoria(String categoria) { + SQLiteDatabase db = this.getReadableDatabase(); + // Soma todas as transações que sejam Despesa (tipo=2) na categoria escolhida + Cursor cursor = db.rawQuery("SELECT SUM(valor) FROM transacoes WHERE tipo=2 AND categoria=?", new String[]{categoria}); + float total = 0; + if (cursor.moveToFirst()) { + total = cursor.getFloat(0); + } + cursor.close(); + return total; + } + + // ========================================== + // MATEMÁTICA PARA OS CARTÕES E GRÁFICOS + // ========================================== + + // Calcula todo o dinheiro que entrou (Receitas, tipo = 1) + public float getTotalReceitas() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT SUM(valor) FROM transacoes WHERE tipo=1", null); + float total = 0; + if (cursor.moveToFirst()) total = cursor.getFloat(0); + cursor.close(); + return total; + } + + // Calcula todo o dinheiro que saiu (Despesas, tipo = 2) + public float getTotalDespesas() { + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT SUM(valor) FROM transacoes WHERE tipo=2", null); + float total = 0; + if (cursor.moveToFirst()) total = cursor.getFloat(0); + cursor.close(); + return total; + } + + // Prepara os dados para a aba dos Gráficos (Soma os gastos de cada categoria) + public HashMap getDespesasPorCategoria() { + HashMap mapa = new HashMap<>(); + SQLiteDatabase db = this.getReadableDatabase(); + Cursor cursor = db.rawQuery("SELECT categoria, SUM(valor) FROM transacoes WHERE tipo=2 GROUP BY categoria", null); + + if (cursor.moveToFirst()) { + do { + mapa.put(cursor.getString(0), cursor.getFloat(1)); + } while (cursor.moveToNext()); + } + cursor.close(); + return mapa; + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/DefinicoesActivity.java b/app/src/main/java/com/example/finzora/DefinicoesActivity.java new file mode 100644 index 0000000..f7a040a --- /dev/null +++ b/app/src/main/java/com/example/finzora/DefinicoesActivity.java @@ -0,0 +1,175 @@ +package com.example.finzora; + +import android.app.Dialog; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.Switch; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; + +public class DefinicoesActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + 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); + TextView btnSuporte = findViewById(R.id.btnSuporte); + Button btnTerminarSessao = findViewById(R.id.btnTerminarSessao); + + // --- 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)); + }); + + // --- 2. MAGIA DO MODO ESCURO / CLARO --- + SharedPreferences prefsTema = getSharedPreferences("TemaApp", MODE_PRIVATE); + boolean isModoEscuro = prefsTema.getBoolean("modo_escuro", true); + switchModoEscuro.setChecked(isModoEscuro); + + switchModoEscuro.setOnCheckedChangeListener((buttonView, isChecked) -> { + prefsTema.edit().putBoolean("modo_escuro", isChecked).apply(); + + if (isChecked) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + Toast.makeText(this, "Modo Escuro Ativado 🌙", Toast.LENGTH_SHORT).show(); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + Toast.makeText(this, "Modo Claro Ativado ☀️", Toast.LENGTH_SHORT).show(); + } + }); + + // --- 3. NOTIFICAÇÕES --- + switchNotificacoes.setOnCheckedChangeListener((buttonView, isChecked) -> { + if (isChecked) { + Toast.makeText(this, "Notificações Ligadas 🔔", Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(this, "Notificações Desligadas 🔕", Toast.LENGTH_SHORT).show(); + } + }); + + // --- 4. CENTRO DE SUPORTE (POP-UP) --- + btnSuporte.setOnClickListener(v -> mostrarDialogSuporte()); + + // --- 5. TERMINAR SESSÃO --- + btnTerminarSessao.setOnClickListener(v -> terminarSessao()); + } + + private void mostrarDialogSuporte() { + Dialog dialog = new Dialog(this); + dialog.setContentView(R.layout.dialog_suporte); + 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.findViewById(R.id.cardTutorial).setOnClickListener(v -> { + dialog.dismiss(); + mostrarDialogTutorial(); + }); + + dialog.findViewById(R.id.cardMensagem).setOnClickListener(v -> { + dialog.dismiss(); + mostrarDialogContactar(); + }); + + dialog.findViewById(R.id.cardContactos).setOnClickListener(v -> { + Toast.makeText(this, "Email: suporte@finzora.pt\nTel: +351 800 123 456", Toast.LENGTH_LONG).show(); + }); + + dialog.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(); + } + + private void mostrarDialogTutorial() { + Dialog dialog = new Dialog(this); + dialog.setContentView(R.layout.dialog_tutorial); + 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(); + } + + private void terminarSessao() { + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + prefs.edit().clear().apply(); + + Toast.makeText(this, "Sessão terminada com sucesso!", Toast.LENGTH_SHORT).show(); + + Intent intent = new Intent(this, LoginActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + startActivity(intent); + finish(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/DicasFragment.java b/app/src/main/java/com/example/finzora/DicasFragment.java index f05b067..4b87698 100644 --- a/app/src/main/java/com/example/finzora/DicasFragment.java +++ b/app/src/main/java/com/example/finzora/DicasFragment.java @@ -1,6 +1,163 @@ package com.example.finzora; +import android.content.res.ColorStateList; +import android.graphics.Color; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import java.util.HashMap; +import java.util.Map; 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 LinearLayout layoutDistribuicao; + + private DBHelper dbHelper; + + @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); + progressPoupanca = view.findViewById(R.id.progressPoupanca); + + tvTituloDica1 = view.findViewById(R.id.tvTituloDica1); + tvDescDica1 = view.findViewById(R.id.tvDescDica1); + tvTituloDica2 = view.findViewById(R.id.tvTituloDica2); + tvDescDica2 = view.findViewById(R.id.tvDescDica2); + + layoutDistribuicao = view.findViewById(R.id.layoutDistribuicao); + + dbHelper = new DBHelper(getActivity()); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + analisarFinancas(); + } + + private void analisarFinancas() { + float receitas = dbHelper.getTotalReceitas(); + float despesas = dbHelper.getTotalDespesas(); + + // 1. Atualizar Textos Iniciais + tvDicasReceitas.setText(String.format("€ %.2f", receitas)); + tvDicasDespesas.setText(String.format("€ %.2f", despesas)); + + // 2. Calcular Taxa de Poupança + float taxaPoupanca = 0; + if (receitas > 0) { + taxaPoupanca = ((receitas - despesas) / receitas) * 100; + } + + // Se gastou mais do que ganhou, a taxa é 0 + if (taxaPoupanca < 0) taxaPoupanca = 0; + + tvTaxaPoupanca.setText(String.format("%.1f%%", taxaPoupanca)); + progressPoupanca.setProgress((int) taxaPoupanca); + + // Cores consoante a saúde financeira + if (taxaPoupanca >= 20) { + tvTaxaPoupanca.setTextColor(Color.parseColor("#00E676")); // Verde + progressPoupanca.setProgressTintList(ColorStateList.valueOf(Color.parseColor("#00E676"))); + + 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"))); + + 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."); + } 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!"); + } + + // 3. Descobrir a Categoria mais gasta + HashMap gastosPorCategoria = dbHelper.getDespesasPorCategoria(); + String piorCategoria = "Nenhuma"; + float maiorGasto = 0; + + for (Map.Entry entry : gastosPorCategoria.entrySet()) { + if (entry.getValue() > maiorGasto) { + maiorGasto = entry.getValue(); + piorCategoria = entry.getKey(); + } + } + + 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!"); + } else { + tvTituloDica2.setText("Tudo Controlado ✅"); + tvTituloDica2.setTextColor(Color.parseColor("#00E676")); + tvDescDica2.setText("Ainda não tens despesas suficientes para analisarmos. Continua o bom trabalho!"); + } + + // 4. Construir as barras de Distribuição de Gastos magicamente + layoutDistribuicao.removeAllViews(); // Limpa as barras antigas + + if (despesas > 0) { + for (Map.Entry entry : gastosPorCategoria.entrySet()) { + float valorCat = entry.getValue(); + if (valorCat > 0) { + float percentagem = (valorCat / despesas) * 100; + + // 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 + + // 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); + } + } + } else { + TextView semDespesas = new TextView(getActivity()); + semDespesas.setText("Ainda não existem despesas registadas."); + semDespesas.setTextColor(Color.parseColor("#B0BEC5")); + layoutDistribuicao.addView(semDespesas); + } + } +} \ 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 new file mode 100644 index 0000000..c30b3b8 --- /dev/null +++ b/app/src/main/java/com/example/finzora/EditarPerfilActivity.java @@ -0,0 +1,59 @@ +package com.example.finzora; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +public class EditarPerfilActivity extends AppCompatActivity { + + private EditText editNomePerfil; + private EditText editEmailPerfil; + + @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); + + // Voltar para as definições + btnVoltar.setOnClickListener(v -> finish()); + + // 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(nomeAtual); + editEmailPerfil.setText(emailAtual); + + // 2. GUARDAR OS DADOS NOVOS + btnGuardarPerfil.setOnClickListener(v -> { + String novoNome = editNomePerfil.getText().toString().trim(); + String novoEmail = editEmailPerfil.getText().toString().trim(); + + if (novoNome.isEmpty()) { + editNomePerfil.setError("O nome não pode estar vazio!"); + return; + } + + // Grava na memória (SharedPreferences) + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("nome_usuario", novoNome); + editor.putString("email_usuario", novoEmail); + editor.apply(); + + Toast.makeText(this, "Perfil atualizado com sucesso! 🎉", Toast.LENGTH_SHORT).show(); + finish(); // Fecha o ecrã e volta atrás + }); + } +} \ 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 a7e3774..4f3bd4c 100644 --- a/app/src/main/java/com/example/finzora/GraficosFragment.java +++ b/app/src/main/java/com/example/finzora/GraficosFragment.java @@ -1,6 +1,190 @@ package com.example.finzora; +import android.graphics.Color; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import com.github.mikephil.charting.charts.BarChart; +import com.github.mikephil.charting.charts.PieChart; +import com.github.mikephil.charting.components.XAxis; +import com.github.mikephil.charting.data.BarData; +import com.github.mikephil.charting.data.BarDataSet; +import com.github.mikephil.charting.data.BarEntry; +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 java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; public class GraficosFragment extends Fragment { -} + + private PieChart pieChartDespesas; + private BarChart barChartOrcamento; + private BarChart barChartTendencia; + private DBHelper dbHelper; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_graficos, container, false); + + pieChartDespesas = view.findViewById(R.id.pieChartDespesas); + barChartOrcamento = view.findViewById(R.id.barChartOrcamento); + barChartTendencia = view.findViewById(R.id.barChartTendencia); + dbHelper = new DBHelper(getActivity()); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + carregarPieChart(); + carregarBarChartOrcamento(); + carregarBarChartTendencia(); + } + + // ========================================== + // 1. GRÁFICO CIRCULAR (Despesas por Categoria) + // ========================================== + private void carregarPieChart() { + pieChartDespesas.getDescription().setEnabled(false); + pieChartDespesas.setHoleColor(Color.parseColor("#2C5364")); + pieChartDespesas.getLegend().setTextColor(Color.WHITE); + pieChartDespesas.setCenterText("Despesas"); + pieChartDespesas.setCenterTextColor(Color.WHITE); + pieChartDespesas.setEntryLabelColor(Color.WHITE); + + 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())); + } + + if (entradas.isEmpty()) { pieChartDespesas.clear(); return; } + + PieDataSet dataSet = new PieDataSet(entradas, ""); + dataSet.setColors(new int[]{Color.parseColor("#7C4DFF"), Color.parseColor("#00E5FF"), Color.parseColor("#FFD600"), Color.parseColor("#FF4081")}); + + PieData data = new PieData(dataSet); + data.setValueTextSize(14f); + data.setValueTextColor(Color.WHITE); + pieChartDespesas.setData(data); + pieChartDespesas.animateY(1000); + } + + // ========================================== + // 2. GRÁFICO DE BARRAS (Orçamento vs Gastos) + // ========================================== + private void carregarBarChartOrcamento() { + configurarEstiloBarChart(barChartOrcamento); + + Map orcamentos = dbHelper.getOrcamentosDefinidos(); + ArrayList gastosEntries = new ArrayList<>(); + ArrayList orcamentoEntries = new ArrayList<>(); + ArrayList categorias = new ArrayList<>(); + + int index = 0; + for (Map.Entry entry : orcamentos.entrySet()) { + String categoria = entry.getKey(); + float limite = entry.getValue(); + float gasto = dbHelper.getGastoPorCategoria(categoria); + + categorias.add(categoria); + gastosEntries.add(new BarEntry(index, gasto)); + orcamentoEntries.add(new BarEntry(index, limite)); + index++; + } + + if (categorias.isEmpty()) { barChartOrcamento.clear(); return; } + + BarDataSet setGastos = new BarDataSet(gastosEntries, "Gastos Reais"); + setGastos.setColor(Color.parseColor("#FF4081")); // Rosa (Figma) + setGastos.setValueTextColor(Color.WHITE); + + BarDataSet setOrcamento = new BarDataSet(orcamentoEntries, "Orçamento"); + setOrcamento.setColor(Color.parseColor("#00E5FF")); // Azul (Figma) + setOrcamento.setValueTextColor(Color.WHITE); + + 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; + data.setBarWidth(barWidth); + barChartOrcamento.setData(data); + barChartOrcamento.groupBars(-0.5f, groupSpace, barSpace); + + // Labels no Eixo X + XAxis xAxis = barChartOrcamento.getXAxis(); + xAxis.setValueFormatter(new IndexAxisValueFormatter(categorias)); + xAxis.setAxisMinimum(-0.5f); + xAxis.setAxisMaximum(categorias.size() - 0.5f); + + barChartOrcamento.animateY(1000); + } + + // ========================================== + // 3. GRÁFICO DE BARRAS (Tendência Mensal Geral) + // ========================================== + private void carregarBarChartTendencia() { + 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)); + + BarDataSet setDespesas = new BarDataSet(despesaEntry, "Despesas"); + setDespesas.setColor(Color.parseColor("#FF1744")); // Vermelho + setDespesas.setValueTextColor(Color.WHITE); + + BarDataSet setReceitas = new BarDataSet(receitaEntry, "Receitas"); + setReceitas.setColor(Color.parseColor("#00E676")); // Verde + setReceitas.setValueTextColor(Color.WHITE); + + BarData data = new BarData(setDespesas, setReceitas); + + float groupSpace = 0.3f; float barSpace = 0.05f; float barWidth = 0.3f; + data.setBarWidth(barWidth); + barChartTendencia.setData(data); + barChartTendencia.groupBars(-0.5f, 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); + + barChartTendencia.animateY(1000); + } + + // 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.getAxisLeft().setTextColor(Color.WHITE); + chart.getAxisLeft().setDrawGridLines(true); + chart.getAxisLeft().setGridColor(Color.parseColor("#455A64")); // Linhas de fundo subtis + + XAxis xAxis = chart.getXAxis(); + xAxis.setPosition(XAxis.XAxisPosition.BOTTOM); + xAxis.setTextColor(Color.WHITE); + xAxis.setDrawGridLines(false); + xAxis.setGranularity(1f); + xAxis.setCenterAxisLabels(true); + } +} \ 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 3689b84..d7c387d 100644 --- a/app/src/main/java/com/example/finzora/LoginActivity.java +++ b/app/src/main/java/com/example/finzora/LoginActivity.java @@ -1,36 +1,66 @@ package com.example.finzora; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.textfield.TextInputEditText; +import org.json.JSONObject; +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 LoginActivity extends AppCompatActivity { private TextInputEditText editEmail, editPassword; private Button btnEntrar; - private TextView txtRegistrar; + private TextView txtRegistrar, txtEsqueciPassword; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + 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 + } + + setContentView(R.layout.activity_login); - // 1. Inicializar os componentes da interface inicializarComponentes(); - // 2. Configurar o clique no botão "Entrar" + // Clique no botão "Entrar" -> Agora faz Login de verdade! btnEntrar.setOnClickListener(v -> validarDados()); - // 3. Configurar o clique para ir para a tela de Registo + // 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)); + }); } private void validarDados() { @@ -44,20 +74,81 @@ public class LoginActivity extends AppCompatActivity { editPassword.setError("Introduza a sua palavra-passe"); editPassword.requestFocus(); } else { - // Se tudo estiver OK, por enquanto vamos apenas simular o login - // e saltar para o Dashboard (que será a nossa próxima fase) - Toast.makeText(this, "Login efetuado com sucesso!", Toast.LENGTH_SHORT).show(); - - // Aqui é onde irás validar com a Base de Dados ou Firebase no futuro - irParaDashboard(); + // Desativa o botão enquanto pensa + btnEntrar.setEnabled(false); + btnEntrar.setText("A VERIFICAR DADOS..."); + fazerLoginNoSupabase(email, password); } } + private void fazerLoginNoSupabase(String email, String password) { + OkHttpClient client = new OkHttpClient(); + + 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) + .addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY) + .post(body) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + runOnUiThread(() -> { + Toast.makeText(LoginActivity.this, "Erro de ligação à internet!", Toast.LENGTH_SHORT).show(); + btnEntrar.setEnabled(true); + btnEntrar.setText("INICIAR SESSÃO"); + }); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + final String responseData = response.body().string(); + + runOnUiThread(() -> { + btnEntrar.setEnabled(true); + 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"); + + 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.apply(); + + Toast.makeText(LoginActivity.this, "Bem-vindo de volta!", Toast.LENGTH_SHORT).show(); + irParaDashboard(); + + } 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 + } + }); + } + }); + } + private void irParaDashboard() { - // Altera 'MainActivity' para o nome da tua classe do Dashboard Intent intent = new Intent(LoginActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); startActivity(intent); - finish(); // Fecha o login para o utilizador não voltar atrás ao carregar no botão 'back' + finish(); } private void inicializarComponentes() { @@ -65,5 +156,6 @@ public class LoginActivity extends AppCompatActivity { editPassword = findViewById(R.id.editPassword); btnEntrar = findViewById(R.id.btnEntrar); txtRegistrar = findViewById(R.id.txtRegistrar); + txtEsqueciPassword = findViewById(R.id.txtEsqueciPassword); } } \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/MainActivity.java b/app/src/main/java/com/example/finzora/MainActivity.java index 9e47a25..6740c69 100644 --- a/app/src/main/java/com/example/finzora/MainActivity.java +++ b/app/src/main/java/com/example/finzora/MainActivity.java @@ -1,59 +1,101 @@ package com.example.finzora; import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Color; import android.os.Bundle; import android.widget.Button; +import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; + public class MainActivity extends AppCompatActivity { - private Button btnSair; private TabLayout tabLayout; private ViewPager2 viewPager; + private FloatingActionButton fabAdicionar; private TextView tvNomeUsuario; + private Button btnSair; + + private TextView tvSaldoGeral, tvReceitasGeral, tvDespesasGeral; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); - // 1. PRIMEIRO PASSO OBRIGATÓRIO: Ligar as variáveis ao XML - inicializarComponentes(); + SharedPreferences prefsTema = getSharedPreferences("TemaApp", MODE_PRIVATE); + boolean isModoEscuro = prefsTema.getBoolean("modo_escuro", true); - // 2. Agora sim, podemos tentar mudar o nome com segurança - try { - String nomeRecebido = getIntent().getStringExtra("CHAVE_NOME"); - - // Só altera o texto se realmente tivermos recebido um nome (ex: vindo do Registo) - if (nomeRecebido != null) { - tvNomeUsuario.setText("Olá, " + nomeRecebido + "!"); - } - } catch (Exception e) { - e.printStackTrace(); // Se der erro, ignora e mantém o texto original + if (isModoEscuro) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); } - // 3. Configurar os botões e abas - configurarBotaoSair(); - configurarAbas(); - } + setContentView(R.layout.activity_main); - private void inicializarComponentes() { - btnSair = findViewById(R.id.btnSair); tabLayout = findViewById(R.id.tabLayoutDashboard); viewPager = findViewById(R.id.viewPager); - tvNomeUsuario = findViewById(R.id.tvNomeUsuario); // Certifica-te que este ID existe no XML - } + fabAdicionar = findViewById(R.id.fabAdicionar); + tvNomeUsuario = findViewById(R.id.tvNomeUsuario); + btnSair = findViewById(R.id.btnSair); + + tvSaldoGeral = findViewById(R.id.tvSaldoGeral); + tvReceitasGeral = findViewById(R.id.tvReceitasGeral); + tvDespesasGeral = findViewById(R.id.tvDespesasGeral); + + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + String nome = prefs.getString("nome_usuario", "Investidor"); + tvNomeUsuario.setText("Olá, " + nome); - private void configurarBotaoSair() { btnSair.setOnClickListener(v -> { - Intent intent = new Intent(MainActivity.this, LoginActivity.class); - startActivity(intent); + prefs.edit().clear().apply(); + startActivity(new Intent(this, LoginActivity.class)); finish(); }); + + 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)); + }); + } + + configurarAbas(); + atualizarCartoes(); // Chama a nova função ligada à net! + } + + @Override + protected void onResume() { + super.onResume(); + atualizarCartoes(); + + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + String nome = prefs.getString("nome_usuario", "Investidor"); + if (tvNomeUsuario != null) { + tvNomeUsuario.setText("Olá, " + nome); + } } private void configurarAbas() { @@ -62,16 +104,80 @@ public class MainActivity extends AppCompatActivity { new TabLayoutMediator(tabLayout, viewPager, (tab, position) -> { switch (position) { - case 0: - tab.setText("Transações"); - break; - case 1: - tab.setText("Gráficos"); - break; - case 2: - tab.setText("Dicas"); - break; + 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; } }).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(); + 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 { + String jsonResposta = response.body().string(); + JSONArray jsonArray = new JSONArray(jsonResposta); + + 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; + } + } + + 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)); + } + } + }); + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }); + } } \ 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 new file mode 100644 index 0000000..d3942b2 --- /dev/null +++ b/app/src/main/java/com/example/finzora/OnboardingActivity.java @@ -0,0 +1,102 @@ +package com.example.finzora; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.widget.Button; +import android.widget.TextView; +import androidx.appcompat.app.AppCompatActivity; +import androidx.viewpager2.widget.ViewPager2; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import java.util.ArrayList; +import java.util.List; + +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 TabLayout tabLayoutIndicator; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_onboarding); + + // --- 1. RECUPERAR O NOME DA MEMÓRIA --- + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + // Tenta ler "nome_usuario". Se não existir, mete "Visitante" + String nomeRecuperado = prefs.getString("nome_usuario", "Visitante"); + + // --- 2. CRIAR OS SLIDES --- + List lista = new ArrayList<>(); + + // SLIDE 1: Usamos a variável 'nomeRecuperado' aqui + 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 + )); + + // SLIDE 2 + 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 + lista.add(new OnboardingItem( + "Inteligência Artificial", + "Recebe dicas automáticas baseadas nos teus gastos para poupares mais todos os meses.", + R.drawable.ic_idea + )); + + // --- 3. CONFIGURAR VIEWPAGER E BOTÕES --- + ViewPager2 viewPager = findViewById(R.id.viewPagerOnboarding); + tabLayoutIndicator = findViewById(R.id.tabLayoutIndicator); + btnProximo = findViewById(R.id.btnProximo); + btnSaltar = findViewById(R.id.btnSaltar); + + onboardingAdapter = new OnboardingAdapter(lista); + viewPager.setAdapter(onboardingAdapter); + + // Ligar as barras de progresso (TabLayout) + new TabLayoutMediator(tabLayoutIndicator, viewPager, (tab, position) -> { + // Deixar vazio (só queremos as barras, sem texto) + }).attach(); + + // Lógica do Botão Próximo + btnProximo.setOnClickListener(v -> { + if (viewPager.getCurrentItem() + 1 < onboardingAdapter.getItemCount()) { + viewPager.setCurrentItem(viewPager.getCurrentItem() + 1); + } else { + finalizarOnboarding(); + } + }); + + // Lógica do Botão Saltar + btnSaltar.setOnClickListener(v -> finalizarOnboarding()); + + // Mudar texto "Próximo" para "Começar" na última página + viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + if (position == onboardingAdapter.getItemCount() - 1) { + btnProximo.setText("Começar"); + } else { + btnProximo.setText("Próximo"); + } + } + }); + } + + 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(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/OnboardingAdapter.java b/app/src/main/java/com/example/finzora/OnboardingAdapter.java new file mode 100644 index 0000000..d35b59c --- /dev/null +++ b/app/src/main/java/com/example/finzora/OnboardingAdapter.java @@ -0,0 +1,55 @@ +package com.example.finzora; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + +public class OnboardingAdapter extends RecyclerView.Adapter { + + private List onboardingItems; + + public OnboardingAdapter(List onboardingItems) { + this.onboardingItems = onboardingItems; + } + + @NonNull + @Override + public OnboardingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_onboarding, parent, false); + return new OnboardingViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull OnboardingViewHolder holder, int position) { + holder.setOnboardingData(onboardingItems.get(position)); + } + + @Override + public int getItemCount() { + return onboardingItems.size(); + } + + class OnboardingViewHolder extends RecyclerView.ViewHolder { + private TextView textTitulo; + private TextView textDescricao; + private ImageView imageOnboarding; + + OnboardingViewHolder(@NonNull View itemView) { + super(itemView); + textTitulo = itemView.findViewById(R.id.textTitulo); + textDescricao = itemView.findViewById(R.id.textDescricao); + imageOnboarding = itemView.findViewById(R.id.imgOnboarding); + } + + void setOnboardingData(OnboardingItem item) { + textTitulo.setText(item.getTitulo()); + textDescricao.setText(item.getDescricao()); + imageOnboarding.setImageResource(item.getImagem()); + } + } +} diff --git a/app/src/main/java/com/example/finzora/OnboardingItem.java b/app/src/main/java/com/example/finzora/OnboardingItem.java new file mode 100644 index 0000000..458b311 --- /dev/null +++ b/app/src/main/java/com/example/finzora/OnboardingItem.java @@ -0,0 +1,16 @@ +package com.example.finzora; + +public class OnboardingItem { + private String titulo, descricao; + private int imagem; + + public OnboardingItem(String titulo, String descricao, int imagem) { + this.titulo = titulo; + this.descricao = descricao; + this.imagem = imagem; + } + + public String getTitulo() { return titulo; } + public String getDescricao() { return descricao; } + public int getImagem() { return imagem; } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/OrcamentoAdapter.java b/app/src/main/java/com/example/finzora/OrcamentoAdapter.java new file mode 100644 index 0000000..6577704 --- /dev/null +++ b/app/src/main/java/com/example/finzora/OrcamentoAdapter.java @@ -0,0 +1,78 @@ +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.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 OrcamentoAdapter(List> lista, DBHelper db) { + this.listaOrcamentos = lista; + this.dbHelper = db; + } + + @NonNull + @Override + public OrcamentoViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_orcamento, parent, false); + return new OrcamentoViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull OrcamentoViewHolder holder, int position) { + Map.Entry entry = listaOrcamentos.get(position); + String categoria = entry.getKey(); + float limite = entry.getValue(); + + // Calcular quanto já gastou + float gasto = dbHelper.getGastoPorCategoria(categoria); + float restante = limite - gasto; + int percentagem = (int) ((gasto / limite) * 100); + if (percentagem > 100) percentagem = 100; + + // Atualizar Textos + holder.tvCategoria.setText(categoria); + holder.tvValores.setText(String.format("€ %.2f / € %.2f", gasto, 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 + } 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 + } + } + + @Override + public int getItemCount() { + return listaOrcamentos.size(); + } + + static class OrcamentoViewHolder extends RecyclerView.ViewHolder { + TextView tvCategoria, tvValores, tvRestante; + ProgressBar progress; + + public OrcamentoViewHolder(@NonNull View itemView) { + super(itemView); + tvCategoria = itemView.findViewById(R.id.tvCatOrcamento); + tvValores = itemView.findViewById(R.id.tvValoresOrcamento); + tvRestante = itemView.findViewById(R.id.tvRestante); + progress = itemView.findViewById(R.id.progressOrcamento); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/OrcamentoFragment.java b/app/src/main/java/com/example/finzora/OrcamentoFragment.java new file mode 100644 index 0000000..fa8b1c8 --- /dev/null +++ b/app/src/main/java/com/example/finzora/OrcamentoFragment.java @@ -0,0 +1,98 @@ +package com.example.finzora; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.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 java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class OrcamentoFragment extends Fragment { + + private Spinner spinnerCategoria; + private EditText editLimite; + private Button btnSalvar; + private RecyclerView recyclerOrcamentos; + private DBHelper dbHelper; + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + // Carrega o layout XML correto + View view = inflater.inflate(R.layout.fragment_orcamento, container, false); + + // Liga os componentes do Java aos IDs do XML + // SE AQUI FICAR VERMELHO, É PORQUE O XML NÃO FOI GUARDADO + spinnerCategoria = view.findViewById(R.id.spinnerOrcamento); + editLimite = view.findViewById(R.id.editLimite); + btnSalvar = view.findViewById(R.id.btnDefinirOrcamento); + recyclerOrcamentos = view.findViewById(R.id.recyclerOrcamentos); + + // Configura a lista + recyclerOrcamentos.setLayoutManager(new LinearLayoutManager(getActivity())); + + // Inicia a base de dados + dbHelper = new DBHelper(getActivity()); + + // Configurações iniciais + configurarSpinner(); + + // Ação do botão + btnSalvar.setOnClickListener(v -> salvarOrcamento()); + + // Mostrar dados + carregarOrcamentos(); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + carregarOrcamentos(); + } + + // 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 salvarOrcamento() { + String limiteStr = editLimite.getText().toString(); + if (limiteStr.isEmpty()) { + editLimite.setError("Define um valor"); + return; + } + + String categoria = spinnerCategoria.getSelectedItem().toString(); + float limite = Float.parseFloat(limiteStr); + + dbHelper.salvarOrcamento(categoria, limite); + + Toast.makeText(getActivity(), "Orçamento definido!", Toast.LENGTH_SHORT).show(); + editLimite.setText(""); // Limpar campo + + carregarOrcamentos(); // Atualizar lista + } + + private void carregarOrcamentos() { + Map orcamentosMap = dbHelper.getOrcamentosDefinidos(); + List> lista = new ArrayList<>(orcamentosMap.entrySet()); + + OrcamentoAdapter adapter = new OrcamentoAdapter(lista, dbHelper); + recyclerOrcamentos.setAdapter(adapter); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/ProfileActivity.java b/app/src/main/java/com/example/finzora/ProfileActivity.java new file mode 100644 index 0000000..8a232ad --- /dev/null +++ b/app/src/main/java/com/example/finzora/ProfileActivity.java @@ -0,0 +1,151 @@ +package com.example.finzora; + +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.widget.Button; +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.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import com.google.android.material.textfield.TextInputEditText; +import java.util.Random; + +public class ProfileActivity extends AppCompatActivity { + + private ImageView imgPerfil; + private TextInputEditText editNomePerfil, editBio; + private Button btnEscolherFoto, btnGuardarPerfil; + private TextView btnSaltarPerfil; + private Uri imagemUriSelecionada = null; + + // Variável de controlo + private boolean vindoDoMenu = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_profile); + + // Verifica de onde viemos + vindoDoMenu = getIntent().getBooleanExtra("origem_menu", false); + + inicializarComponentes(); + adaptarInterface(); // Ajusta os textos dos botões + carregarDadosAtuais(); + + // 1. Escolher Foto + ActivityResultLauncher galleryLauncher = registerForActivityResult( + new ActivityResultContracts.GetContent(), + uri -> { + if (uri != null) { + imagemUriSelecionada = uri; + imgPerfil.setImageURI(uri); + imgPerfil.setColorFilter(null); + } + } + ); + + btnEscolherFoto.setOnClickListener(v -> galleryLauncher.launch("image/*")); + + // 2. Guardar + btnGuardarPerfil.setOnClickListener(v -> salvarPerfil()); + + // 3. Botão Saltar / Cancelar + btnSaltarPerfil.setOnClickListener(v -> { + if (vindoDoMenu) { + // Se veio do menu, "Saltar" funciona como "Cancelar" + finish(); + } else { + // Se veio do Tutorial, salta mas mostra as boas-vindas + mostrarMensagemBoasVindas("Investidor"); + } + }); + } + + private void inicializarComponentes() { + imgPerfil = findViewById(R.id.imgPerfil); + editNomePerfil = findViewById(R.id.editNomePerfil); + editBio = findViewById(R.id.editBio); + btnEscolherFoto = findViewById(R.id.btnEscolherFoto); + btnGuardarPerfil = findViewById(R.id.btnGuardarPerfil); + btnSaltarPerfil = findViewById(R.id.btnSaltarPerfil); + } + + private void adaptarInterface() { + if (vindoDoMenu) { + // Se já estamos na app + btnSaltarPerfil.setText("Cancelar"); + btnGuardarPerfil.setText("Atualizar Perfil"); + } else { + // Se é o tutorial inicial + btnSaltarPerfil.setText("Saltar por agora"); + btnGuardarPerfil.setText("Guardar e Entrar"); + } + } + + private void carregarDadosAtuais() { + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + editNomePerfil.setText(prefs.getString("nome_usuario", "")); + editBio.setText(prefs.getString("bio_usuario", "")); + + String fotoStr = prefs.getString("foto_usuario", null); + if (fotoStr != null) { + imgPerfil.setImageURI(Uri.parse(fotoStr)); + imgPerfil.setColorFilter(null); + } + } + + private void salvarPerfil() { + String novoNome = editNomePerfil.getText().toString().trim(); + String novaBio = editBio.getText().toString().trim(); + + if (novoNome.isEmpty()) { + editNomePerfil.setError("O nome não pode estar vazio"); + return; + } + + SharedPreferences.Editor editor = getSharedPreferences("DadosUtilizador", MODE_PRIVATE).edit(); + editor.putString("nome_usuario", novoNome); + editor.putString("bio_usuario", novaBio); + if (imagemUriSelecionada != null) { + editor.putString("foto_usuario", imagemUriSelecionada.toString()); + } + editor.apply(); + + // --- LÓGICA DE SAÍDA --- + if (vindoDoMenu) { + // Edição simples: avisa e volta para trás + Toast.makeText(this, "Perfil atualizado!", Toast.LENGTH_SHORT).show(); + finish(); + } else { + // Primeira vez: Mostra a mensagem inspiradora + mostrarMensagemBoasVindas(novoNome); + } + } + + private void mostrarMensagemBoasVindas(String nome) { + String[] frases = { + "\"Não poupes o que resta depois de gastar, mas gasta o que resta depois de poupar.\" – Warren Buffett", + "\"O segredo para ficar à frente é começar.\" – Mark Twain", + "\"A tua liberdade financeira começa hoje!\" 🚀" + }; + String fraseDoDia = frases[new Random().nextInt(frases.length)]; + + new AlertDialog.Builder(this) + .setTitle("Bem-vindo(a), " + nome + "!") + .setMessage(fraseDoDia + "\n\nO teu perfil está pronto. Vamos dominar as tuas finanças?") + .setPositiveButton("VAMOS LÁ!", (dialog, which) -> { + Intent intent = new Intent(ProfileActivity.this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + }) + .setCancelable(false) + .show(); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/RecuperarPasswordActivity.java b/app/src/main/java/com/example/finzora/RecuperarPasswordActivity.java new file mode 100644 index 0000000..ec5775a --- /dev/null +++ b/app/src/main/java/com/example/finzora/RecuperarPasswordActivity.java @@ -0,0 +1,90 @@ +package com.example.finzora; + +import android.os.Bundle; +import android.text.TextUtils; +import android.widget.Button; +import android.widget.ImageView; +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 RecuperarPasswordActivity extends AppCompatActivity { + + private TextInputEditText editEmailRecuperar; + private Button btnEnviarEmail; + private ImageView btnVoltar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recuperar_password); + + editEmailRecuperar = findViewById(R.id.editEmailRecuperar); + btnEnviarEmail = findViewById(R.id.btnEnviarEmail); + btnVoltar = findViewById(R.id.btnVoltarRecuperar); + + // Voltar para trás + btnVoltar.setOnClickListener(v -> finish()); + + btnEnviarEmail.setOnClickListener(v -> { + String email = editEmailRecuperar.getText().toString().trim(); + if (TextUtils.isEmpty(email)) { + editEmailRecuperar.setError("Introduz o teu email"); + editEmailRecuperar.requestFocus(); + } else { + btnEnviarEmail.setEnabled(false); + btnEnviarEmail.setText("A ENVIAR..."); + pedirRecuperacaoPassword(email); + } + }); + } + + private void pedirRecuperacaoPassword(String email) { + OkHttpClient client = new OkHttpClient(); + String json = "{\"email\":\"" + email + "\"}"; + RequestBody body = RequestBody.create(json, MediaType.parse("application/json; charset=utf-8")); + + Request request = new Request.Builder() + .url(SupabaseConfig.SUPABASE_URL + "/auth/v1/recover") + .addHeader("apikey", SupabaseConfig.SUPABASE_KEY) + .addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY) + .post(body) + .build(); + + client.newCall(request).enqueue(new Callback() { + @Override + public void onFailure(@NonNull Call call, @NonNull IOException e) { + runOnUiThread(() -> { + Toast.makeText(RecuperarPasswordActivity.this, "Erro de ligação à internet!", Toast.LENGTH_SHORT).show(); + btnEnviarEmail.setEnabled(true); + btnEnviarEmail.setText("ENVIAR LINK MÁGICO"); + }); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + runOnUiThread(() -> { + btnEnviarEmail.setEnabled(true); + btnEnviarEmail.setText("ENVIAR LINK MÁGICO"); + if (response.isSuccessful()) { + Toast.makeText(RecuperarPasswordActivity.this, "Email enviado! Verifica a caixa de entrada.", Toast.LENGTH_LONG).show(); + finish(); // Fecha esta tela e volta ao Login + } else { + Toast.makeText(RecuperarPasswordActivity.this, "Erro: Verifica se o email está correto ou se a conta existe.", Toast.LENGTH_LONG).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 ebe8c97..961ba20 100644 --- a/app/src/main/java/com/example/finzora/RegisterActivity.java +++ b/app/src/main/java/com/example/finzora/RegisterActivity.java @@ -1,17 +1,30 @@ package com.example.finzora; import android.content.Intent; +import android.content.SharedPreferences; import android.os.Bundle; import android.text.TextUtils; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; +import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import com.google.android.material.textfield.TextInputEditText; +import org.json.JSONObject; +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 RegisterActivity extends AppCompatActivity { - private TextInputEditText editNome, editEmail, editPassword, editConfirmPassword; + private TextInputEditText editNome, editEmailRegister, editPassRegister, editConfirmPass; private Button btnCriarConta; private TextView txtLogin; @@ -22,45 +35,123 @@ public class RegisterActivity extends AppCompatActivity { inicializarComponentes(); - // Volta para a tela de Login + // Voltar para o Login txtLogin.setOnClickListener(v -> finish()); - // Valida e cria a conta + // Clicar em Criar Conta btnCriarConta.setOnClickListener(v -> validarDados()); } private void validarDados() { String nome = editNome.getText().toString().trim(); - String email = editEmail.getText().toString().trim(); - String password = editPassword.getText().toString().trim(); - String confirmPass = editConfirmPassword.getText().toString().trim(); + String email = editEmailRegister.getText().toString().trim(); + String password = editPassRegister.getText().toString().trim(); + String confirmPass = editConfirmPass.getText().toString().trim(); if (TextUtils.isEmpty(nome)) { editNome.setError("Introduza o seu nome"); - } else if (TextUtils.isEmpty(email)) { - editEmail.setError("Introduza um email válido"); - } else if (TextUtils.isEmpty(password)) { - editPassword.setError("Defina uma palavra-passe"); - } else if (password.length() < 6) { - editPassword.setError("A senha deve ter pelo menos 6 caracteres"); - } else if (!password.equals(confirmPass)) { - editConfirmPassword.setError("As palavras-passe não coincidem"); - } else { - - Toast.makeText(this, "Conta criada com sucesso!", Toast.LENGTH_SHORT).show(); - Intent intent = new Intent(RegisterActivity.this, MainActivity.class); - intent.putExtra("CHAVE_NOME", nome); - startActivity(intent); - finish(); + return; } + if (TextUtils.isEmpty(email)) { + editEmailRegister.setError("Introduza um email válido"); + return; + } + if (TextUtils.isEmpty(password)) { + editPassRegister.setError("Defina uma palavra-passe"); + return; + } + if (password.length() < 6) { + editPassRegister.setError("Mínimo 6 caracteres"); + return; + } + if (!password.equals(confirmPass)) { + editConfirmPass.setError("As senhas não coincidem"); + return; + } + + btnCriarConta.setEnabled(false); + btnCriarConta.setText("A criar conta na nuvem..."); + + registarNoSupabase(nome, email, password); + } + + 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")); + + Request request = new Request.Builder() + .url(SupabaseConfig.SUPABASE_URL + "/auth/v1/signup") + .addHeader("apikey", SupabaseConfig.SUPABASE_KEY) + .addHeader("Authorization", "Bearer " + SupabaseConfig.SUPABASE_KEY) + .post(body) + .build(); + + client.newCall(request).enqueue(new Callback() { + @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(); + resetBotao(); + }); + } + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { + final String responseData = response.body().string(); + + 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 + SharedPreferences prefs = getSharedPreferences("DadosUtilizador", MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + editor.putString("nome_usuario", nome); + editor.putString("user_id", userId); + editor.apply(); + + Toast.makeText(RegisterActivity.this, "Sucesso! Bem-vindo à nuvem.", Toast.LENGTH_SHORT).show(); + + // Avançar para o Onboarding + 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(); + } + } 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(); + } catch (Exception e) { + Toast.makeText(RegisterActivity.this, "Erro desconhecido: " + responseData, Toast.LENGTH_LONG).show(); + } + resetBotao(); + } + }); + } + }); + } + + private void resetBotao() { + btnCriarConta.setEnabled(true); + btnCriarConta.setText("CRIAR CONTA"); } private void inicializarComponentes() { editNome = findViewById(R.id.editNome); - editEmail = findViewById(R.id.editEmailRegister); - editPassword = findViewById(R.id.editPassRegister); - editConfirmPassword = findViewById(R.id.editConfirmPass); + editEmailRegister = findViewById(R.id.editEmailRegister); + editPassRegister = findViewById(R.id.editPassRegister); + editConfirmPass = findViewById(R.id.editConfirmPass); btnCriarConta = findViewById(R.id.btnCriarConta); txtLogin = findViewById(R.id.txtLogin); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/example/finzora/SupabaseConfig.java b/app/src/main/java/com/example/finzora/SupabaseConfig.java new file mode 100644 index 0000000..8e65037 --- /dev/null +++ b/app/src/main/java/com/example/finzora/SupabaseConfig.java @@ -0,0 +1,8 @@ +package com.example.finzora; + +public class SupabaseConfig { + + public static final String SUPABASE_URL = "https://cnxbsherdagpdpjhbtae.supabase.co"; + public static final String SUPABASE_KEY = "sb_secret_bJg45XiAMz-KTyXw2FmMMA_HF97o_aV"; + +} diff --git a/app/src/main/java/com/example/finzora/Transacao.java b/app/src/main/java/com/example/finzora/Transacao.java new file mode 100644 index 0000000..884a331 --- /dev/null +++ b/app/src/main/java/com/example/finzora/Transacao.java @@ -0,0 +1,38 @@ +package com.example.finzora; + +public class Transacao { + private int id; + private float valor; + private String categoria; + private int tipo; // 1 = Receita, 2 = Despesa + private String data; + + // --- CONSTRUTOR 1: Para ler da Base de Dados (TEM ID) --- + public Transacao(int id, float valor, String categoria, int tipo, String data) { + this.id = id; + this.valor = valor; + this.categoria = categoria; + this.tipo = tipo; + this.data = data; + } + + // --- CONSTRUTOR 2: Para criar nova transação (NÃO TEM ID) --- + public Transacao(float valor, String categoria, int tipo, String data) { + this.valor = valor; + this.categoria = categoria; + this.tipo = tipo; + this.data = data; + } + + // --- GETTERS (Para os outros lerem) --- + public int getId() { return id; } + public float getValor() { return valor; } + public String getCategoria() { return categoria; } + public int getTipo() { return tipo; } + public String getData() { return data; } + + // --- SETTERS (Opcional, mas evita erros se algum código antigo os chamar) --- + public void setId(int id) { this.id = id; } + public void setValor(float valor) { this.valor = valor; } + public void setCategoria(String categoria) { this.categoria = categoria; } +} \ 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 new file mode 100644 index 0000000..fedf1a0 --- /dev/null +++ b/app/src/main/java/com/example/finzora/TransacoesAdapter.java @@ -0,0 +1,79 @@ +package com.example.finzora; + +import android.content.Context; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + +public class TransacoesAdapter extends RecyclerView.Adapter { + + private List listaTransacoes; + private Context context; + private TransacoesFragment fragment; + + // O teu construtor mantém-se igual! + public TransacoesAdapter(List lista, Context context, TransacoesFragment fragment) { + this.listaTransacoes = lista; + this.context = context; + this.fragment = fragment; + } + + @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); + } + + @Override + public void onBindViewHolder(@NonNull MyViewHolder holder, int position) { + Transacao transacao = listaTransacoes.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); + } + + // O botão de apagar agora chama-se btnEliminar no XML + holder.btnEliminar.setOnClickListener(v -> { + if (fragment != null) fragment.confirmarExclusao(transacao); + }); + } + + @Override + public int getItemCount() { + return listaTransacoes.size(); + } + + public class MyViewHolder extends RecyclerView.ViewHolder { + // --- AQUI ESTÃO OS NOVOS NOMES DO XML --- + TextView tvDescricao, tvValor, tvData; + ImageView imgIcone, btnEliminar; + + public MyViewHolder(@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 f9a0849..01afa5d 100644 --- a/app/src/main/java/com/example/finzora/TransacoesFragment.java +++ b/app/src/main/java/com/example/finzora/TransacoesFragment.java @@ -1,6 +1,158 @@ 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.Toast; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; public class TransacoesFragment extends Fragment { -} + + private RecyclerView recyclerTransacoes; + private TransacoesAdapter adapter; + + @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())); + return view; + } + + @Override + public void onResume() { + super.onResume(); + 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 + + 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") + .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) { + if (getActivity() != null) { + getActivity().runOnUiThread(() -> + Toast.makeText(getActivity(), "Erro de internet ao carregar transações.", 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 { + 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"); + + Transacao t = new Transacao(valor, categoria, tipo, data); + t.setId(id); // Guarda o ID verdadeiro da nuvem para podermos apagar depois! + 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); + recyclerTransacoes.setAdapter(adapter); + }); + } + + } catch (Exception e) { + e.printStackTrace(); + } + } + } + }); + } + + // ==================================================================== + // APAGAR TRANSAÇÃO NA NUVEM + // ==================================================================== + public void confirmarExclusao(Transacao transacao) { + new AlertDialog.Builder(getActivity()) + .setTitle("Eliminar Transação") + .setMessage("Apagar " + transacao.getCategoria() + "?") + .setPositiveButton("Sim", (dialog, which) -> { + + 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(); + + 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 das nuvens!", Toast.LENGTH_SHORT).show(); + carregarDadosDoSupabase(); // Recarrega a lista + + // Atualiza os cartões na MainActivity + if (getActivity() instanceof MainActivity) { + ((MainActivity) getActivity()).atualizarCartoes(); + } + }); + } + } + }); + + }) + .setNegativeButton("Não", null) + .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 bcdee36..f579958 100644 --- a/app/src/main/java/com/example/finzora/ViewPagerAdapter.java +++ b/app/src/main/java/com/example/finzora/ViewPagerAdapter.java @@ -14,17 +14,17 @@ public class ViewPagerAdapter extends FragmentStateAdapter { @NonNull @Override public Fragment createFragment(int position) { - // Aqui definimos a ordem das abas switch (position) { - case 0: return new TransacoesFragment(); // Aba 1 - case 1: return new GraficosFragment(); // Aba 2 - case 2: return new DicasFragment(); // Aba 3 + case 0: return new TransacoesFragment(); + case 1: return new OrcamentoFragment(); + case 2: return new GraficosFragment(); + case 3: return new DicasFragment(); default: return new TransacoesFragment(); } } @Override public int getItemCount() { - return 3; // Temos 3 abas no total + return 4; // Total de 4 abas } -} +} \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_card_tech.xml b/app/src/main/res/drawable/bg_card_tech.xml new file mode 100644 index 0000000..8492834 --- /dev/null +++ b/app/src/main/res/drawable/bg_card_tech.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_circle_icon.xml b/app/src/main/res/drawable/bg_circle_icon.xml new file mode 100644 index 0000000..2374ebe --- /dev/null +++ b/app/src/main/res/drawable/bg_circle_icon.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_icone_transacao.xml b/app/src/main/res/drawable/bg_icone_transacao.xml new file mode 100644 index 0000000..ac3ad52 --- /dev/null +++ b/app/src/main/res/drawable/bg_icone_transacao.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_tech_gradient.xml b/app/src/main/res/drawable/bg_tech_gradient.xml new file mode 100644 index 0000000..3ac40a9 --- /dev/null +++ b/app/src/main/res/drawable/bg_tech_gradient.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/eliminar.png b/app/src/main/res/drawable/eliminar.png new file mode 100644 index 0000000000000000000000000000000000000000..2993cffb29dabd4bd44c45286ba1f66a366a24ea GIT binary patch literal 7842 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 v@;vP(R_q0%s^(>F6B;(G`WmWF^2ZhI#n3F + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_baseline_delete_24.xml b/app/src/main/res/drawable/ic_baseline_delete_24.xml new file mode 100644 index 0000000..a8b409b --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_delete_24.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_carteira_tech.xml b/app/src/main/res/drawable/ic_carteira_tech.xml new file mode 100644 index 0000000..65cae36 --- /dev/null +++ b/app/src/main/res/drawable/ic_carteira_tech.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_chart.xml b/app/src/main/res/drawable/ic_chart.xml new file mode 100644 index 0000000..134828b --- /dev/null +++ b/app/src/main/res/drawable/ic_chart.xml @@ -0,0 +1,10 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_idea.xml b/app/src/main/res/drawable/ic_idea.xml new file mode 100644 index 0000000..7eb050a --- /dev/null +++ b/app/src/main/res/drawable/ic_idea.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_pulse.xml b/app/src/main/res/drawable/ic_pulse.xml new file mode 100644 index 0000000..0b3c421 --- /dev/null +++ b/app/src/main/res/drawable/ic_pulse.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_pap.xml b/app/src/main/res/drawable/ic_settings_pap.xml new file mode 100644 index 0000000..ebb9aa4 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_pap.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_wallet.xml b/app/src/main/res/drawable/ic_wallet.xml index d7707dd..76f73d1 100644 --- a/app/src/main/res/drawable/ic_wallet.xml +++ b/app/src/main/res/drawable/ic_wallet.xml @@ -1,5 +1,9 @@ - - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_neon.xml b/app/src/main/res/drawable/progress_neon.xml new file mode 100644 index 0000000..d15ef06 --- /dev/null +++ b/app/src/main/res/drawable/progress_neon.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/progress_savings.xml b/app/src/main/res/drawable/progress_savings.xml new file mode 100644 index 0000000..aea0751 --- /dev/null +++ b/app/src/main/res/drawable/progress_savings.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/wallet.png b/app/src/main/res/drawable/wallet.png new file mode 100644 index 0000000000000000000000000000000000000000..dd3dcdb872d880680123ca754dee397b74ce57bf GIT binary patch literal 3371 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 + + + + + + + + + + +