diff --git a/reserva-mesa-dashboard/app/(auth)/login/page.tsx b/reserva-mesa-dashboard/app/(auth)/login/page.tsx
index 73c8870..9f94d84 100644
--- a/reserva-mesa-dashboard/app/(auth)/login/page.tsx
+++ b/reserva-mesa-dashboard/app/(auth)/login/page.tsx
@@ -23,11 +23,51 @@ export default function LoginPage() {
setLoading(true);
try {
- await signInWithEmailAndPassword(auth, email, password);
+ const userCredential = await signInWithEmailAndPassword(auth, email, password);
+
+ // Verify the user has a restaurant record before redirecting
+ if (!userCredential.user?.email) {
+ setError("Erro: Utilizador sem email válido.");
+ return;
+ }
+
+ // Success — redirect to dashboard
router.push("/");
} catch (err: any) {
- console.error(err);
- setError("Credenciais inválidas. Verifique o seu email e palavra-passe.");
+ console.error("[Login Error]", err.code, err.message);
+
+ // Map Firebase Auth error codes to user-friendly messages in Portuguese
+ switch (err.code) {
+ case "auth/user-not-found":
+ setError("Esta conta não existe. Verifique o email ou registe-se.");
+ break;
+ case "auth/wrong-password":
+ setError("Palavra-passe incorrecta. Tente novamente.");
+ break;
+ case "auth/invalid-email":
+ setError("Email inválido. Verifique o formato do email.");
+ break;
+ case "auth/invalid-credential":
+ setError("Credenciais inválidas. Verifique o email e a palavra-passe.");
+ break;
+ case "auth/too-many-requests":
+ setError("Demasiadas tentativas falhadas. Aguarde alguns minutos e tente novamente.");
+ break;
+ case "auth/invalid-verification-id":
+ setError("Sessão expirada. Por favor, recarregue a página e tente novamente.");
+ break;
+ case "auth/network-request-failed":
+ setError("Erro de conexão. Verifique a sua ligação à internet.");
+ break;
+ case "auth/weak-password":
+ setError("A palavra-passe é demasiado curta. Deve ter pelo menos 6 caracteres.");
+ break;
+ case "auth/popup-closed-by-user":
+ // User closed the popup — no error needed
+ break;
+ default:
+ setError("Erro ao entrar. Verifique as suas credenciais e tente novamente.");
+ }
} finally {
setLoading(false);
}
diff --git a/reserva-mesa-dashboard/app/(auth)/register/page.tsx b/reserva-mesa-dashboard/app/(auth)/register/page.tsx
index 402df26..73ae056 100644
--- a/reserva-mesa-dashboard/app/(auth)/register/page.tsx
+++ b/reserva-mesa-dashboard/app/(auth)/register/page.tsx
@@ -37,6 +37,23 @@ export default function RegisterPage() {
setError("");
setLoading(true);
+ // Validate inputs before attempting registration
+ if (!formData.email || !formData.email.includes("@")) {
+ setError("Por favor, insira um email válido.");
+ setLoading(false);
+ return;
+ }
+ if (formData.password.length < 6) {
+ setError("A palavra-passe deve ter pelo menos 6 caracteres.");
+ setLoading(false);
+ return;
+ }
+ if (!formData.establishmentName.trim()) {
+ setError("Por favor, insira o nome do restaurante.");
+ setLoading(false);
+ return;
+ }
+
try {
// 1. Criar utilizador na Firebase Auth
const userCredential = await createUserWithEmailAndPassword(auth, formData.email, formData.password);
@@ -62,10 +79,37 @@ export default function RegisterPage() {
// 3. Gravar na Realtime Database em /Restaurantes
await set(ref(db, `Restaurantes/${documentId}`), payload);
+ // 4. Success — redirect to dashboard
router.push("/");
} catch (err: any) {
- console.error(err);
- setError(err.message || "Ocorreu um erro ao registar o restaurante.");
+ console.error("[Register Error]", err.code, err.message);
+
+ // Map Firebase Auth error codes to user-friendly messages in Portuguese
+ switch (err.code) {
+ case "auth/email-already-in-use":
+ setError("Este email já está registado. Tente fazer login.");
+ break;
+ case "auth/weak-password":
+ setError("A palavra-passe deve ter pelo menos 6 caracteres.");
+ break;
+ case "auth/invalid-email":
+ setError("Email inválido. Verifique o formato do email.");
+ break;
+ case "auth/operation-not-allowed":
+ setError("Registo de contas está desactivado. Contacte o suporte.");
+ break;
+ case "auth/network-request-failed":
+ setError("Erro de conexão. Verifique a sua ligação à internet.");
+ break;
+ case "auth/too-many-requests":
+ setError("Demasiadas tentativas. Aguarde alguns minutos e tente novamente.");
+ break;
+ case "auth/invalid-credential":
+ setError("Credenciais inválidas.");
+ break;
+ default:
+ setError(`Erro ao criar conta: ${err.message || "Tente novamente."}`);
+ }
} finally {
setLoading(false);
}
diff --git a/reserva-mesa-dashboard/app/(dashboard)/page.tsx b/reserva-mesa-dashboard/app/(dashboard)/page.tsx
index 39f48d4..635970c 100644
--- a/reserva-mesa-dashboard/app/(dashboard)/page.tsx
+++ b/reserva-mesa-dashboard/app/(dashboard)/page.tsx
@@ -21,6 +21,15 @@ export default function DashboardHomePage() {
const { mesas, loading: loadingMesas } = useMesas();
const { staff } = useStaff();
+ // Guard against missing user data
+ if (!user) {
+ return (
+
+ );
+ }
+
// 1. Calculate top stats
const todayStr = new Date().toISOString().split('T')[0];
const todayReservas = reservas.filter(r => r.data === todayStr || r.estado.startsWith("Confirmada"));
diff --git a/reserva-mesa-dashboard/components/auth/AuthGuard.tsx b/reserva-mesa-dashboard/components/auth/AuthGuard.tsx
index 1209e72..f6bd417 100644
--- a/reserva-mesa-dashboard/components/auth/AuthGuard.tsx
+++ b/reserva-mesa-dashboard/components/auth/AuthGuard.tsx
@@ -11,6 +11,7 @@ export default function AuthGuard({ children }: { children: React.ReactNode }) {
useEffect(() => {
if (!loading) {
+ // If user is not authenticated and not on a public page, redirect to login
if (!user && !pathname.startsWith("/login") && !pathname.startsWith("/register")) {
router.push("/login");
}
@@ -25,8 +26,8 @@ export default function AuthGuard({ children }: { children: React.ReactNode }) {
);
}
- // Se não estiver logado e não estiver numa rota pública, não renderiza nada
- // (o useEffect vai redirecionar)
+ // If not authenticated and not on a public page, don't render children
+ // (the useEffect will redirect)
if (!user && !pathname.startsWith("/login") && !pathname.startsWith("/register")) {
return null;
}
diff --git a/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx b/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx
index da207e1..218378c 100644
--- a/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx
+++ b/reserva-mesa-dashboard/components/dashboard/NotificationMonitor.tsx
@@ -20,12 +20,17 @@ export function NotificationMonitor() {
// Primeiro, marcamos todas as reservas existentes como "vistas"
// para não disparar notificações para o passado
const loadExisting = async () => {
- const snapshot = await get(reservasRef);
- if (snapshot.exists()) {
- const data = snapshot.val();
- Object.keys(data).forEach(id => seenReservas.current.add(id));
+ try {
+ const snapshot = await get(reservasRef);
+ if (snapshot.exists()) {
+ const data = snapshot.val();
+ Object.keys(data).forEach(id => seenReservas.current.add(id));
+ }
+ } catch (error) {
+ console.error("[NotificationMonitor] Error loading existing reservas:", error);
+ } finally {
+ isInitialLoad.current = false;
}
- isInitialLoad.current = false;
};
loadExisting();
@@ -40,9 +45,13 @@ export function NotificationMonitor() {
// Se ainda estivermos no load inicial (do get), ignoramos o toast
if (isInitialLoad.current) return;
- const data = snapshot.val();
- if (data.restauranteEmail === user.email && data.estado === "Pendente") {
- toast(`Nova reserva recebida de ${data.clienteEmail}!`, "info");
+ try {
+ const data = snapshot.val();
+ if (data?.restauranteEmail === user.email && data?.estado === "Pendente") {
+ toast(`Nova reserva recebida de ${data?.clienteEmail || "cliente"}!`, "info");
+ }
+ } catch (error) {
+ console.error("[NotificationMonitor] Error processing new reserva:", error);
}
});
diff --git a/reserva-mesa-dashboard/contexts/AuthContext.tsx b/reserva-mesa-dashboard/contexts/AuthContext.tsx
index cdba70d..9416145 100644
--- a/reserva-mesa-dashboard/contexts/AuthContext.tsx
+++ b/reserva-mesa-dashboard/contexts/AuthContext.tsx
@@ -33,34 +33,106 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
return email.replace(/\./g, "_").replace(/@/g, "_at_");
};
+ // Search for user by UID across all Restaurantes documents
+ const findUserByUid = async (uid: string, currentUser: FirebaseUser): Promise => {
+ try {
+ const restaurantsRef = ref(db, "Restaurantes");
+ const snapshot = await get(restaurantsRef);
+ if (!snapshot.exists()) return null;
+
+ const data = snapshot.val();
+ for (const key of Object.keys(data)) {
+ const item = data[key];
+ if (item?.uid === uid) {
+ return { ...item, email: item.email || currentUser.email } as RestaurantUser;
+ }
+ }
+ } catch (error) {
+ console.error("[Auth] Error searching by UID:", error);
+ }
+ return null;
+ };
+
+ // Search for user by email across all Restaurantes documents
+ const findUserByEmail = async (email: string): Promise => {
+ try {
+ const restaurantsRef = ref(db, "Restaurantes");
+ const snapshot = await get(restaurantsRef);
+ if (!snapshot.exists()) return null;
+
+ const data = snapshot.val();
+ for (const key of Object.keys(data)) {
+ const item = data[key];
+ if (item?.email === email || item?.establishmentEmail === email || item?.ownerEmail === email) {
+ return { ...item, email: item.email || email } as RestaurantUser;
+ }
+ }
+ } catch (error) {
+ console.error("[Auth] Error searching by email:", error);
+ }
+ return null;
+ };
+
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (currentUser) => {
try {
if (currentUser && currentUser.email) {
setFirebaseUser(currentUser);
+
+ let data: RestaurantUser | null = null;
+
+ // Strategy 1: Try building document ID from email (website format)
const docId = buildDocumentId(currentUser.email);
const userRef = ref(db, `Restaurantes/${docId}`);
- const snapshot = await get(userRef);
+ let snapshot = await get(userRef);
if (snapshot.exists()) {
- const data = snapshot.val() as RestaurantUser;
+ data = snapshot.val() as RestaurantUser;
+ }
+
+ // Strategy 2: Try with original email as document ID (Android format)
+ if (!data) {
+ const originalRef = ref(db, `Restaurantes/${currentUser.email}`);
+ snapshot = await get(originalRef);
+ if (snapshot.exists()) {
+ data = snapshot.val() as RestaurantUser;
+ }
+ }
+
+ // Strategy 3: Search all Restaurantes documents by UID
+ if (!data && currentUser.uid) {
+ data = await findUserByUid(currentUser.uid, currentUser);
+ }
+
+ // Strategy 4: Search all Restaurantes documents by email
+ if (!data) {
+ data = await findUserByEmail(currentUser.email);
+ }
+
+ if (data) {
if (data.accountType === "ESTABELECIMENTO") {
setUser(data);
} else {
- console.warn("User is not a restaurant");
+ console.warn("[Auth] User is not a restaurant account type:", data.accountType);
setUser(null);
await signOut(auth);
}
} else {
- console.warn("User record not found in Restaurantes");
+ // User exists in Auth but not in database — possible orphaned account
+ console.warn("[Auth] User record not found in Restaurantes for:", currentUser.email);
+ console.warn("[Auth] UID:", currentUser.uid);
setUser(null);
}
} else {
+ // No Firebase user — clear state
setFirebaseUser(null);
setUser(null);
}
} catch (error) {
- console.error("Auth initialization error:", error);
+ console.error("[Auth] Error in onAuthStateChanged:", error);
+ // On error, clear user state to prevent broken state
+ setUser(null);
+ setFirebaseUser(null);
} finally {
setLoading(false);
}
diff --git a/reserva-mesa-dashboard/hooks/useMesas.ts b/reserva-mesa-dashboard/hooks/useMesas.ts
index 1851552..93abdd6 100644
--- a/reserva-mesa-dashboard/hooks/useMesas.ts
+++ b/reserva-mesa-dashboard/hooks/useMesas.ts
@@ -17,26 +17,32 @@ export function useMesas() {
const mesasRef = ref(db, "Mesas");
const unsubscribe = onValue(mesasRef, (snapshot) => {
- const data = snapshot.val();
- const list: Mesa[] = [];
-
- if (data) {
- Object.keys(data).forEach((key) => {
- const item = data[key];
- if (item.restauranteEmail === user.email) {
- list.push({
- id: key,
- ...item
- });
- }
- });
- }
-
- // Sort by table number
- list.sort((a, b) => a.numero - b.numero);
+ try {
+ const data = snapshot.val();
+ const list: Mesa[] = [];
+
+ if (data) {
+ Object.keys(data).forEach((key) => {
+ const item = data[key];
+ if (item?.restauranteEmail === user.email) {
+ list.push({
+ id: key,
+ ...item
+ });
+ }
+ });
+ }
+
+ // Sort by table number
+ list.sort((a, b) => a.numero - b.numero);
- setMesas(list);
- setLoading(false);
+ setMesas(list);
+ } catch (error) {
+ console.error("[useMesas] Error processing data:", error);
+ setMesas([]);
+ } finally {
+ setLoading(false);
+ }
});
return () => off(mesasRef, "value", unsubscribe);
diff --git a/reserva-mesa-dashboard/hooks/useReservas.ts b/reserva-mesa-dashboard/hooks/useReservas.ts
index 48a573a..0aeaf98 100644
--- a/reserva-mesa-dashboard/hooks/useReservas.ts
+++ b/reserva-mesa-dashboard/hooks/useReservas.ts
@@ -17,30 +17,36 @@ export function useReservas() {
const reservasRef = ref(db, "reservas");
const unsubscribe = onValue(reservasRef, (snapshot) => {
- const data = snapshot.val();
- const list: Reserva[] = [];
-
- if (data) {
- Object.keys(data).forEach((key) => {
- const item = data[key];
- if (item.restauranteEmail === user.email) {
- list.push({
- id: key,
- ...item
- });
- }
+ try {
+ const data = snapshot.val();
+ const list: Reserva[] = [];
+
+ if (data) {
+ Object.keys(data).forEach((key) => {
+ const item = data[key];
+ if (item?.restauranteEmail === user.email) {
+ list.push({
+ id: key,
+ ...item
+ });
+ }
+ });
+ }
+
+ // Sort by date and time (newest first for management)
+ list.sort((a, b) => {
+ const dateA = new Date(`${a.data.replace(/-/g, "/")} ${a.hora}`);
+ const dateB = new Date(`${b.data.replace(/-/g, "/")} ${b.hora}`);
+ return dateB.getTime() - dateA.getTime();
});
- }
-
- // Sort by date and time (newest first for management)
- list.sort((a, b) => {
- const dateA = new Date(`${a.data.replace(/-/g, "/")} ${a.hora}`);
- const dateB = new Date(`${b.data.replace(/-/g, "/")} ${b.hora}`);
- return dateB.getTime() - dateA.getTime();
- });
- setReservas(list);
- setLoading(false);
+ setReservas(list);
+ } catch (error) {
+ console.error("[useReservas] Error processing data:", error);
+ setReservas([]);
+ } finally {
+ setLoading(false);
+ }
});
return () => off(reservasRef, "value", unsubscribe);
diff --git a/reserva-mesa-dashboard/hooks/useStaff.ts b/reserva-mesa-dashboard/hooks/useStaff.ts
index 1bb327d..864cf54 100644
--- a/reserva-mesa-dashboard/hooks/useStaff.ts
+++ b/reserva-mesa-dashboard/hooks/useStaff.ts
@@ -17,23 +17,29 @@ export function useStaff() {
const staffRef = ref(db, "Staff");
const unsubscribe = onValue(staffRef, (snapshot) => {
- const data = snapshot.val();
- const list: Staff[] = [];
-
- if (data) {
- Object.keys(data).forEach((key) => {
- const item = data[key];
- if (item.restauranteEmail === user.email) {
- list.push({
- id: key,
- ...item
- });
- }
- });
+ try {
+ const data = snapshot.val();
+ const list: Staff[] = [];
+
+ if (data) {
+ Object.keys(data).forEach((key) => {
+ const item = data[key];
+ if (item?.restauranteEmail === user.email) {
+ list.push({
+ id: key,
+ ...item
+ });
+ }
+ });
+ }
+
+ setStaff(list);
+ } catch (error) {
+ console.error("[useStaff] Error processing data:", error);
+ setStaff([]);
+ } finally {
+ setLoading(false);
}
-
- setStaff(list);
- setLoading(false);
});
return () => off(staffRef, "value", unsubscribe);
diff --git a/reserva-mesa-dashboard/lib/firebase.ts b/reserva-mesa-dashboard/lib/firebase.ts
index f743d8f..4ee3470 100644
--- a/reserva-mesa-dashboard/lib/firebase.ts
+++ b/reserva-mesa-dashboard/lib/firebase.ts
@@ -1,22 +1,53 @@
-import { initializeApp, getApps } from "firebase/app";
+import { initializeApp, getApps, getApp } from "firebase/app";
import { getAuth } from "firebase/auth";
import { getDatabase } from "firebase/database";
// As variáveis de ambiente devem ser configuradas no Vercel e no ficheiro .env.local
const firebaseConfig = {
- apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY || "AIzaSyCPz7Pd3tJj3QkF7fV_vudCJythNsyR57k",
- authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN || "namesa-429c1.firebaseapp.com",
- projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID || "namesa-429c1",
- storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET || "namesa-429c1.firebasestorage.app",
- messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID || "476421715902",
- appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID || "1:476421715902:web:placeholder", // placeholder needed for web client SDK
- // Nota importante: Como verificado na codebase Android,
- // O ReservaMesa usa Realtime Database e não Firestore.
+ apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
+ authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
+ projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
+ storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
+ messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
+ appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
databaseURL: process.env.NEXT_PUBLIC_FIREBASE_DATABASE_URL || "https://namesa-429c1-default-rtdb.firebaseio.com"
};
-// Initialize Firebase only if there are no apps initialized yet
-const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
+// Validate that all required environment variables are present
+const requiredEnvVars = [
+ "NEXT_PUBLIC_FIREBASE_API_KEY",
+ "NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN",
+ "NEXT_PUBLIC_FIREBASE_PROJECT_ID",
+ "NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET",
+ "NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID",
+ "NEXT_PUBLIC_FIREBASE_APP_ID",
+];
+
+const missingVars = requiredEnvVars.filter(
+ (key) => !process.env[key]
+);
+
+if (missingVars.length > 0) {
+ console.error(
+ "[Firebase] Missing environment variables:",
+ missingVars.join(", "),
+ "\nPlease ensure all Firebase environment variables are set in .env.local or your hosting platform."
+ );
+}
+
+// Initialize Firebase only once — prevents duplicate app errors in development (HMR) and SSR
+let app;
+try {
+ app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApp();
+} catch (error: any) {
+ if (error?.code === "app/duplicate-app") {
+ app = getApp();
+ } else {
+ throw new Error(
+ `[Firebase] Failed to initialize: ${error?.message || "Unknown error"}. Check your environment variables.`
+ );
+ }
+}
export const auth = getAuth(app);
export const db = getDatabase(app);
diff --git a/reserva-mesa-dashboard/package-lock.json b/reserva-mesa-dashboard/package-lock.json
index 2caa625..d4764ed 100644
--- a/reserva-mesa-dashboard/package-lock.json
+++ b/reserva-mesa-dashboard/package-lock.json
@@ -14,7 +14,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
- "firebase": "^10.12.0",
+ "firebase": "^10.14.1",
"framer-motion": "^11.1.7",
"lucide-react": "^0.378.0",
"next": "14.2.3",
diff --git a/reserva-mesa-dashboard/package.json b/reserva-mesa-dashboard/package.json
index 35f89a5..6f16048 100644
--- a/reserva-mesa-dashboard/package.json
+++ b/reserva-mesa-dashboard/package.json
@@ -10,10 +10,12 @@
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
+ "@radix-ui/react-label": "^2.0.2",
+ "@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"date-fns": "^3.6.0",
- "firebase": "^10.12.0",
+ "firebase": "^10.14.1",
"framer-motion": "^11.1.7",
"lucide-react": "^0.378.0",
"next": "14.2.3",
@@ -23,8 +25,6 @@
"recharts": "^2.12.7",
"tailwind-merge": "^2.3.0",
"tailwindcss-animate": "^1.0.7",
- "@radix-ui/react-label": "^2.0.2",
- "@radix-ui/react-slot": "^1.0.2",
"zod": "^3.23.6"
},
"devDependencies": {