diff --git a/app/src/main/java/pt/epvc/gestodedespesas/AddExpenseActivity.java b/app/src/main/java/pt/epvc/gestodedespesas/AddExpenseActivity.java index dc3f7be..0dd2249 100644 --- a/app/src/main/java/pt/epvc/gestodedespesas/AddExpenseActivity.java +++ b/app/src/main/java/pt/epvc/gestodedespesas/AddExpenseActivity.java @@ -8,12 +8,16 @@ import androidx.appcompat.app.AppCompatActivity; // Importa classe base para Act import com.google.android.material.button.MaterialButton; // Importa botão Material Design import com.google.android.material.textfield.TextInputEditText; // Importa campo de texto Material Design +import com.google.android.material.textfield.MaterialAutoCompleteTextView; // Campo com menu suspenso import java.text.SimpleDateFormat; // Importa classe para formatação de datas import java.util.Date; // Importa classe para trabalhar com datas import java.util.Locale; // Importa classe para configurações de localização +import java.util.Arrays; // Utilitário para trabalhar com arrays +import java.util.ArrayList; // Lista mutável import pt.epvc.gestodedespesas.data.DatabaseHelper; // DatabaseHelper movido para pacote data +import android.widget.ArrayAdapter; // Adapter para preencher o dropdown /** * AddExpenseActivity - Tela de Adicionar/Editar Despesas @@ -30,7 +34,8 @@ import pt.epvc.gestodedespesas.data.DatabaseHelper; // DatabaseHelper movido par public class AddExpenseActivity extends AppCompatActivity { // Declara classe que estende AppCompatActivity // Views da interface - campos de entrada do formulário - private TextInputEditText etDescription, etAmount, etCategory, etDate, etNotes; // Declara campos de texto para entrada de dados + private TextInputEditText etDescription, etAmount, etDate, etNotes; // Declara campos de texto para entrada de dados + private MaterialAutoCompleteTextView etCategory; // Campo de categoria com dropdown private MaterialButton btnSave, btnCancel; // Declara botões de ação // Objetos para funcionalidade @@ -59,6 +64,8 @@ public class AddExpenseActivity extends AppCompatActivity { // Declara classe qu // Para nova despesa, define a data atual como padrão SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()); // Cria formato de data brasileiro etDate.setText(sdf.format(new Date())); // Define data atual no campo de data + // Categoria padrão opcional (ex.: "Outros") para facilitar + etCategory.setText("Outros", false); } } @@ -70,13 +77,26 @@ public class AddExpenseActivity extends AppCompatActivity { // Declara classe qu // Conecta os campos de entrada de texto etDescription = findViewById(R.id.etDescription); // Busca campo de descrição por ID e atribui à variável etAmount = findViewById(R.id.etAmount); // Busca campo de valor por ID e atribui à variável - etCategory = findViewById(R.id.etCategory); // Busca campo de categoria por ID e atribui à variável + etCategory = findViewById(R.id.etCategory); // Busca campo de categoria (dropdown) por ID etDate = findViewById(R.id.etDate); // Busca campo de data por ID e atribui à variável etNotes = findViewById(R.id.etNotes); // Busca campo de notas por ID e atribui à variável // Conecta os botões de ação btnSave = findViewById(R.id.btnSave); // Busca botão salvar por ID e atribui à variável btnCancel = findViewById(R.id.btnCancel); // Busca botão cancelar por ID e atribui à variável + + // Configura o dropdown de categorias com as mesmas opções do filtro + String[] allCategories = getResources().getStringArray(R.array.expense_categories); // Inclui "Todas" + ArrayList formCategories = new ArrayList<>(Arrays.asList(allCategories)); + formCategories.remove("Todas"); // Remove "Todas" do formulário (apenas para filtro) + ArrayAdapter categoryAdapter = new ArrayAdapter<>( + this, + android.R.layout.simple_list_item_1, + formCategories + ); + etCategory.setAdapter(categoryAdapter); + etCategory.setOnClickListener(v -> etCategory.showDropDown()); // Abre ao tocar + etCategory.setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus) etCategory.showDropDown(); }); } /** diff --git a/app/src/main/java/pt/epvc/gestodedespesas/ExpensesListActivity.java b/app/src/main/java/pt/epvc/gestodedespesas/ExpensesListActivity.java index 00d7e67..afd782f 100644 --- a/app/src/main/java/pt/epvc/gestodedespesas/ExpensesListActivity.java +++ b/app/src/main/java/pt/epvc/gestodedespesas/ExpensesListActivity.java @@ -15,7 +15,6 @@ import androidx.core.view.WindowInsetsCompat; // Insets de janela import androidx.recyclerview.widget.LinearLayoutManager; // Layout vertical import androidx.recyclerview.widget.RecyclerView; // Lista eficiente -import com.google.android.material.floatingactionbutton.FloatingActionButton; // Botão flutuante import com.google.android.material.button.MaterialButton; // Botão Material import java.text.DecimalFormat; // Formatar valores monetários @@ -34,13 +33,12 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd private RecyclerView recyclerViewExpenses; // Lista de despesas private TextView tvTotal, tvExpenseCount, tvAverage; // Indicadores de total, contagem e média - private FloatingActionButton fabAddExpense; // Botão flutuante para adicionar private MaterialButton btnFilter; // Botão para filtrar por categoria private DatabaseHelper databaseHelper; // Acesso ao SQLite private ExpenseAdapter expenseAdapter; // Adapter do RecyclerView private List expenseList; // Dados das despesas private DecimalFormat currencyFormat = new DecimalFormat("€#,##0.00"); // Formatação em euro - private boolean isShowingEmptyState = false; // Controle de qual layout está visível (lista ou estado vazio) + private View emptyStateView; // Container do empty state incluído no layout /** * onCreate: configura layout, Edge-to-Edge e listeners de insets, inicializa @@ -50,8 +48,19 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd protected void onCreate(Bundle savedInstanceState) { // Ciclo de vida: criação da Activity super.onCreate(savedInstanceState); // Chama implementação base EdgeToEdge.enable(this); // Ativa layout de ponta a ponta - databaseHelper = new DatabaseHelper(this); // Instancia acesso ao DB (SQLiteHelper) - renderLayoutByData(); // Decide e aplica o layout conforme existência de despesas + setContentView(R.layout.activity_expenses_list); // Usa layout principal sempre + try { + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + } catch (Exception ignored) {} + databaseHelper = new DatabaseHelper(this); // Instancia acesso ao DB + initializeViews(); // Faz bind das views (inclui empty state) + setupRecyclerView(); // Configura lista + setupClickListeners(); // Configura cliques (filtro e empty state) + loadExpenses(); // Carrega dados iniciais } /** @@ -63,8 +72,8 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd tvTotal = findViewById(R.id.tvTotal); // Texto do total gasto tvExpenseCount = findViewById(R.id.tvExpenseCount); // Texto da contagem tvAverage = findViewById(R.id.tvAverage); // Texto da média - fabAddExpense = findViewById(R.id.fabAddExpense); // FAB adicionar btnFilter = findViewById(R.id.btnFilter); // Botão filtrar + emptyStateView = findViewById(R.id.emptyStateContainer); // Empty state incluído } catch (Exception e) { Toast.makeText(this, "Erro ao inicializar views: " + e.getMessage(), Toast.LENGTH_LONG).show(); // Feedback de erro } @@ -92,16 +101,6 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd */ private void setupClickListeners() { try { - if (fabAddExpense != null) { // Verifica se a view foi encontrada - fabAddExpense.setOnClickListener(new View.OnClickListener() { // Clique do FAB - @Override - public void onClick(View v) { // Ação ao clicar - Intent intent = new Intent(ExpensesListActivity.this, AddExpenseActivity.class); // Ir para adicionar - startActivityForResult(intent, 1); // Abrir aguardando resultado - } - }); - } - if (btnFilter != null) { // Verifica existência do botão btnFilter.setOnClickListener(new View.OnClickListener() { // Clique do filtro @Override @@ -110,6 +109,18 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd } }); } + // Botões do empty state (se presentes no include) + View add = findViewById(R.id.btnAddFirstExpense); + if (add != null) { + add.setOnClickListener(v -> { + Intent intent = new Intent(ExpensesListActivity.this, AddExpenseActivity.class); + startActivityForResult(intent, 1); + }); + } + View back = findViewById(R.id.btnBackFromEmpty); + if (back != null) { + back.setOnClickListener(v -> finish()); + } } catch (Exception e) { Toast.makeText(this, "Erro ao configurar click listeners: " + e.getMessage(), Toast.LENGTH_LONG).show(); // Feedback de erro } @@ -122,20 +133,15 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd private void loadExpenses() { try { expenseList = databaseHelper.getAllExpenses(); // Puxa despesas do DB - boolean hasData = expenseList != null && !expenseList.isEmpty(); - if (!hasData) { - if (!isShowingEmptyState) { - renderEmptyLayout(); - } - return; // Nada para atualizar no layout vazio - } - if (isShowingEmptyState) { - renderMainLayout(); - } - if (expenseAdapter != null) { // Garante adapter existente - expenseAdapter.updateExpenses(expenseList); // Atualiza lista na UI + if (expenseAdapter != null) { // Atualiza a lista + expenseAdapter.updateExpenses(expenseList); } updateTotal(); // Recalcula indicadores + // Mostra/oculta o empty state no fim da tela + boolean isEmpty = expenseList == null || expenseList.isEmpty(); + if (emptyStateView != null) { + emptyStateView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); + } } catch (Exception e) { Toast.makeText(this, "Erro ao carregar despesas: " + e.getMessage(), Toast.LENGTH_LONG).show(); // Feedback de erro } @@ -178,14 +184,11 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd } else { // Categoria específica String category = categories[which]; // Nome da categoria expenseList = databaseHelper.getExpensesByCategory(category); // Busca filtrada - if (expenseList == null || expenseList.isEmpty()) { - renderEmptyLayout(); - } else { - if (isShowingEmptyState) { - renderMainLayout(); - } - expenseAdapter.updateExpenses(expenseList); // Atualiza adapter - updateTotal(); // Recalcula totais com filtro + expenseAdapter.updateExpenses(expenseList); // Atualiza adapter (lista pode ficar vazia) + updateTotal(); // Recalcula totais com filtro + boolean isEmpty = expenseList == null || expenseList.isEmpty(); + if (emptyStateView != null) { + emptyStateView.setVisibility(isEmpty ? View.VISIBLE : View.GONE); } } }) @@ -230,54 +233,5 @@ public class ExpensesListActivity extends AppCompatActivity implements ExpenseAd .show(); // Exibe diálogo } - /** - * Decide qual layout usar com base nos dados atuais do DB. - */ - private void renderLayoutByData() { // Decide qual layout mostrar com base nos dados - try { - List initial = databaseHelper.getAllExpenses(); // Consulta inicial ao DB - if (initial == null || initial.isEmpty()) { // Sem dados? - renderEmptyLayout(); // Mostra estado vazio - } else { // Há dados - expenseList = initial; // Mantém em memória - renderMainLayout(); // Mostra lista principal - } - } catch (Exception e) { // Qualquer erro - renderEmptyLayout(); // Fallback: evita crash mostrando estado vazio - } - } - - /** - * Mostra o layout principal de lista, inicializa views/adapter e listeners. - */ - private void renderMainLayout() { // Exibe layout principal de lista - setContentView(R.layout.activity_expenses_list); // Aplica XML de lista - isShowingEmptyState = false; // Marca que a lista está ativa - try { - ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { // Listener de insets - Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); // Áreas das barras - v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); // Padding seguro - return insets; // Continua propagação - }); - } catch (Exception ignored) { } // Se não existir view com id main, ignora - initializeViews(); // Associa views do layout - setupRecyclerView(); // Configura RecyclerView e adapter - setupClickListeners(); // Registra cliques (FAB e filtro) - updateTotal(); // Atualiza totais na UI - } - - /** - * Mostra o layout de estado vazio e configura o botão de adicionar. - */ - private void renderEmptyLayout() { // Exibe layout de estado vazio - setContentView(R.layout.empty_state); // Aplica XML vazio - isShowingEmptyState = true; // Marca que estado vazio está ativo - View add = findViewById(R.id.btnAddFirstExpense); // Botão "Adicionar Despesa" - if (add != null) { // Se existir no layout - add.setOnClickListener(v -> { // Ao clicar - Intent intent = new Intent(ExpensesListActivity.this, AddExpenseActivity.class); // Abre tela de adição - startActivityForResult(intent, 1); // Aguarda resultado para recarregar - }); - } - } + // Métodos de alternância de layout foram substituídos por um include do empty state no XML } diff --git a/app/src/main/java/pt/epvc/gestodedespesas/MainActivity.java b/app/src/main/java/pt/epvc/gestodedespesas/MainActivity.java index 05b1f81..9ac5aaf 100644 --- a/app/src/main/java/pt/epvc/gestodedespesas/MainActivity.java +++ b/app/src/main/java/pt/epvc/gestodedespesas/MainActivity.java @@ -112,8 +112,8 @@ public class MainActivity extends AppCompatActivity { // Declara classe que este btnViewExpenses.setOnClickListener(new View.OnClickListener() { // Define listener de clique para o botão "Ver Despesas" @Override // Sobrescreve método da interface public void onClick(View v) { // Método chamado quando botão é clicado - // Navega para a tela simplificada de lista de despesas - Intent intent = new Intent(MainActivity.this, SimpleExpensesActivity.class); // Cria Intent para navegar para SimpleExpensesActivity + // Navega para a tela avançada de lista de despesas (usa empty_state e activity_expenses_list) + Intent intent = new Intent(MainActivity.this, ExpensesListActivity.class); // Cria Intent para ExpensesListActivity startActivity(intent); // Inicia Activity sem esperar resultado } }); diff --git a/app/src/main/java/pt/epvc/gestodedespesas/data/DatabaseHelper.java b/app/src/main/java/pt/epvc/gestodedespesas/data/DatabaseHelper.java index be20fa1..644c417 100644 --- a/app/src/main/java/pt/epvc/gestodedespesas/data/DatabaseHelper.java +++ b/app/src/main/java/pt/epvc/gestodedespesas/data/DatabaseHelper.java @@ -6,8 +6,8 @@ import android.database.Cursor; // Onde ficam os resultados de uma consulta import android.database.sqlite.SQLiteDatabase; // Objeto que permite ler/escrever no banco import android.database.sqlite.SQLiteOpenHelper; // Classe que ajuda a criar e atualizar o banco -import java.util.ArrayList; // Lista de tamanho variável -import java.util.List; // Tipo "lista" em Java +import java.util.ArrayList; // Lista de tamanho variável (pode crescer conforme necessário) +import java.util.List; // Tipo "lista" em Java (coleção de itens) import pt.epvc.gestodedespesas.Expense; // Nossa classe que representa uma despesa @@ -23,24 +23,24 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Helper do SQLite private static final int DATABASE_VERSION = 1; // Versão do banco (muda quando a estrutura mudar) // Nome da tabela e das colunas (os "campos" da tabela) - private static final String TABLE_EXPENSES = "expenses"; - private static final String COLUMN_ID = "id"; - private static final String COLUMN_DESCRIPTION = "description"; - private static final String COLUMN_AMOUNT = "amount"; - private static final String COLUMN_CATEGORY = "category"; - private static final String COLUMN_DATE = "date"; - private static final String COLUMN_NOTES = "notes"; + private static final String TABLE_EXPENSES = "expenses"; // Nome da tabela no banco + private static final String COLUMN_ID = "id"; // Coluna: identificador único da despesa + private static final String COLUMN_DESCRIPTION = "description"; // Coluna: descrição do gasto + private static final String COLUMN_AMOUNT = "amount"; // Coluna: valor do gasto + private static final String COLUMN_CATEGORY = "category"; // Coluna: categoria do gasto + private static final String COLUMN_DATE = "date"; // Coluna: data do gasto (texto formatado) + private static final String COLUMN_NOTES = "notes"; // Coluna: notas/opcional // Comando que cria a tabela de despesas (rodado somente na primeira vez) - private static final String CREATE_TABLE_EXPENSES = - "CREATE TABLE " + TABLE_EXPENSES + "(" + - COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + - COLUMN_DESCRIPTION + " TEXT NOT NULL," + - COLUMN_AMOUNT + " REAL NOT NULL," + - COLUMN_CATEGORY + " TEXT NOT NULL," + - COLUMN_DATE + " TEXT NOT NULL," + - COLUMN_NOTES + " TEXT" + - ")"; + private static final String CREATE_TABLE_EXPENSES = // Texto do comando para criar a tabela + "CREATE TABLE " + TABLE_EXPENSES + "(" + // Começa criando a tabela "expenses" + COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + // id numérico que cresce sozinho + COLUMN_DESCRIPTION + " TEXT NOT NULL," + // descrição obrigatória + COLUMN_AMOUNT + " REAL NOT NULL," + // valor obrigatório (número) + COLUMN_CATEGORY + " TEXT NOT NULL," + // categoria obrigatória + COLUMN_DATE + " TEXT NOT NULL," + // data obrigatória (guardada como texto) + COLUMN_NOTES + " TEXT" + // notas opcionais + ")"; // fecha o comando public DatabaseHelper(Context context) { // Chamado quando precisamos usar o banco super(context, DATABASE_NAME, null, DATABASE_VERSION); // Diz o nome e a versão do banco @@ -73,11 +73,11 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Helper do SQLite public Expense getExpense(int id) { // Busca uma despesa específica pelo id SQLiteDatabase db = this.getReadableDatabase(); // Abre o banco para leitura - Cursor cursor = db.query( - TABLE_EXPENSES, - new String[]{COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_AMOUNT, COLUMN_CATEGORY, COLUMN_DATE, COLUMN_NOTES}, - COLUMN_ID + "=?", - new String[]{String.valueOf(id)}, null, null, null, null + Cursor cursor = db.query( // Faz uma consulta procurando pelo id + TABLE_EXPENSES, // na tabela de despesas + new String[]{COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_AMOUNT, COLUMN_CATEGORY, COLUMN_DATE, COLUMN_NOTES}, // colunas desejadas + COLUMN_ID + "=?", // condição: id igual ao informado + new String[]{String.valueOf(id)}, null, null, null, null // valor do id ); if (cursor != null && cursor.moveToFirst()) { // Se achou o registro @@ -100,9 +100,9 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Helper do SQLite public List getAllExpenses() { // Busca todas as despesas, mais recentes primeiro List expenseList = new ArrayList<>(); // Cria uma lista vazia - String selectQuery = "SELECT * FROM " + TABLE_EXPENSES + " ORDER BY " + COLUMN_DATE + " DESC"; // Consulta + String selectQuery = "SELECT * FROM " + TABLE_EXPENSES + " ORDER BY " + COLUMN_DATE + " DESC"; // Consulta: tudo, ordenado pela data (mais recente primeiro) SQLiteDatabase db = this.getWritableDatabase(); // Abre o banco - Cursor cursor = db.rawQuery(selectQuery, null); // Executa a consulta + Cursor cursor = db.rawQuery(selectQuery, null); // Executa a consulta e devolve um "cursor" para navegar if (cursor.moveToFirst()) { // Se tem pelo menos uma linha do { // Repete para cada linha @@ -157,10 +157,10 @@ public class DatabaseHelper extends SQLiteOpenHelper { // Helper do SQLite List expenseList = new ArrayList<>(); // Lista vazia SQLiteDatabase db = this.getReadableDatabase(); // Abre para leitura Cursor cursor = db.query( // Faz a consulta com filtro de categoria - TABLE_EXPENSES, - new String[]{COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_AMOUNT, COLUMN_CATEGORY, COLUMN_DATE, COLUMN_NOTES}, - COLUMN_CATEGORY + "=?", - new String[]{category}, null, null, COLUMN_DATE + " DESC", null + TABLE_EXPENSES, // tabela "expenses" + new String[]{COLUMN_ID, COLUMN_DESCRIPTION, COLUMN_AMOUNT, COLUMN_CATEGORY, COLUMN_DATE, COLUMN_NOTES}, // colunas + COLUMN_CATEGORY + "=?", // condição de filtro + new String[]{category}, null, null, COLUMN_DATE + " DESC", null // valor do filtro e ordenação ); if (cursor.moveToFirst()) { // Se achou resultados diff --git a/app/src/main/res/layout/activity_add_expense.xml b/app/src/main/res/layout/activity_add_expense.xml index 38d7b42..777267d 100644 --- a/app/src/main/res/layout/activity_add_expense.xml +++ b/app/src/main/res/layout/activity_add_expense.xml @@ -89,14 +89,17 @@ android:hint="Categoria" app:startIconDrawable="@drawable/ic_category" app:boxStrokeColor="@color/primary_color" - app:hintTextColor="@color/primary_color"> + app:hintTextColor="@color/primary_color" + app:endIconMode="dropdown_menu"> - + android:inputType="none" + android:textColor="@color/text_primary" + android:hint="Selecione uma categoria" + android:entries="@array/expense_categories" /> diff --git a/app/src/main/res/layout/activity_expenses_list.xml b/app/src/main/res/layout/activity_expenses_list.xml index 9b0c03b..e3e37c2 100644 --- a/app/src/main/res/layout/activity_expenses_list.xml +++ b/app/src/main/res/layout/activity_expenses_list.xml @@ -225,23 +225,16 @@ android:layout_height="wrap_content" android:layout_marginBottom="80dp" /> + + + - - + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 2976fe1..67b86bf 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -33,40 +33,40 @@ + android:padding="32dp"> + android:src="@drawable/ic_wallet" /> + android:text="💰 Gestão de Despesas" + android:textColor="@color/text_primary" + android:textSize="32sp" + android:textStyle="bold" /> + android:text="Controle seus gastos de forma inteligente e organizada" + android:textColor="@color/text_secondary" + android:textSize="16sp" /> + app:iconTint="@color/white" /> diff --git a/app/src/main/res/layout/empty_state.xml b/app/src/main/res/layout/empty_state.xml index d955dc0..1d3cbae 100644 --- a/app/src/main/res/layout/empty_state.xml +++ b/app/src/main/res/layout/empty_state.xml @@ -45,4 +45,16 @@ app:icon="@drawable/ic_add_white" app:iconTint="@color/white" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bc6316..969d4e6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,13 @@ Gestão de Despesas + + + Todas + Alimentação + Transporte + Entretenimento + Saúde + Compras + Outros + \ No newline at end of file