Compare commits

..

6 Commits

Author SHA1 Message Date
824ff28001 22\06 2026-06-25 18:25:45 +01:00
86dbe2d319 22\06 2026-06-25 18:25:36 +01:00
8c1e01dc4c 22\06 2026-06-22 08:39:29 +01:00
995d23ac7a 15/06 2026-06-15 14:12:03 +01:00
eaa3d86fc9 9/06 2026-06-09 17:12:11 +01:00
d192568ed8 9/06 2026-06-09 17:12:07 +01:00
21 changed files with 778 additions and 155 deletions

View File

@@ -4,10 +4,10 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2026-04-14T16:06:26.670067Z"> <DropdownSelection timestamp="2026-06-25T17:21:43.291530Z">
<Target type="DEFAULT_BOOT"> <Target type="DEFAULT_BOOT">
<handle> <handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=b93659d0e5dd" /> <DeviceId pluginId="LocalEmulator" identifier="path=/Users/230409/.android/avd/Pixel_8_Pro.avd" />
</handle> </handle>
</Target> </Target>
</DropdownSelection> </DropdownSelection>

View File

@@ -15,6 +15,10 @@
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <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 <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@@ -113,6 +117,11 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<service
android:name=".ReservationNotificationService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application> </application>
</manifest> </manifest>

View File

@@ -37,19 +37,17 @@ public class ClientDashboardActivity extends AppCompatActivity {
private TextView txtGreeting; private TextView txtGreeting;
private ImageView imgProfile; private ImageView imgProfile;
private EditText etSearch; private EditText etSearch;
private ChipGroup chipGroupCategories; private RecyclerView rvFeatured;
private RecyclerView rvFeatured, rvMainRestaurants;
private ProgressBar progressBar; private ProgressBar progressBar;
private View layoutFeatured, layoutAllRestaurants; private View layoutFeatured;
private android.widget.LinearLayout categoriesContainer;
private List<Restaurant> allRestaurants = new ArrayList<>(); private List<Restaurant> allRestaurants = new ArrayList<>();
private List<Restaurant> filteredRestaurants = new ArrayList<>(); private List<Restaurant> filteredRestaurants = new ArrayList<>();
private RestaurantAdapter mainAdapter;
private FeaturedRestaurantAdapter featuredAdapter; private FeaturedRestaurantAdapter featuredAdapter;
private final String[] CATEGORIES = { "Tudo", "Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas" }; private final String[] CATEGORIES = { "Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas" };
private String currentCategoryFilter = "Tudo";
private String currentSearchFilter = ""; private String currentSearchFilter = "";
@Override @Override
@@ -70,43 +68,38 @@ public class ClientDashboardActivity extends AppCompatActivity {
initViews(); initViews();
setupBottomNavigation(); setupBottomNavigation();
setupSearch(); setupSearch();
setupCategories();
updateGreeting(); updateGreeting();
fetchProfilePicture(); fetchProfilePicture();
fetchRestaurants(); 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() { private void initViews() {
txtGreeting = findViewById(R.id.txtClientGreeting); txtGreeting = findViewById(R.id.txtClientGreeting);
imgProfile = findViewById(R.id.imgProfile); imgProfile = findViewById(R.id.imgProfile);
etSearch = findViewById(R.id.etSearch); etSearch = findViewById(R.id.etSearch);
chipGroupCategories = findViewById(R.id.chipGroupCategories);
rvFeatured = findViewById(R.id.rvFeatured); rvFeatured = findViewById(R.id.rvFeatured);
rvMainRestaurants = findViewById(R.id.rvMainRestaurants);
progressBar = findViewById(R.id.progressBar); progressBar = findViewById(R.id.progressBar);
layoutFeatured = findViewById(R.id.layoutFeatured); layoutFeatured = findViewById(R.id.layoutFeatured);
layoutAllRestaurants = findViewById(R.id.layoutAllRestaurants); categoriesContainer = findViewById(R.id.categoriesContainer);
rvFeatured.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); rvFeatured.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
rvMainRestaurants.setLayoutManager(new LinearLayoutManager(this));
// Click listener for restaurants to open booking flow // Click listener for restaurants to open booking flow
RestaurantAdapter.OnRestaurantClickListener clickListener = restaurant -> { clickListener = restaurant -> {
Intent intent = new Intent(this, ExplorarRestaurantesActivity.class); 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); intent.putExtra("restaurant", restaurant);
startActivity(intent); startActivity(intent);
}; };
featuredAdapter = new FeaturedRestaurantAdapter(new ArrayList<>(), clickListener); featuredAdapter = new FeaturedRestaurantAdapter(new ArrayList<>(), clickListener);
mainAdapter = new RestaurantAdapter(filteredRestaurants, clickListener);
rvFeatured.setAdapter(featuredAdapter); rvFeatured.setAdapter(featuredAdapter);
rvMainRestaurants.setAdapter(mainAdapter);
// Click listener for profile picture in the header // Click listener for profile picture in the header
findViewById(R.id.cardProfile).setOnClickListener(v -> { findViewById(R.id.cardProfile).setOnClickListener(v -> {
@@ -163,7 +156,7 @@ public class ClientDashboardActivity extends AppCompatActivity {
private void fetchRestaurants() { private void fetchRestaurants() {
progressBar.setVisibility(View.VISIBLE); progressBar.setVisibility(View.VISIBLE);
layoutFeatured.setVisibility(View.GONE); layoutFeatured.setVisibility(View.GONE);
layoutAllRestaurants.setVisibility(View.GONE); categoriesContainer.removeAllViews();
DatabaseReference usersRef = FirebaseDatabase.getInstance().getReference("Restaurantes"); DatabaseReference usersRef = FirebaseDatabase.getInstance().getReference("Restaurantes");
usersRef.addValueEventListener(new ValueEventListener() { 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() { private void setupSearch() {
etSearch.addTextChangedListener(new TextWatcher() { etSearch.addTextChangedListener(new TextWatcher() {
@Override @Override
@@ -271,35 +223,46 @@ public class ClientDashboardActivity extends AppCompatActivity {
String normalizedSearch = normalizeString(currentSearchFilter); String normalizedSearch = normalizeString(currentSearchFilter);
for (Restaurant r : allRestaurants) { for (Restaurant r : allRestaurants) {
boolean matchesCategory = currentCategoryFilter.equals("Tudo")
|| currentCategoryFilter.equals(r.getCategory());
String normalizedName = normalizeString(r.getName()); String normalizedName = normalizeString(r.getName());
boolean matchesSearch = currentSearchFilter.isEmpty() || normalizedName.contains(normalizedSearch); boolean matchesSearch = currentSearchFilter.isEmpty() || normalizedName.contains(normalizedSearch);
if (matchesCategory && matchesSearch) { if (matchesSearch) {
filteredRestaurants.add(r); filteredRestaurants.add(r);
} }
} }
mainAdapter.notifyDataSetChanged(); // Update featured carousel with top restaurants
// Update featured (just the first 3 for demo, or based on a specific logic)
List<Restaurant> featuredList = new ArrayList<>(); List<Restaurant> featuredList = new ArrayList<>();
for (int i = 0; i < Math.min(3, filteredRestaurants.size()); i++) { for (int i = 0; i < Math.min(3, filteredRestaurants.size()); i++) {
featuredList.add(filteredRestaurants.get(i)); featuredList.add(filteredRestaurants.get(i));
} }
featuredAdapter = new FeaturedRestaurantAdapter(featuredList, featuredAdapter = new FeaturedRestaurantAdapter(featuredList, clickListener);
mainAdapter instanceof RestaurantAdapter ? restaurant -> {
Intent intent = new Intent(this, ExplorarRestaurantesActivity.class);
intent.putExtra("restaurant", restaurant);
startActivity(intent);
} : null);
rvFeatured.setAdapter(featuredAdapter); rvFeatured.setAdapter(featuredAdapter);
layoutFeatured.setVisibility(featuredList.isEmpty() ? View.GONE : View.VISIBLE); 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) { private String normalizeString(String str) {

View File

@@ -31,6 +31,15 @@ import com.google.firebase.storage.StorageReference;
import java.util.UUID; import java.util.UUID;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.view.LayoutInflater;
import android.view.View;
import android.app.TimePickerDialog;
import java.util.ArrayList;
import java.util.List;
import com.example.pap_teste.models.ScheduleDay;
public class DefinicoesAdminActivity extends AppCompatActivity { public class DefinicoesAdminActivity extends AppCompatActivity {
@@ -42,6 +51,9 @@ public class DefinicoesAdminActivity extends AppCompatActivity {
private String photoUrl; private String photoUrl;
private ActivityResultLauncher<Intent> imagePickerLauncher; private ActivityResultLauncher<Intent> imagePickerLauncher;
private String[] categories = {"Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas", "Italiana", "Moderna"}; private String[] categories = {"Carnes", "Massas", "Sushi", "Pizzas", "Sobremesas", "Italiana", "Moderna"};
private String[] dayNames = {"Domingo", "Segunda-feira", "Terça-feira", "Quarta-feira", "Quinta-feira", "Sexta-feira", "Sábado"};
private String[] dayKeys = {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"};
private List<View> scheduleViews = new ArrayList<>();
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
@@ -105,6 +117,29 @@ public class DefinicoesAdminActivity extends AppCompatActivity {
}).show(); }).show();
}); });
LinearLayout containerSchedule = findViewById(R.id.containerSchedule);
LayoutInflater inflater = LayoutInflater.from(this);
for (int i = 0; i < dayNames.length; i++) {
View view = inflater.inflate(R.layout.item_schedule_day, containerSchedule, false);
TextView tvDayName = view.findViewById(R.id.tvDayName);
TextView tvOpenTime = view.findViewById(R.id.tvOpenTime);
TextView tvCloseTime = view.findViewById(R.id.tvCloseTime);
CheckBox cbClosed = view.findViewById(R.id.cbClosed);
tvDayName.setText(dayNames[i]);
tvOpenTime.setOnClickListener(v -> showTimePicker(tvOpenTime));
tvCloseTime.setOnClickListener(v -> showTimePicker(tvCloseTime));
cbClosed.setOnCheckedChangeListener((buttonView, isChecked) -> {
tvOpenTime.setEnabled(!isChecked);
tvCloseTime.setEnabled(!isChecked);
});
containerSchedule.addView(view);
scheduleViews.add(view);
}
loadCurrentSettings(); loadCurrentSettings();
btnSave.setOnClickListener(v -> saveSettings()); btnSave.setOnClickListener(v -> saveSettings());
@@ -130,6 +165,13 @@ public class DefinicoesAdminActivity extends AppCompatActivity {
builder.show(); builder.show();
} }
private void showTimePicker(TextView targetTextView) {
java.util.Calendar cal = java.util.Calendar.getInstance();
new TimePickerDialog(this, (view, hourOfDay, minute) -> {
targetTextView.setText(String.format(java.util.Locale.getDefault(), "%02d:%02d", hourOfDay, minute));
}, cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE), true).show();
}
private void uploadImageToFirebase(Uri imageUri) { private void uploadImageToFirebase(Uri imageUri) {
if (documentId == null) return; if (documentId == null) return;
@@ -179,6 +221,27 @@ public class DefinicoesAdminActivity extends AppCompatActivity {
Glide.with(DefinicoesAdminActivity.this).load(photoUrl).circleCrop().into(imgLogo); Glide.with(DefinicoesAdminActivity.this).load(photoUrl).circleCrop().into(imgLogo);
} }
} }
if (snapshot.hasChild("schedule")) {
for (int i = 0; i < dayKeys.length; i++) {
DataSnapshot daySnapshot = snapshot.child("schedule").child(dayKeys[i]);
if (daySnapshot.exists()) {
ScheduleDay sd = daySnapshot.getValue(ScheduleDay.class);
if (sd != null) {
View view = scheduleViews.get(i);
CheckBox cbClosed = view.findViewById(R.id.cbClosed);
TextView tvOpenTime = view.findViewById(R.id.tvOpenTime);
TextView tvCloseTime = view.findViewById(R.id.tvCloseTime);
cbClosed.setChecked(sd.isClosed());
if (sd.getOpenTime() != null) tvOpenTime.setText(sd.getOpenTime());
if (sd.getCloseTime() != null) tvCloseTime.setText(sd.getCloseTime());
tvOpenTime.setEnabled(!sd.isClosed());
tvCloseTime.setEnabled(!sd.isClosed());
}
}
}
}
} }
} }
@@ -213,6 +276,17 @@ public class DefinicoesAdminActivity extends AppCompatActivity {
updates.put("logoUrl", photoUrl); updates.put("logoUrl", photoUrl);
} }
Map<String, ScheduleDay> scheduleMap = new HashMap<>();
for (int i = 0; i < dayKeys.length; i++) {
View view = scheduleViews.get(i);
CheckBox cbClosed = view.findViewById(R.id.cbClosed);
TextView tvOpenTime = view.findViewById(R.id.tvOpenTime);
TextView tvCloseTime = view.findViewById(R.id.tvCloseTime);
scheduleMap.put(dayKeys[i], new ScheduleDay(cbClosed.isChecked(), tvOpenTime.getText().toString(), tvCloseTime.getText().toString()));
}
updates.put("schedule", scheduleMap);
databaseReference.child(documentId).updateChildren(updates) databaseReference.child(documentId).updateChildren(updates)
.addOnSuccessListener(aVoid -> { .addOnSuccessListener(aVoid -> {
Toast.makeText(this, "Definições guardadas com sucesso!", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Definições guardadas com sucesso!", Toast.LENGTH_SHORT).show();

View File

@@ -118,9 +118,9 @@ public class DetalhesReservasActivity extends AppCompatActivity {
btnConfirmar.setOnClickListener(v -> { btnConfirmar.setOnClickListener(v -> {
com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex); com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex);
if ("Pendente".equals(item.getEstado())) { if ("Pendente".equals(item.getEstado())) {
atualizarEstadoSelecionado("Confirmada"); mostrarMesasDisponiveis();
} else if ("Confirmada".equals(item.getEstado())) { } else if ("Confirmada".equals(item.getEstado()) || item.getEstado().startsWith("Confirmada (Mesa")) {
atualizarEstadoSelecionado("Concluída"); 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() { private void showRecusarDialog() {
if (selectedIndex < 0) if (selectedIndex < 0)
return; return;
@@ -142,7 +233,7 @@ public class DetalhesReservasActivity extends AppCompatActivity {
new androidx.appcompat.app.AlertDialog.Builder(this) new androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("Motivo da Recusa") .setTitle("Motivo da Recusa")
.setItems(motivos, (dialog, which) -> { .setItems(motivos, (dialog, which) -> {
atualizarEstadoSelecionado("Recusada (" + motivos[which] + ")"); atualizarEstadoSelecionado("Recusada", motivos[which]);
}) })
.setNegativeButton("Voltar", null) .setNegativeButton("Voltar", null)
.show(); .show();
@@ -186,17 +277,27 @@ public class DetalhesReservasActivity extends AppCompatActivity {
toggleButtons(null); toggleButtons(null);
} }
private void atualizarEstadoSelecionado(String novoEstado) { private void atualizarEstadoSelecionado(String novoEstado, String motivo) {
if (selectedIndex < 0 || selectedIndex >= reservas.size()) { if (selectedIndex < 0 || selectedIndex >= reservas.size()) {
Toast.makeText(this, "Selecione uma reserva para atualizar.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Selecione uma reserva para atualizar.", Toast.LENGTH_SHORT).show();
return; return;
} }
com.example.pap_teste.models.Reserva item = reservas.get(selectedIndex); 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()) { if (task.isSuccessful()) {
txtMensagem txtMensagem
.setText(String.format("Reserva de %s marcada como %s.", item.getClienteEmail(), novoEstado)); .setText(String.format("Reserva de %s marcada como %s.", item.getClienteEmail(), novoEstado));
item.setEstado(novoEstado);
item.setMotivo(motivo);
mostrarDetalhe(item); mostrarDetalhe(item);
} }
}); });
@@ -206,7 +307,13 @@ public class DetalhesReservasActivity extends AppCompatActivity {
txtInfo.setText(String.format("%s • %s • %s • %dp", item.getClienteEmail(), item.getRestauranteName(), txtInfo.setText(String.format("%s • %s • %s • %dp", item.getClienteEmail(), item.getRestauranteName(),
item.getHora(), item.getPessoas())); item.getHora(), item.getPessoas()));
txtNotas.setText("Data: " + item.getData()); 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); toggleButtons(item);
} }
@@ -225,21 +332,23 @@ public class DetalhesReservasActivity extends AppCompatActivity {
btnRecusar.setVisibility(android.view.View.VISIBLE); btnRecusar.setVisibility(android.view.View.VISIBLE);
btnApagar.setVisibility(android.view.View.GONE); btnApagar.setVisibility(android.view.View.GONE);
break; 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": case "Concluída":
btnConfirmar.setVisibility(android.view.View.GONE); btnConfirmar.setVisibility(android.view.View.GONE);
btnRecusar.setVisibility(android.view.View.GONE); btnRecusar.setVisibility(android.view.View.GONE);
btnApagar.setVisibility(android.view.View.VISIBLE); btnApagar.setVisibility(android.view.View.VISIBLE);
break; break;
default: // Recusada or Cancelada 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); btnConfirmar.setVisibility(android.view.View.GONE);
btnRecusar.setVisibility(android.view.View.GONE); btnRecusar.setVisibility(android.view.View.GONE);
btnApagar.setVisibility(android.view.View.VISIBLE); // Allow deleting refused ones as well btnApagar.setVisibility(android.view.View.VISIBLE); // Allow deleting refused ones as well
}
break; break;
} }
} }

View File

@@ -99,6 +99,7 @@ public class EstablishmentDashboardActivity extends AppCompatActivity {
loadProximasReservas(); loadProximasReservas();
loadEstatisticas(); loadEstatisticas();
checkPendingReservationsOnLogin();
} }
private void loadProximasReservas() { private void loadProximasReservas() {
@@ -247,4 +248,70 @@ public class EstablishmentDashboardActivity extends AppCompatActivity {
} }
}); });
} }
private void checkPendingReservationsOnLogin() {
String email = getIntent().getStringExtra(MainActivity.EXTRA_EMAIL);
if (email == null) {
if (com.google.firebase.auth.FirebaseAuth.getInstance().getCurrentUser() != null) {
email = com.google.firebase.auth.FirebaseAuth.getInstance().getCurrentUser().getEmail();
} else {
return;
}
}
final String finalEmail = email != null ? email.trim() : "";
com.google.firebase.database.FirebaseDatabase.getInstance().getReference("reservas")
.addListenerForSingleValueEvent(new com.google.firebase.database.ValueEventListener() {
@Override
public void onDataChange(@androidx.annotation.NonNull com.google.firebase.database.DataSnapshot snapshot) {
java.util.List<com.example.pap_teste.models.Reserva> pendingReservations = new java.util.ArrayList<>();
for (com.google.firebase.database.DataSnapshot ds : snapshot.getChildren()) {
com.example.pap_teste.models.Reserva r = ds.getValue(com.example.pap_teste.models.Reserva.class);
if (r != null && r.getRestauranteEmail() != null && r.getRestauranteEmail().trim().equalsIgnoreCase(finalEmail)) {
if ("Pendente".equals(r.getEstado())) {
pendingReservations.add(r);
}
}
}
if (!pendingReservations.isEmpty()) {
showPendingReservationsDialog(pendingReservations);
}
}
@Override
public void onCancelled(@androidx.annotation.NonNull com.google.firebase.database.DatabaseError error) {
}
});
}
private void showPendingReservationsDialog(java.util.List<com.example.pap_teste.models.Reserva> pendingReservations) {
if (isFinishing() || isDestroyed()) return;
android.app.AlertDialog.Builder builder = new android.app.AlertDialog.Builder(this);
builder.setTitle("Reservas Pendentes (" + pendingReservations.size() + ")");
StringBuilder message = new StringBuilder("Tem reservas a aguardar confirmação:\n\n");
for (com.example.pap_teste.models.Reserva r : pendingReservations) {
message.append("").append(r.getData()).append(" às ").append(r.getHora())
.append(" - ").append(r.getPessoas()).append(" pessoas")
.append("\n Cliente: ").append(r.getClienteEmail()).append("\n\n");
}
builder.setMessage(message.toString());
builder.setPositiveButton("Ver Reservas", (dialog, which) -> {
Intent intent = new Intent(EstablishmentDashboardActivity.this, DetalhesReservasActivity.class);
intent.putExtra(MainActivity.EXTRA_EMAIL, getIntent().getStringExtra(MainActivity.EXTRA_EMAIL));
startActivity(intent);
});
builder.setNegativeButton("Fechar", (dialog, which) -> dialog.dismiss());
android.app.AlertDialog dialog = builder.create();
dialog.show();
android.widget.TextView textView = dialog.findViewById(android.R.id.message);
if (textView != null) {
textView.setTextSize(18);
textView.setGravity(android.view.Gravity.CENTER);
}
}
} }

View File

@@ -105,7 +105,7 @@ public class ListaEsperaActivity extends AppCompatActivity {
List<com.example.pap_teste.models.Mesa> mesasLivres = new ArrayList<>(); List<com.example.pap_teste.models.Mesa> mesasLivres = new ArrayList<>();
for (DataSnapshot ds : snapshot.getChildren()) { for (DataSnapshot ds : snapshot.getChildren()) {
com.example.pap_teste.models.Mesa m = ds.getValue(com.example.pap_teste.models.Mesa.class); 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()); m.setId(ds.getKey());
mesasLivres.add(m); mesasLivres.add(m);
} }
@@ -114,7 +114,7 @@ public class ListaEsperaActivity extends AppCompatActivity {
if (mesasLivres.isEmpty()) { if (mesasLivres.isEmpty()) {
new androidx.appcompat.app.AlertDialog.Builder(ListaEsperaActivity.this) new androidx.appcompat.app.AlertDialog.Builder(ListaEsperaActivity.this)
.setTitle("Sem mesas disponíveis") .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")) .setPositiveButton("Sim", (dialog, which) -> atualizarEstadoSelecionado("Confirmada"))
.setNegativeButton("Não", null) .setNegativeButton("Não", null)
.show(); .show();

View File

@@ -161,11 +161,18 @@ public class MainActivity extends AppCompatActivity {
} }
} }
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()) { if (!permissionsNeeded.isEmpty()) {
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle("Permissões Necessárias") .setTitle("Permissões Necessárias")
.setMessage( .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) -> { .setPositiveButton("Configurar", (dialog, which) -> {
permissionRequest.launch(permissionsNeeded.toArray(new String[0])); permissionRequest.launch(permissionsNeeded.toArray(new String[0]));
}) })

View File

@@ -120,7 +120,20 @@ public class MinhasReservasActivity extends AppCompatActivity {
} }
private void cancelReservation(Reserva reserva) { private void cancelReservation(Reserva reserva) {
databaseReference.child(reserva.getId()).child("estado").setValue("Cancelada") 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 -> { .addOnCompleteListener(task -> {
if (task.isSuccessful()) { if (task.isSuccessful()) {
Toast.makeText(this, "Reserva cancelada.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Reserva cancelada.", Toast.LENGTH_SHORT).show();
@@ -128,5 +141,8 @@ public class MinhasReservasActivity extends AppCompatActivity {
Toast.makeText(this, "Erro ao cancelar reserva.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Erro ao cancelar reserva.", Toast.LENGTH_SHORT).show();
} }
}); });
})
.setNegativeButton("Voltar", null)
.show();
} }
} }

View File

@@ -79,6 +79,18 @@ public class NovaReservaActivity extends AppCompatActivity {
} else { } else {
txtTitle.setText("Reserva: " + (selectedRestaurant != null ? selectedRestaurant.getName() : "")); txtTitle.setText("Reserva: " + (selectedRestaurant != null ? selectedRestaurant.getName() : ""));
setupReservationOptions(); setupReservationOptions();
android.widget.TextView txtFechado = findViewById(R.id.txtRestauranteFechado);
boolean isFechado = selectedRestaurant != null && selectedRestaurant.isFechadoWebsite();
if (txtFechado != null) {
txtFechado.setVisibility(isFechado ? android.view.View.VISIBLE : android.view.View.GONE);
}
findViewById(R.id.btnSelectDate).setEnabled(!isFechado);
findViewById(R.id.btnSelectTime).setEnabled(!isFechado);
findViewById(R.id.etPartySize).setEnabled(!isFechado);
findViewById(R.id.btnConfirmarReserva).setEnabled(!isFechado);
} }
} }
@@ -127,10 +139,23 @@ public class NovaReservaActivity extends AppCompatActivity {
String email = ds.child("email").getValue(String.class); String email = ds.child("email").getValue(String.class);
String cat = ds.child("category").getValue(String.class); String cat = ds.child("category").getValue(String.class);
String logoUrl = ds.child("logoUrl").getValue(String.class); String logoUrl = ds.child("logoUrl").getValue(String.class);
Boolean fechadoWebsite = ds.child("fechadoWebsite").getValue(Boolean.class);
if (name != null && email != null) { if (name != null && email != null) {
filteredList.add(new com.example.pap_teste.models.Restaurant(name, cat, email, com.example.pap_teste.models.Restaurant rest = new com.example.pap_teste.models.Restaurant(name, cat, email,
false, logoUrl)); false, logoUrl);
if (fechadoWebsite != null) {
rest.setFechadoWebsite(fechadoWebsite);
}
if (ds.hasChild("schedule")) {
java.util.Map<String, com.example.pap_teste.models.ScheduleDay> schMap = new java.util.HashMap<>();
for (com.google.firebase.database.DataSnapshot schDs : ds.child("schedule").getChildren()) {
com.example.pap_teste.models.ScheduleDay sd = schDs.getValue(com.example.pap_teste.models.ScheduleDay.class);
schMap.put(schDs.getKey(), sd);
}
rest.setSchedule(schMap);
}
filteredList.add(rest);
} }
} }
} }
@@ -165,17 +190,70 @@ public class NovaReservaActivity extends AppCompatActivity {
btnDate.setOnClickListener(v -> { btnDate.setOnClickListener(v -> {
java.util.Calendar cal = java.util.Calendar.getInstance(); java.util.Calendar cal = java.util.Calendar.getInstance();
new android.app.DatePickerDialog(this, (view, year, month, dayOfMonth) -> { new android.app.DatePickerDialog(this, (view, year, month, dayOfMonth) -> {
java.util.Calendar selectedCal = java.util.Calendar.getInstance();
selectedCal.set(year, month, dayOfMonth);
int dayOfWeek = selectedCal.get(java.util.Calendar.DAY_OF_WEEK);
String[] dayKeys = {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"};
String dayKey = dayKeys[dayOfWeek - 1];
boolean isClosed = false;
if (selectedRestaurant.getSchedule() != null && selectedRestaurant.getSchedule().containsKey(dayKey)) {
com.example.pap_teste.models.ScheduleDay sd = selectedRestaurant.getSchedule().get(dayKey);
if (sd != null && sd.isClosed()) isClosed = true;
}
if (isClosed) {
android.widget.Toast.makeText(this, "O restaurante encontra-se encerrado na data selecionada.", android.widget.Toast.LENGTH_LONG).show();
selectedDate = null;
btnDate.setText("Selecionar Data");
btnTime.setEnabled(false);
} else {
selectedDate = dayOfMonth + "/" + (month + 1) + "/" + year; selectedDate = dayOfMonth + "/" + (month + 1) + "/" + year;
btnDate.setText(selectedDate); btnDate.setText(selectedDate);
btnTime.setEnabled(true);
}
}, cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH), }, cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH),
cal.get(java.util.Calendar.DAY_OF_MONTH)).show(); cal.get(java.util.Calendar.DAY_OF_MONTH)).show();
}); });
btnTime.setOnClickListener(v -> { btnTime.setOnClickListener(v -> {
if (selectedDate == null) {
android.widget.Toast.makeText(this, "Por favor, selecione primeiro a data.", android.widget.Toast.LENGTH_SHORT).show();
return;
}
java.util.Calendar cal = java.util.Calendar.getInstance(); java.util.Calendar cal = java.util.Calendar.getInstance();
new android.app.TimePickerDialog(this, (view, hourOfDay, minute) -> { new android.app.TimePickerDialog(this, (view, hourOfDay, minute) -> {
selectedTime = String.format(java.util.Locale.getDefault(), "%02d:%02d", hourOfDay, minute); String chosenTime = String.format(java.util.Locale.getDefault(), "%02d:%02d", hourOfDay, minute);
boolean isOutOfHours = false;
if (selectedDate != null && selectedRestaurant.getSchedule() != null) {
try {
java.util.Date d = new java.text.SimpleDateFormat("d/M/yyyy", java.util.Locale.getDefault()).parse(selectedDate);
java.util.Calendar selectedCal = java.util.Calendar.getInstance();
if (d != null) selectedCal.setTime(d);
int dayOfWeek = selectedCal.get(java.util.Calendar.DAY_OF_WEEK);
String[] dayKeys = {"sunday", "monday", "tuesday", "wednesday", "thursday", "friday", "saturday"};
String dayKey = dayKeys[dayOfWeek - 1];
com.example.pap_teste.models.ScheduleDay sd = selectedRestaurant.getSchedule().get(dayKey);
if (sd != null && !sd.isClosed()) {
if (sd.getOpenTime() != null && sd.getCloseTime() != null && !sd.getOpenTime().isEmpty() && !sd.getCloseTime().isEmpty()) {
if (chosenTime.compareTo(sd.getOpenTime()) < 0 || chosenTime.compareTo(sd.getCloseTime()) > 0) {
isOutOfHours = true;
}
}
}
} catch (Exception e) {}
}
if (isOutOfHours) {
android.widget.Toast.makeText(this, "Por favor selecione uma hora dentro do horário de funcionamento.", android.widget.Toast.LENGTH_LONG).show();
selectedTime = null;
btnTime.setText("Selecionar Hora");
} else {
selectedTime = chosenTime;
btnTime.setText(selectedTime); btnTime.setText(selectedTime);
}
}, cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE), true).show(); }, cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE), true).show();
}); });

View File

@@ -118,6 +118,7 @@ public class ProfileDashboardActivity extends AppCompatActivity {
private void performLogOut() { private void performLogOut() {
try { try {
stopService(new Intent(this, ReservationNotificationService.class));
FirebaseAuth.getInstance().signOut(); FirebaseAuth.getInstance().signOut();
Toast.makeText(this, "Sessão terminada com sucesso.", Toast.LENGTH_SHORT).show(); Toast.makeText(this, "Sessão terminada com sucesso.", Toast.LENGTH_SHORT).show();

View File

@@ -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;
}
}

View File

@@ -9,6 +9,7 @@ public class Reserva {
private String hora; private String hora;
private int pessoas; private int pessoas;
private String estado; // Pendente, Confirmada, Concluída, Cancelada, Recusada private String estado; // Pendente, Confirmada, Concluída, Cancelada, Recusada
private String motivo;
public Reserva() { public Reserva() {
// Required for Firebase // Required for Firebase
@@ -89,4 +90,12 @@ public class Reserva {
public void setEstado(String estado) { public void setEstado(String estado) {
this.estado = estado; this.estado = estado;
} }
public String getMotivo() {
return motivo;
}
public void setMotivo(String motivo) {
this.motivo = motivo;
}
} }

View File

@@ -10,6 +10,7 @@ public class Restaurant implements Serializable {
private String logoUrl; private String logoUrl;
private Double ratingAvg; private Double ratingAvg;
private Integer ratingCount; private Integer ratingCount;
private boolean fechadoWebsite;
// No-argument constructor required for Firebase // No-argument constructor required for Firebase
public Restaurant() { public Restaurant() {
@@ -41,9 +42,15 @@ public class Restaurant implements Serializable {
public void setEmail(String email) { this.email = email; } public void setEmail(String email) { this.email = email; }
public void setAvailable(boolean available) { this.available = available; } public void setAvailable(boolean available) { this.available = available; }
public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; } public void setLogoUrl(String logoUrl) { this.logoUrl = logoUrl; }
public boolean isFechadoWebsite() { return fechadoWebsite; }
public void setFechadoWebsite(boolean fechadoWebsite) { this.fechadoWebsite = fechadoWebsite; }
public Double getRatingAvg() { return ratingAvg; } public Double getRatingAvg() { return ratingAvg; }
public void setRatingAvg(Double ratingAvg) { this.ratingAvg = ratingAvg; } public void setRatingAvg(Double ratingAvg) { this.ratingAvg = ratingAvg; }
public Integer getRatingCount() { return ratingCount; } public Integer getRatingCount() { return ratingCount; }
public void setRatingCount(Integer ratingCount) { this.ratingCount = ratingCount; } public void setRatingCount(Integer ratingCount) { this.ratingCount = ratingCount; }
private java.util.Map<String, ScheduleDay> schedule;
public java.util.Map<String, ScheduleDay> getSchedule() { return schedule; }
public void setSchedule(java.util.Map<String, ScheduleDay> schedule) { this.schedule = schedule; }
} }

View File

@@ -0,0 +1,43 @@
package com.example.pap_teste.models;
import java.io.Serializable;
public class ScheduleDay implements Serializable {
private boolean closed;
private String openTime;
private String closeTime;
public ScheduleDay() {
// No-argument constructor for Firebase
}
public ScheduleDay(boolean closed, String openTime, String closeTime) {
this.closed = closed;
this.openTime = openTime;
this.closeTime = closeTime;
}
public boolean isClosed() {
return closed;
}
public void setClosed(boolean closed) {
this.closed = closed;
}
public String getOpenTime() {
return openTime;
}
public void setOpenTime(String openTime) {
this.openTime = openTime;
}
public String getCloseTime() {
return closeTime;
}
public void setCloseTime(String closeTime) {
this.closeTime = closeTime;
}
}

View File

@@ -112,24 +112,6 @@
</LinearLayout> </LinearLayout>
</com.google.android.material.card.MaterialCardView> </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 --> <!-- Loading Spinner -->
<ProgressBar <ProgressBar
@@ -173,36 +155,14 @@
tools:listitem="@layout/item_restaurant_featured" /> tools:listitem="@layout/item_restaurant_featured" />
</LinearLayout> </LinearLayout>
<!-- Main Restaurant Grid/List --> <!-- Dynamic Categories Container -->
<LinearLayout <LinearLayout
android:id="@+id/layoutAllRestaurants" android:id="@+id/categoriesContainer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
android:visibility="gone" android:layout_marginTop="8dp"
android:layout_marginTop="32dp"> android:paddingBottom="16dp" />
<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>
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>

View File

@@ -156,6 +156,22 @@
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:background="@drawable/input_bg" /> android:background="@drawable/input_bg" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="Horário de Funcionamento"
android:textColor="@color/colorTextPrimary"
android:textSize="16sp"
android:fontFamily="sans-serif-medium" />
<LinearLayout
android:id="@+id/containerSchedule"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:orientation="vertical" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/btnSaveSettings" android:id="@+id/btnSaveSettings"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@@ -122,6 +122,19 @@
android:textStyle="bold" android:textStyle="bold"
android:layout_marginBottom="24dp" /> android:layout_marginBottom="24dp" />
<TextView
android:id="@+id/txtRestauranteFechado"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Restaurante fechado temporariamente. Não é possível efetuar reservas."
android:textColor="@android:color/holo_red_dark"
android:textSize="16sp"
android:textStyle="bold"
android:layout_marginBottom="16dp"
android:visibility="gone"
android:gravity="center"
android:padding="8dp" />
<TextView <TextView
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View 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>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical"
android:paddingVertical="8dp">
<CheckBox
android:id="@+id/cbClosed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Folga"
android:textColor="@color/colorTextSecondary" />
<TextView
android:id="@+id/tvDayName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="8dp"
android:text="Segunda-feira"
android:textColor="@color/colorTextPrimary"
android:textSize="14sp"
android:fontFamily="sans-serif-medium" />
<TextView
android:id="@+id/tvOpenTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="09:00"
android:padding="8dp"
android:background="@drawable/input_bg"
android:textColor="@color/colorTextPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="-"
android:layout_marginHorizontal="8dp"
android:textColor="@color/colorTextSecondary" />
<TextView
android:id="@+id/tvCloseTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="22:00"
android:padding="8dp"
android:background="@drawable/input_bg"
android:textColor="@color/colorTextPrimary" />
</LinearLayout>

View File

@@ -14,7 +14,28 @@
<!-- Typography base generic configuration --> <!-- Typography base generic configuration -->
<item name="android:fontFamily">sans-serif</item> <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>
<style name="Theme.Pap_teste" parent="Base.Theme.Pap_teste" /> <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> </resources>