Compare commits
3 Commits
31a7cbb2df
...
995d23ac7a
| Author | SHA1 | Date | |
|---|---|---|---|
| 995d23ac7a | |||
| eaa3d86fc9 | |||
| d192568ed8 |
@@ -14,6 +14,10 @@
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||
|
||||
<!-- Notification permissions -->
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
@@ -113,6 +117,11 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".ReservationNotificationService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -37,19 +37,17 @@ public class ClientDashboardActivity extends AppCompatActivity {
|
||||
private TextView txtGreeting;
|
||||
private ImageView imgProfile;
|
||||
private EditText etSearch;
|
||||
private ChipGroup chipGroupCategories;
|
||||
private RecyclerView rvFeatured, rvMainRestaurants;
|
||||
private RecyclerView rvFeatured;
|
||||
private ProgressBar progressBar;
|
||||
private View layoutFeatured, layoutAllRestaurants;
|
||||
private View layoutFeatured;
|
||||
private android.widget.LinearLayout categoriesContainer;
|
||||
|
||||
private List<Restaurant> allRestaurants = new ArrayList<>();
|
||||
private List<Restaurant> filteredRestaurants = new ArrayList<>();
|
||||
|
||||
private RestaurantAdapter mainAdapter;
|
||||
private FeaturedRestaurantAdapter featuredAdapter;
|
||||
|
||||
private final String[] CATEGORIES = { "Tudo", "Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas" };
|
||||
private String currentCategoryFilter = "Tudo";
|
||||
private final String[] CATEGORIES = { "Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas" };
|
||||
private String currentSearchFilter = "";
|
||||
|
||||
@Override
|
||||
@@ -70,43 +68,38 @@ public class ClientDashboardActivity extends AppCompatActivity {
|
||||
initViews();
|
||||
setupBottomNavigation();
|
||||
setupSearch();
|
||||
setupCategories();
|
||||
|
||||
updateGreeting();
|
||||
fetchProfilePicture();
|
||||
fetchRestaurants();
|
||||
|
||||
// Iniciar serviço de notificações se for Android O ou superior
|
||||
Intent serviceIntent = new Intent(this, ReservationNotificationService.class);
|
||||
startService(serviceIntent);
|
||||
}
|
||||
|
||||
private RestaurantAdapter.OnRestaurantClickListener clickListener;
|
||||
|
||||
private void initViews() {
|
||||
txtGreeting = findViewById(R.id.txtClientGreeting);
|
||||
imgProfile = findViewById(R.id.imgProfile);
|
||||
etSearch = findViewById(R.id.etSearch);
|
||||
chipGroupCategories = findViewById(R.id.chipGroupCategories);
|
||||
rvFeatured = findViewById(R.id.rvFeatured);
|
||||
rvMainRestaurants = findViewById(R.id.rvMainRestaurants);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
layoutFeatured = findViewById(R.id.layoutFeatured);
|
||||
layoutAllRestaurants = findViewById(R.id.layoutAllRestaurants);
|
||||
categoriesContainer = findViewById(R.id.categoriesContainer);
|
||||
|
||||
rvFeatured.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
rvMainRestaurants.setLayoutManager(new LinearLayoutManager(this));
|
||||
|
||||
// Click listener for restaurants to open booking flow
|
||||
RestaurantAdapter.OnRestaurantClickListener clickListener = restaurant -> {
|
||||
clickListener = restaurant -> {
|
||||
Intent intent = new Intent(this, ExplorarRestaurantesActivity.class);
|
||||
// Reusing existing activity for details if needed, or pass data to
|
||||
// NovaReservaActivity
|
||||
// We pass the filter so it can maybe open directly or we just pass restaurant
|
||||
// email
|
||||
intent.putExtra("restaurant", restaurant);
|
||||
startActivity(intent);
|
||||
};
|
||||
|
||||
featuredAdapter = new FeaturedRestaurantAdapter(new ArrayList<>(), clickListener);
|
||||
mainAdapter = new RestaurantAdapter(filteredRestaurants, clickListener);
|
||||
|
||||
rvFeatured.setAdapter(featuredAdapter);
|
||||
rvMainRestaurants.setAdapter(mainAdapter);
|
||||
|
||||
// Click listener for profile picture in the header
|
||||
findViewById(R.id.cardProfile).setOnClickListener(v -> {
|
||||
@@ -163,7 +156,7 @@ public class ClientDashboardActivity extends AppCompatActivity {
|
||||
private void fetchRestaurants() {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
layoutFeatured.setVisibility(View.GONE);
|
||||
layoutAllRestaurants.setVisibility(View.GONE);
|
||||
categoriesContainer.removeAllViews();
|
||||
|
||||
DatabaseReference usersRef = FirebaseDatabase.getInstance().getReference("Restaurantes");
|
||||
usersRef.addValueEventListener(new ValueEventListener() {
|
||||
@@ -207,47 +200,6 @@ public class ClientDashboardActivity extends AppCompatActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void setupCategories() {
|
||||
for (String category : CATEGORIES) {
|
||||
Chip chip = new Chip(this);
|
||||
chip.setText(category);
|
||||
chip.setCheckable(true);
|
||||
chip.setClickable(true);
|
||||
// Default styling
|
||||
chip.setChipBackgroundColorResource(R.color.colorSurface);
|
||||
chip.setTextColor(getResources().getColor(R.color.colorTextSecondary));
|
||||
chip.setChipStrokeWidth(0f);
|
||||
|
||||
if (category.equals("Tudo")) {
|
||||
chip.setChecked(true);
|
||||
chip.setChipBackgroundColorResource(R.color.colorPrimary);
|
||||
chip.setTextColor(getResources().getColor(R.color.white));
|
||||
}
|
||||
|
||||
chip.setOnCheckedChangeListener((buttonView, isChecked) -> {
|
||||
if (isChecked) {
|
||||
// Reset all other chips visual state
|
||||
for (int i = 0; i < chipGroupCategories.getChildCount(); i++) {
|
||||
Chip c = (Chip) chipGroupCategories.getChildAt(i);
|
||||
if (c != chip) {
|
||||
c.setChipBackgroundColorResource(R.color.colorSurface);
|
||||
c.setTextColor(getResources().getColor(R.color.colorTextSecondary));
|
||||
}
|
||||
}
|
||||
chip.setChipBackgroundColorResource(R.color.colorPrimary);
|
||||
chip.setTextColor(getResources().getColor(R.color.white));
|
||||
|
||||
currentCategoryFilter = category;
|
||||
applyFilters();
|
||||
} else if (currentCategoryFilter.equals(category)) {
|
||||
// Prevent unchecking the currently selected chip
|
||||
chip.setChecked(true);
|
||||
}
|
||||
});
|
||||
chipGroupCategories.addView(chip);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
@@ -271,35 +223,46 @@ public class ClientDashboardActivity extends AppCompatActivity {
|
||||
String normalizedSearch = normalizeString(currentSearchFilter);
|
||||
|
||||
for (Restaurant r : allRestaurants) {
|
||||
boolean matchesCategory = currentCategoryFilter.equals("Tudo")
|
||||
|| currentCategoryFilter.equals(r.getCategory());
|
||||
|
||||
String normalizedName = normalizeString(r.getName());
|
||||
boolean matchesSearch = currentSearchFilter.isEmpty() || normalizedName.contains(normalizedSearch);
|
||||
|
||||
if (matchesCategory && matchesSearch) {
|
||||
if (matchesSearch) {
|
||||
filteredRestaurants.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
mainAdapter.notifyDataSetChanged();
|
||||
|
||||
// Update featured (just the first 3 for demo, or based on a specific logic)
|
||||
// Update featured carousel with top restaurants
|
||||
List<Restaurant> featuredList = new ArrayList<>();
|
||||
for (int i = 0; i < Math.min(3, filteredRestaurants.size()); i++) {
|
||||
featuredList.add(filteredRestaurants.get(i));
|
||||
}
|
||||
|
||||
featuredAdapter = new FeaturedRestaurantAdapter(featuredList,
|
||||
mainAdapter instanceof RestaurantAdapter ? restaurant -> {
|
||||
Intent intent = new Intent(this, ExplorarRestaurantesActivity.class);
|
||||
intent.putExtra("restaurant", restaurant);
|
||||
startActivity(intent);
|
||||
} : null);
|
||||
featuredAdapter = new FeaturedRestaurantAdapter(featuredList, clickListener);
|
||||
rvFeatured.setAdapter(featuredAdapter);
|
||||
|
||||
layoutFeatured.setVisibility(featuredList.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
layoutAllRestaurants.setVisibility(filteredRestaurants.isEmpty() ? View.GONE : View.VISIBLE);
|
||||
|
||||
// Update category rows
|
||||
categoriesContainer.removeAllViews();
|
||||
for (String category : CATEGORIES) {
|
||||
List<Restaurant> catList = new ArrayList<>();
|
||||
for (Restaurant r : filteredRestaurants) {
|
||||
if (category.equals(r.getCategory())) {
|
||||
catList.add(r);
|
||||
}
|
||||
}
|
||||
|
||||
if (!catList.isEmpty()) {
|
||||
View rowView = android.view.LayoutInflater.from(this).inflate(R.layout.item_category_row, categoriesContainer, false);
|
||||
TextView txtTitle = rowView.findViewById(R.id.txtCategoryTitle);
|
||||
RecyclerView rvCategory = rowView.findViewById(R.id.rvCategoryRestaurants);
|
||||
|
||||
txtTitle.setText(category);
|
||||
rvCategory.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
rvCategory.setAdapter(new FeaturedRestaurantAdapter(catList, clickListener));
|
||||
|
||||
categoriesContainer.addView(rowView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeString(String str) {
|
||||
|
||||
@@ -118,9 +118,9 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
btnConfirmar.setOnClickListener(v -> {
|
||||
com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex);
|
||||
if ("Pendente".equals(item.getEstado())) {
|
||||
atualizarEstadoSelecionado("Confirmada");
|
||||
} else if ("Confirmada".equals(item.getEstado())) {
|
||||
atualizarEstadoSelecionado("Concluída");
|
||||
mostrarMesasDisponiveis();
|
||||
} else if ("Confirmada".equals(item.getEstado()) || item.getEstado().startsWith("Confirmada (Mesa")) {
|
||||
showConcluirDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -134,6 +134,97 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
}
|
||||
}
|
||||
|
||||
private void mostrarMesasDisponiveis() {
|
||||
if (selectedIndex < 0 || selectedIndex >= reservas.size()) {
|
||||
Toast.makeText(this, "Selecione uma reserva.", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex);
|
||||
|
||||
com.google.firebase.database.DatabaseReference mesasRef = com.google.firebase.database.FirebaseDatabase.getInstance().getReference("Mesas");
|
||||
mesasRef.orderByChild("restauranteEmail").equalTo(restaurantEmail).addListenerForSingleValueEvent(new com.google.firebase.database.ValueEventListener() {
|
||||
@Override
|
||||
public void onDataChange(@androidx.annotation.NonNull com.google.firebase.database.DataSnapshot snapshot) {
|
||||
List<com.example.pap_teste.models.Mesa> mesasLivres = new ArrayList<>();
|
||||
for (com.google.firebase.database.DataSnapshot ds : snapshot.getChildren()) {
|
||||
com.example.pap_teste.models.Mesa m = ds.getValue(com.example.pap_teste.models.Mesa.class);
|
||||
if (m != null && m.getEstado() != null && m.getEstado().equalsIgnoreCase("Livre") && m.getCapacidade() >= item.getPessoas()) {
|
||||
m.setId(ds.getKey());
|
||||
mesasLivres.add(m);
|
||||
}
|
||||
}
|
||||
|
||||
if (mesasLivres.isEmpty()) {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(DetalhesReservasActivity.this)
|
||||
.setTitle("Sem mesas disponíveis")
|
||||
.setMessage("Não há mesas livres com capacidade suficiente para " + item.getPessoas() + " pessoas. Deseja confirmar a reserva mesmo assim (sem mesa atribuída)?")
|
||||
.setPositiveButton("Sim", (dialog, which) -> atualizarEstadoSelecionado("Confirmada", null))
|
||||
.setNegativeButton("Não", null)
|
||||
.show();
|
||||
return;
|
||||
}
|
||||
|
||||
String[] mesaOptions = new String[mesasLivres.size()];
|
||||
for (int i = 0; i < mesasLivres.size(); i++) {
|
||||
com.example.pap_teste.models.Mesa m = mesasLivres.get(i);
|
||||
mesaOptions[i] = String.format("Mesa %d (%d lugares)", m.getNumero(), m.getCapacidade());
|
||||
}
|
||||
|
||||
new androidx.appcompat.app.AlertDialog.Builder(DetalhesReservasActivity.this)
|
||||
.setTitle("Atribuir Mesa")
|
||||
.setItems(mesaOptions, (dialog, which) -> {
|
||||
com.example.pap_teste.models.Mesa selecionada = mesasLivres.get(which);
|
||||
confirmarReservaComMesa(item, selecionada);
|
||||
})
|
||||
.setNegativeButton("Cancelar", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancelled(@androidx.annotation.NonNull com.google.firebase.database.DatabaseError error) {
|
||||
Toast.makeText(DetalhesReservasActivity.this, "Erro ao carregar mesas.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void confirmarReservaComMesa(com.example.pap_teste.models.Reserva reserva, com.example.pap_teste.models.Mesa mesa) {
|
||||
String novoEstado = "Confirmada (Mesa " + mesa.getNumero() + ")";
|
||||
|
||||
java.util.Map<String, Object> updates = new java.util.HashMap<>();
|
||||
updates.put("estado", novoEstado);
|
||||
updates.put("motivo", null); // limpar possível motivo antigo
|
||||
|
||||
databaseReference.child(reserva.getId()).updateChildren(updates).addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
com.google.firebase.database.FirebaseDatabase.getInstance().getReference("Mesas").child(mesa.getId()).child("estado").setValue("Reservada")
|
||||
.addOnCompleteListener(t2 -> {
|
||||
Toast.makeText(this, "Reserva aceite e mesa atribuída com sucesso.", Toast.LENGTH_SHORT).show();
|
||||
reserva.setEstado(novoEstado);
|
||||
reserva.setMotivo(null);
|
||||
mostrarDetalhe(reserva);
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(this, "Erro ao confirmar reserva.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showConcluirDialog() {
|
||||
if (selectedIndex < 0) return;
|
||||
android.widget.EditText input = new android.widget.EditText(this);
|
||||
input.setHint("Notas de conclusão (opcional)");
|
||||
new androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Concluir Reserva")
|
||||
.setMessage("Deseja adicionar alguma nota ao concluir esta reserva?")
|
||||
.setView(input)
|
||||
.setPositiveButton("Concluir", (dialog, which) -> {
|
||||
String motivo = input.getText().toString();
|
||||
atualizarEstadoSelecionado("Concluída", motivo.isEmpty() ? null : motivo);
|
||||
})
|
||||
.setNegativeButton("Cancelar", null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showRecusarDialog() {
|
||||
if (selectedIndex < 0)
|
||||
return;
|
||||
@@ -142,7 +233,7 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Motivo da Recusa")
|
||||
.setItems(motivos, (dialog, which) -> {
|
||||
atualizarEstadoSelecionado("Recusada (" + motivos[which] + ")");
|
||||
atualizarEstadoSelecionado("Recusada", motivos[which]);
|
||||
})
|
||||
.setNegativeButton("Voltar", null)
|
||||
.show();
|
||||
@@ -186,17 +277,27 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
toggleButtons(null);
|
||||
}
|
||||
|
||||
private void atualizarEstadoSelecionado(String novoEstado) {
|
||||
private void atualizarEstadoSelecionado(String novoEstado, String motivo) {
|
||||
if (selectedIndex < 0 || selectedIndex >= reservas.size()) {
|
||||
Toast.makeText(this, "Selecione uma reserva para atualizar.", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex);
|
||||
databaseReference.child(item.getId()).child("estado").setValue(novoEstado).addOnCompleteListener(task -> {
|
||||
java.util.Map<String, Object> updates = new java.util.HashMap<>();
|
||||
updates.put("estado", novoEstado);
|
||||
if (motivo != null && !motivo.isEmpty()) {
|
||||
updates.put("motivo", motivo);
|
||||
} else {
|
||||
updates.put("motivo", null);
|
||||
}
|
||||
|
||||
databaseReference.child(item.getId()).updateChildren(updates).addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
txtMensagem
|
||||
.setText(String.format("Reserva de %s marcada como %s.", item.getClienteEmail(), novoEstado));
|
||||
item.setEstado(novoEstado);
|
||||
item.setMotivo(motivo);
|
||||
mostrarDetalhe(item);
|
||||
}
|
||||
});
|
||||
@@ -206,7 +307,13 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
txtInfo.setText(String.format("%s • %s • %s • %dp", item.getClienteEmail(), item.getRestauranteName(),
|
||||
item.getHora(), item.getPessoas()));
|
||||
txtNotas.setText("Data: " + item.getData());
|
||||
txtEstado.setText(String.format("Estado: %s", item.getEstado()));
|
||||
|
||||
String estadoTexto = item.getEstado();
|
||||
if (item.getMotivo() != null && !item.getMotivo().isEmpty()) {
|
||||
estadoTexto += " (" + item.getMotivo() + ")";
|
||||
}
|
||||
txtEstado.setText(String.format("Estado: %s", estadoTexto));
|
||||
|
||||
toggleButtons(item);
|
||||
}
|
||||
|
||||
@@ -225,21 +332,23 @@ public class DetalhesReservasActivity extends AppCompatActivity {
|
||||
btnRecusar.setVisibility(android.view.View.VISIBLE);
|
||||
btnApagar.setVisibility(android.view.View.GONE);
|
||||
break;
|
||||
case "Confirmada":
|
||||
btnConfirmar.setText("Concluir");
|
||||
btnConfirmar.setVisibility(android.view.View.VISIBLE);
|
||||
btnRecusar.setVisibility(android.view.View.VISIBLE); // Still allow refusal
|
||||
btnApagar.setVisibility(android.view.View.GONE);
|
||||
break;
|
||||
case "Concluída":
|
||||
btnConfirmar.setVisibility(android.view.View.GONE);
|
||||
btnRecusar.setVisibility(android.view.View.GONE);
|
||||
btnApagar.setVisibility(android.view.View.VISIBLE);
|
||||
break;
|
||||
default: // Recusada or Cancelada
|
||||
btnConfirmar.setVisibility(android.view.View.GONE);
|
||||
btnRecusar.setVisibility(android.view.View.GONE);
|
||||
btnApagar.setVisibility(android.view.View.VISIBLE); // Allow deleting refused ones as well
|
||||
default:
|
||||
if (item.getEstado() != null && item.getEstado().startsWith("Confirmada")) {
|
||||
btnConfirmar.setText("Concluir");
|
||||
btnConfirmar.setVisibility(android.view.View.VISIBLE);
|
||||
btnRecusar.setVisibility(android.view.View.VISIBLE); // Still allow refusal
|
||||
btnApagar.setVisibility(android.view.View.GONE);
|
||||
} else {
|
||||
// Recusada or Cancelada
|
||||
btnConfirmar.setVisibility(android.view.View.GONE);
|
||||
btnRecusar.setVisibility(android.view.View.GONE);
|
||||
btnApagar.setVisibility(android.view.View.VISIBLE); // Allow deleting refused ones as well
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ public class ListaEsperaActivity extends AppCompatActivity {
|
||||
List<com.example.pap_teste.models.Mesa> mesasLivres = new ArrayList<>();
|
||||
for (DataSnapshot ds : snapshot.getChildren()) {
|
||||
com.example.pap_teste.models.Mesa m = ds.getValue(com.example.pap_teste.models.Mesa.class);
|
||||
if (m != null && m.getEstado() != null && m.getEstado().equalsIgnoreCase("Livre")) {
|
||||
if (m != null && m.getEstado() != null && m.getEstado().equalsIgnoreCase("Livre") && m.getCapacidade() >= item.getPessoas()) {
|
||||
m.setId(ds.getKey());
|
||||
mesasLivres.add(m);
|
||||
}
|
||||
@@ -114,7 +114,7 @@ public class ListaEsperaActivity extends AppCompatActivity {
|
||||
if (mesasLivres.isEmpty()) {
|
||||
new androidx.appcompat.app.AlertDialog.Builder(ListaEsperaActivity.this)
|
||||
.setTitle("Sem mesas disponíveis")
|
||||
.setMessage("Não há mesas livres registadas. Deseja confirmar a reserva mesmo assim (sem lugar reservado)?")
|
||||
.setMessage("Não há mesas livres com capacidade suficiente para " + item.getPessoas() + " pessoas. Deseja confirmar a reserva mesmo assim (sem lugar reservado)?")
|
||||
.setPositiveButton("Sim", (dialog, which) -> atualizarEstadoSelecionado("Confirmada"))
|
||||
.setNegativeButton("Não", null)
|
||||
.show();
|
||||
|
||||
@@ -160,12 +160,19 @@ public class MainActivity extends AppCompatActivity {
|
||||
permissionsNeeded.add(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
}
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
if (ContextCompat.checkSelfPermission(this,
|
||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
permissionsNeeded.add(Manifest.permission.POST_NOTIFICATIONS);
|
||||
}
|
||||
}
|
||||
|
||||
if (!permissionsNeeded.isEmpty()) {
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("Permissões Necessárias")
|
||||
.setMessage(
|
||||
"Para o correto funcionamento do check-in, serviços de proximidade e fotos da galeria, precisamos de algumas permissões.")
|
||||
"Para o correto funcionamento do check-in, receber notificações de reservas e acesso a fotos, precisamos de algumas permissões.")
|
||||
.setPositiveButton("Configurar", (dialog, which) -> {
|
||||
permissionRequest.launch(permissionsNeeded.toArray(new String[0]));
|
||||
})
|
||||
|
||||
@@ -120,13 +120,29 @@ public class MinhasReservasActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
private void cancelReservation(Reserva reserva) {
|
||||
databaseReference.child(reserva.getId()).child("estado").setValue("Cancelada")
|
||||
.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
Toast.makeText(this, "Reserva cancelada.", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "Erro ao cancelar reserva.", Toast.LENGTH_SHORT).show();
|
||||
android.widget.EditText input = new android.widget.EditText(this);
|
||||
input.setHint("Motivo do cancelamento (opcional)");
|
||||
new androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Cancelar Reserva")
|
||||
.setMessage("Deseja indicar o motivo do cancelamento?")
|
||||
.setView(input)
|
||||
.setPositiveButton("Confirmar", (dialog, which) -> {
|
||||
String motivo = input.getText().toString();
|
||||
java.util.Map<String, Object> updates = new java.util.HashMap<>();
|
||||
updates.put("estado", "Cancelada");
|
||||
if (!motivo.isEmpty()) {
|
||||
updates.put("motivo", motivo);
|
||||
}
|
||||
});
|
||||
databaseReference.child(reserva.getId()).updateChildren(updates)
|
||||
.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
Toast.makeText(this, "Reserva cancelada.", Toast.LENGTH_SHORT).show();
|
||||
} else {
|
||||
Toast.makeText(this, "Erro ao cancelar reserva.", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
})
|
||||
.setNegativeButton("Voltar", null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
package com.example.pap_teste;
|
||||
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.example.pap_teste.models.Reserva;
|
||||
import com.google.firebase.auth.FirebaseAuth;
|
||||
import com.google.firebase.auth.FirebaseUser;
|
||||
import com.google.firebase.database.ChildEventListener;
|
||||
import com.google.firebase.database.DataSnapshot;
|
||||
import com.google.firebase.database.DatabaseError;
|
||||
import com.google.firebase.database.DatabaseReference;
|
||||
import com.google.firebase.database.FirebaseDatabase;
|
||||
|
||||
public class ReservationNotificationService extends Service {
|
||||
|
||||
private static final String CHANNEL_ID = "ReservaNotifications";
|
||||
private DatabaseReference reservasRef;
|
||||
private ChildEventListener childEventListener;
|
||||
private String currentUserEmail;
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createNotificationChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
|
||||
if (user != null && user.getEmail() != null) {
|
||||
currentUserEmail = user.getEmail();
|
||||
startListeningForReservations();
|
||||
} else {
|
||||
stopSelf();
|
||||
}
|
||||
return START_STICKY;
|
||||
}
|
||||
|
||||
private void startListeningForReservations() {
|
||||
if (reservasRef != null && childEventListener != null) {
|
||||
return; // Already listening
|
||||
}
|
||||
|
||||
reservasRef = FirebaseDatabase.getInstance().getReference("reservas");
|
||||
childEventListener = new ChildEventListener() {
|
||||
@Override
|
||||
public void onChildAdded(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
|
||||
// Not needed for new reservations created by the user
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
|
||||
Reserva reserva = snapshot.getValue(Reserva.class);
|
||||
if (reserva != null && currentUserEmail.equals(reserva.getClienteEmail())) {
|
||||
String estado = reserva.getEstado();
|
||||
if (estado != null && (estado.startsWith("Confirmada") || estado.equals("Recusada") || estado.equals("Concluída") || estado.equals("Cancelada"))) {
|
||||
sendNotification(reserva);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChildRemoved(@NonNull DataSnapshot snapshot) {}
|
||||
|
||||
@Override
|
||||
public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {}
|
||||
|
||||
@Override
|
||||
public void onCancelled(@NonNull DatabaseError error) {}
|
||||
};
|
||||
|
||||
reservasRef.addChildEventListener(childEventListener);
|
||||
}
|
||||
|
||||
private void sendNotification(Reserva reserva) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
|
||||
ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
return; // Permission not granted
|
||||
}
|
||||
|
||||
Intent intent = new Intent(this, MainActivity.class); // Or deep link to reservations
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
String title = "Atualização de Reserva";
|
||||
String message = "A sua reserva no " + reserva.getRestauranteName() + " foi " + reserva.getEstado().toLowerCase() + ".";
|
||||
|
||||
if (reserva.getMotivo() != null && !reserva.getMotivo().isEmpty()) {
|
||||
message += " Motivo: " + reserva.getMotivo();
|
||||
}
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.na_mesa) // Assuming na_mesa is a valid drawable
|
||||
.setContentTitle(title)
|
||||
.setContentText(message)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(message))
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setAutoCancel(true);
|
||||
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.notify(reserva.getId().hashCode(), builder.build());
|
||||
}
|
||||
}
|
||||
|
||||
private void createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
CharSequence name = "Notificações de Reservas";
|
||||
String description = "Notificações sobre o estado das suas reservas";
|
||||
int importance = NotificationManager.IMPORTANCE_HIGH;
|
||||
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
|
||||
channel.setDescription(description);
|
||||
|
||||
NotificationManager notificationManager = getSystemService(NotificationManager.class);
|
||||
if (notificationManager != null) {
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (reservasRef != null && childEventListener != null) {
|
||||
reservasRef.removeEventListener(childEventListener);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public class Reserva {
|
||||
private String hora;
|
||||
private int pessoas;
|
||||
private String estado; // Pendente, Confirmada, Concluída, Cancelada, Recusada
|
||||
private String motivo;
|
||||
|
||||
public Reserva() {
|
||||
// Required for Firebase
|
||||
@@ -89,4 +90,12 @@ public class Reserva {
|
||||
public void setEstado(String estado) {
|
||||
this.estado = estado;
|
||||
}
|
||||
|
||||
public String getMotivo() {
|
||||
return motivo;
|
||||
}
|
||||
|
||||
public void setMotivo(String motivo) {
|
||||
this.motivo = motivo;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,24 +112,6 @@
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<!-- Category Pills -->
|
||||
<HorizontalScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:scrollbars="none"
|
||||
android:clipToPadding="false"
|
||||
android:paddingHorizontal="20dp">
|
||||
|
||||
<com.google.android.material.chip.ChipGroup
|
||||
android:id="@+id/chipGroupCategories"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:singleLine="true"
|
||||
app:singleSelection="true">
|
||||
<!-- Chips will be added programmatically -->
|
||||
</com.google.android.material.chip.ChipGroup>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<!-- Loading Spinner -->
|
||||
<ProgressBar
|
||||
@@ -173,36 +155,14 @@
|
||||
tools:listitem="@layout/item_restaurant_featured" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Main Restaurant Grid/List -->
|
||||
<!-- Dynamic Categories Container -->
|
||||
<LinearLayout
|
||||
android:id="@+id/layoutAllRestaurants"
|
||||
android:id="@+id/categoriesContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
android:layout_marginTop="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtListTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:text="Todos os Restaurantes"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvMainRestaurants"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingBottom="16dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_restaurant" />
|
||||
</LinearLayout>
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingBottom="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
|
||||
32
app/src/main/res/layout/item_category_row.xml
Normal file
32
app/src/main/res/layout/item_category_row.xml
Normal file
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:layout_marginTop="24dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/txtCategoryTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="24dp"
|
||||
android:textColor="@color/colorTextPrimary"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="sans-serif"
|
||||
tools:text="Sushi" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/rvCategoryRestaurants"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
tools:listitem="@layout/item_restaurant_featured" />
|
||||
</LinearLayout>
|
||||
@@ -14,7 +14,28 @@
|
||||
|
||||
<!-- Typography base generic configuration -->
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
|
||||
<!-- Modern Shape Appearance (Rounded Corners) -->
|
||||
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>
|
||||
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
|
||||
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Pap_teste" parent="Base.Theme.Pap_teste" />
|
||||
|
||||
<!-- Shape Styles -->
|
||||
<style name="ShapeAppearance.App.SmallComponent" parent="ShapeAppearance.Material3.SmallComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">24dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user