chore: add project files and setup gitignore
This commit is contained in:
159
reserva-mesa-dashboard/app/(dashboard)/configuracoes/page.tsx
Normal file
159
reserva-mesa-dashboard/app/(dashboard)/configuracoes/page.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Store, Mail, Phone, MapPin, Save, CheckCircle2 } from "lucide-react";
|
||||
|
||||
export default function ConfiguracoesPage() {
|
||||
const { user, updateRestaurantProfile } = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [success, setSuccess] = useState(false);
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
establishmentName: "",
|
||||
category: "",
|
||||
phoneNumber: "",
|
||||
address: "",
|
||||
isAvailable: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setFormData({
|
||||
establishmentName: user.establishmentName || "",
|
||||
category: user.category || "",
|
||||
phoneNumber: user.phoneNumber || "",
|
||||
address: user.address || "",
|
||||
isAvailable: user.isAvailable !== false, // default true
|
||||
});
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setSuccess(false);
|
||||
|
||||
const res = await updateRestaurantProfile(formData);
|
||||
|
||||
if (res.success) {
|
||||
setSuccess(true);
|
||||
setTimeout(() => setSuccess(false), 3000);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="max-w-4xl space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Configurações</h1>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<Card className="border-border/50">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Perfil do Estabelecimento</CardTitle>
|
||||
<CardDescription>Gerencie as informações que os clientes veem na App.</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 bg-muted/30 px-4 py-2 rounded-lg border">
|
||||
<Label htmlFor="available" className="text-sm font-medium">Estado do Restaurante</Label>
|
||||
<Switch
|
||||
id="available"
|
||||
checked={formData.isAvailable}
|
||||
onCheckedChange={(checked) => setFormData({...formData, isAvailable: checked})}
|
||||
/>
|
||||
<span className={`text-xs font-bold uppercase ${formData.isAvailable ? "text-green-500" : "text-destructive"}`}>
|
||||
{formData.isAvailable ? "Aberto" : "Fechado"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Nome do Estabelecimento</Label>
|
||||
<div className="relative">
|
||||
<Store className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="name"
|
||||
className="pl-10"
|
||||
value={formData.establishmentName}
|
||||
onChange={(e) => setFormData({...formData, establishmentName: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category">Categoria / Cozinha</Label>
|
||||
<Input
|
||||
id="category"
|
||||
value={formData.category}
|
||||
onChange={(e) => setFormData({...formData, category: e.target.value})}
|
||||
placeholder="Ex: Portuguesa, Italiana..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email de Contacto (Não editável)</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
className="pl-10 bg-muted/50"
|
||||
value={user?.email || ""}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefone</Label>
|
||||
<div className="relative">
|
||||
<Phone className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="phone"
|
||||
className="pl-10"
|
||||
value={formData.phoneNumber}
|
||||
onChange={(e) => setFormData({...formData, phoneNumber: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="address">Morada Completa</Label>
|
||||
<div className="relative">
|
||||
<MapPin className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="address"
|
||||
className="pl-10"
|
||||
value={formData.address}
|
||||
onChange={(e) => setFormData({...formData, address: e.target.value})}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="flex items-center justify-end gap-4">
|
||||
{success && (
|
||||
<div className="flex items-center gap-2 text-green-500 font-medium animate-in fade-in slide-in-from-right-4">
|
||||
<CheckCircle2 className="h-5 w-5" /> Alterações guardadas!
|
||||
</div>
|
||||
)}
|
||||
<Button type="submit" disabled={loading} className="px-8 gap-2">
|
||||
{loading ? "A guardar..." : <><Save className="h-4 w-4" /> Guardar Alterações</>}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
164
reserva-mesa-dashboard/app/(dashboard)/equipa/page.tsx
Normal file
164
reserva-mesa-dashboard/app/(dashboard)/equipa/page.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useStaff } from "@/hooks/useStaff";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Users,
|
||||
UserPlus,
|
||||
Trash2,
|
||||
Mail,
|
||||
Phone,
|
||||
Briefcase,
|
||||
Search,
|
||||
Plus
|
||||
} from "lucide-react";
|
||||
|
||||
export default function EquipaPage() {
|
||||
const { staff, loading, addStaff, deleteStaff } = useStaff();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const [isAdding, setIsAdding] = useState(false);
|
||||
const [newMember, setNewMember] = useState({
|
||||
name: "",
|
||||
role: "",
|
||||
email: "",
|
||||
phoneNumber: ""
|
||||
});
|
||||
|
||||
const filteredStaff = staff.filter(s =>
|
||||
s.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
s.role.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const res = await addStaff(newMember);
|
||||
if (res.success) {
|
||||
setIsAdding(false);
|
||||
setNewMember({ name: "", role: "", email: "", phoneNumber: "" });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Gestão de Equipa</h1>
|
||||
<Button onClick={() => setIsAdding(!isAdding)} className="gap-2">
|
||||
{isAdding ? "Cancelar" : <><UserPlus className="h-4 w-4" /> Adicionar Funcionário</>}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isAdding && (
|
||||
<Card className="border-primary/20 bg-primary/5">
|
||||
<CardHeader>
|
||||
<CardTitle>Novo Funcionário</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleAdd} className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Nome</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newMember.name}
|
||||
onChange={e => setNewMember({...newMember, name: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="role">Cargo</Label>
|
||||
<Input
|
||||
id="role"
|
||||
value={newMember.role}
|
||||
onChange={e => setNewMember({...newMember, role: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={newMember.email}
|
||||
onChange={e => setNewMember({...newMember, email: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="phone">Telefone</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="phone"
|
||||
value={newMember.phoneNumber}
|
||||
onChange={e => setNewMember({...newMember, phoneNumber: e.target.value})}
|
||||
/>
|
||||
<Button type="submit">Adicionar</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Pesquisar por nome ou cargo..."
|
||||
className="pl-10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{loading ? (
|
||||
<div className="col-span-full flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : filteredStaff.length > 0 ? (
|
||||
filteredStaff.map((member) => (
|
||||
<Card key={member.id} className="overflow-hidden border-border/50 hover:shadow-md transition-all group">
|
||||
<CardHeader className="pb-2">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="h-12 w-12 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold text-xl">
|
||||
{member.name.charAt(0)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="text-muted-foreground hover:text-destructive opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={() => deleteStaff(member.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
<CardTitle className="mt-4">{member.name}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-1">
|
||||
<Briefcase className="h-3 w-3" /> {member.role}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3 pt-4 border-t border-border/50">
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<Mail className="h-4 w-4" /> {member.email}
|
||||
</div>
|
||||
{member.phoneNumber && (
|
||||
<div className="flex items-center gap-3 text-sm text-muted-foreground">
|
||||
<Phone className="h-4 w-4" /> {member.phoneNumber}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="col-span-full flex flex-col items-center justify-center py-20 text-center border-2 border-dashed rounded-xl">
|
||||
<Users className="h-12 w-12 text-muted-foreground/20 mb-4" />
|
||||
<h3 className="text-lg font-medium">Nenhum funcionário encontrado</h3>
|
||||
<p className="text-sm text-muted-foreground">Adicione membros à sua equipa para começar.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
80
reserva-mesa-dashboard/app/(dashboard)/historico/page.tsx
Normal file
80
reserva-mesa-dashboard/app/(dashboard)/historico/page.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
|
||||
import { useReservas } from "@/hooks/useReservas";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { History, Calendar, User, Search } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function HistoricoPage() {
|
||||
const { reservas, loading } = useReservas();
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const historico = reservas.filter(r =>
|
||||
["Concluída", "Recusada", "Cancelada"].includes(r.estado) ||
|
||||
(r.estado.includes("Confirmada") && new Date(r.data) < new Date())
|
||||
);
|
||||
|
||||
const filtered = historico.filter(r =>
|
||||
r.clienteEmail.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Histórico de Reservas</h1>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Pesquisar por email do cliente..."
|
||||
className="pl-10"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : filtered.length > 0 ? (
|
||||
filtered.map((reserva) => (
|
||||
<Card key={reserva.id} className="border-border/40 bg-card/50">
|
||||
<CardContent className="p-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`h-10 w-10 rounded-full flex items-center justify-center ${
|
||||
reserva.estado === "Concluída" ? "bg-green-500/10 text-green-500" : "bg-destructive/10 text-destructive"
|
||||
}`}>
|
||||
<History className="h-5 w-5" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{reserva.clienteEmail}</p>
|
||||
<div className="flex items-center gap-3 text-xs text-muted-foreground mt-0.5">
|
||||
<span className="flex items-center gap-1"><Calendar className="h-3 w-3" /> {reserva.data}</span>
|
||||
<span className="flex items-center gap-1"><User className="h-3 w-3" /> {reserva.pessoas} p.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`text-[10px] font-bold uppercase px-2 py-0.5 rounded border ${
|
||||
reserva.estado === "Concluída" ? "border-green-500/20 text-green-500" : "border-destructive/20 text-destructive"
|
||||
}`}>
|
||||
{reserva.estado}
|
||||
</span>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-center text-muted-foreground border-2 border-dashed rounded-xl">
|
||||
<History className="h-12 w-12 opacity-20 mb-4" />
|
||||
<p>Nenhum registo encontrado no histórico.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
38
reserva-mesa-dashboard/app/(dashboard)/layout.tsx
Normal file
38
reserva-mesa-dashboard/app/(dashboard)/layout.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from "react";
|
||||
import AuthGuard from "@/components/auth/AuthGuard";
|
||||
import { Sidebar } from "@/components/dashboard/Sidebar";
|
||||
import { MobileNav } from "@/components/dashboard/MobileNav";
|
||||
import { Header } from "@/components/dashboard/Header";
|
||||
|
||||
import { NotificationMonitor } from "@/components/dashboard/NotificationMonitor";
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<AuthGuard>
|
||||
<NotificationMonitor />
|
||||
<div className="flex min-h-screen bg-background">
|
||||
{/* Mobile Navigation */}
|
||||
<MobileNav />
|
||||
|
||||
{/* Desktop Sidebar */}
|
||||
<aside className="hidden md:flex md:w-64 md:flex-col md:fixed md:inset-y-0">
|
||||
<Sidebar />
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="md:pl-64 flex flex-col flex-1">
|
||||
<Header />
|
||||
<main className="flex-1">
|
||||
<div className="py-6 px-4 sm:px-6 md:px-8">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</AuthGuard>
|
||||
);
|
||||
}
|
||||
126
reserva-mesa-dashboard/app/(dashboard)/lista-espera/page.tsx
Normal file
126
reserva-mesa-dashboard/app/(dashboard)/lista-espera/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { useReservas } from "@/hooks/useReservas";
|
||||
import { useMesas } from "@/hooks/useMesas";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AssignTableModal } from "@/components/dashboard/AssignTableModal";
|
||||
import { Clock, User, ListOrdered, Check, X } from "lucide-react";
|
||||
import { Reserva } from "@/types/reserva";
|
||||
import { Mesa } from "@/types/mesa";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
export default function ListaEsperaPage() {
|
||||
const { reservas, loading: loadingReservas, confirmarComMesa, updateReservaEstado } = useReservas();
|
||||
const { mesas, loading: loadingMesas } = useMesas();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [selectedReserva, setSelectedReserva] = useState<Reserva | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const reservasPendentes = reservas.filter(r => r.estado === "Pendente");
|
||||
|
||||
const handleOpenAssign = (reserva: Reserva) => {
|
||||
setSelectedReserva(reserva);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleAssign = async (mesa: Mesa) => {
|
||||
if (selectedReserva) {
|
||||
let res;
|
||||
if (mesa.numero === 0) {
|
||||
// Confirm without table
|
||||
res = await updateReservaEstado(selectedReserva.id, "Confirmada");
|
||||
if (res.success) toast("Reserva confirmada sem mesa.", "info");
|
||||
} else {
|
||||
res = await confirmarComMesa(selectedReserva.id, mesa.id, mesa.numero);
|
||||
if (res.success) toast(`Reserva confirmada na Mesa ${mesa.numero}.`, "success");
|
||||
}
|
||||
|
||||
if (!res.success) toast("Erro ao atualizar reserva.", "error");
|
||||
|
||||
setIsModalOpen(false);
|
||||
setSelectedReserva(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRecusar = async (id: string) => {
|
||||
const res = await updateReservaEstado(id, "Recusada");
|
||||
if (res.success) {
|
||||
toast("Reserva recusada.", "info");
|
||||
} else {
|
||||
toast("Erro ao recusar reserva.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Lista de Espera</h1>
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-primary bg-primary/10 px-4 py-2 rounded-full border border-primary/20">
|
||||
<ListOrdered className="h-5 w-5" />
|
||||
<span>{reservasPendentes.length} Pendentes</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{loadingReservas || loadingMesas ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : reservasPendentes.length > 0 ? (
|
||||
reservasPendentes.map((reserva) => (
|
||||
<Card key={reserva.id} className="overflow-hidden border-border/50 hover:border-primary/30 transition-all duration-200">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col md:flex-row md:items-center p-6 gap-6">
|
||||
<div className="flex-1 space-y-1">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground mb-1">
|
||||
<Clock className="h-3 w-3" /> {reserva.hora}
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">{reserva.clienteEmail}</h3>
|
||||
<p className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<User className="h-4 w-4" /> {reserva.pessoas} pessoas
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Button
|
||||
onClick={() => handleOpenAssign(reserva)}
|
||||
className="bg-primary hover:bg-primary/90"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" /> Atribuir Mesa
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => handleRecusar(reserva.id)}
|
||||
variant="ghost"
|
||||
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
|
||||
>
|
||||
<X className="h-4 w-4 mr-2" /> Recusar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-24 text-center border-2 border-dashed rounded-xl bg-card/30">
|
||||
<ListOrdered className="h-16 w-16 text-muted-foreground/20 mb-4" />
|
||||
<h3 className="text-xl font-medium">A lista está limpa!</h3>
|
||||
<p className="text-muted-foreground max-w-xs mx-auto">
|
||||
Não há reservas pendentes de aprovação neste momento.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AssignTableModal
|
||||
isOpen={isModalOpen}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
reserva={selectedReserva}
|
||||
mesas={mesas}
|
||||
onAssign={handleAssign}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
150
reserva-mesa-dashboard/app/(dashboard)/mesas/page.tsx
Normal file
150
reserva-mesa-dashboard/app/(dashboard)/mesas/page.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { useMesas } from "@/hooks/useMesas";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Table as TableIcon, Users, Plus, Trash2 } from "lucide-react";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
export default function MesasPage() {
|
||||
const { mesas, loading, updateMesaEstado, addMesa, deleteMesa } = useMesas();
|
||||
const { toast } = useToast();
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [newMesa, setNewMesa] = useState({ numero: "", capacidade: "" });
|
||||
|
||||
const handleAddMesa = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const res = await addMesa(parseInt(newMesa.numero), parseInt(newMesa.capacidade));
|
||||
if (res.success) {
|
||||
setNewMesa({ numero: "", capacidade: "" });
|
||||
setShowAddForm(false);
|
||||
toast("Mesa adicionada com sucesso!", "success");
|
||||
} else {
|
||||
toast("Erro ao adicionar mesa.", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
const res = await deleteMesa(id);
|
||||
if (res.success) {
|
||||
toast("Mesa removida.", "success");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdateEstado = async (id: string, estado: any) => {
|
||||
const res = await updateMesaEstado(id, estado);
|
||||
if (res.success) {
|
||||
toast(`Estado da mesa atualizado para ${estado}`, "info");
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (estado: string) => {
|
||||
switch (estado) {
|
||||
case "Ocupada": return "bg-primary text-primary-foreground border-primary";
|
||||
case "Reservada": return "bg-amber-500 text-white border-amber-500";
|
||||
default: return "bg-muted text-muted-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Estado das Mesas</h1>
|
||||
<Button onClick={() => setShowAddForm(!showAddForm)} className="gap-2">
|
||||
<Plus className="h-4 w-4" /> Nova Mesa
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{showAddForm && (
|
||||
<Card className="border-primary/30 bg-primary/5">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Adicionar Nova Mesa</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleAddMesa} className="flex flex-wrap items-end gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="numero">Número da Mesa</Label>
|
||||
<Input
|
||||
id="numero"
|
||||
type="number"
|
||||
value={newMesa.numero}
|
||||
onChange={(e) => setNewMesa({...newMesa, numero: e.target.value})}
|
||||
required
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="capacidade">Capacidade (Lugares)</Label>
|
||||
<Input
|
||||
id="capacidade"
|
||||
type="number"
|
||||
value={newMesa.capacidade}
|
||||
onChange={(e) => setNewMesa({...newMesa, capacidade: e.target.value})}
|
||||
required
|
||||
className="w-32"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit">Criar Mesa</Button>
|
||||
<Button type="button" variant="ghost" onClick={() => setShowAddForm(false)}>Cancelar</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6">
|
||||
{mesas.map((mesa) => (
|
||||
<Card key={mesa.id} className="relative group overflow-hidden border-border/50 hover:border-primary/30 transition-all duration-200">
|
||||
<CardContent className="p-6 flex flex-col items-center text-center">
|
||||
<div className={`mb-4 flex h-16 w-16 items-center justify-center rounded-2xl border-2 font-display text-2xl font-bold transition-colors ${getStatusColor(mesa.estado)}`}>
|
||||
{mesa.numero}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="font-bold uppercase text-[10px] tracking-widest text-muted-foreground">Estado</p>
|
||||
<p className="text-sm font-medium">{mesa.estado}</p>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center gap-2 text-xs text-muted-foreground bg-muted px-2 py-1 rounded-md">
|
||||
<Users className="h-3 w-3" /> {mesa.capacidade} lugares
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex gap-2">
|
||||
<select
|
||||
className="text-xs bg-background border rounded px-1"
|
||||
value={mesa.estado}
|
||||
onChange={(e) => handleUpdateEstado(mesa.id, e.target.value as any)}
|
||||
>
|
||||
<option value="Livre">Livre</option>
|
||||
<option value="Ocupada">Ocupada</option>
|
||||
<option value="Reservada">Reservada</option>
|
||||
</select>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-6 w-6 text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleDelete(mesa.id)}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{mesas.length === 0 && !showAddForm && (
|
||||
<div className="col-span-full py-20 flex flex-col items-center justify-center text-center text-muted-foreground border-2 border-dashed rounded-xl">
|
||||
<TableIcon className="h-12 w-12 opacity-20 mb-4" />
|
||||
<p>Nenhuma mesa configurada.</p>
|
||||
<Button variant="link" onClick={() => setShowAddForm(true)}>Clique aqui para adicionar a primeira mesa</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
reserva-mesa-dashboard/app/(dashboard)/page.tsx
Normal file
182
reserva-mesa-dashboard/app/(dashboard)/page.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
"use client";
|
||||
|
||||
import { useAuth } from "@/hooks/useAuth";
|
||||
import { useReservas } from "@/hooks/useReservas";
|
||||
import { useMesas } from "@/hooks/useMesas";
|
||||
import { useStaff } from "@/hooks/useStaff";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
||||
import { OverviewChart } from "@/components/dashboard/OverviewChart";
|
||||
import { OccupancyPieChart } from "@/components/dashboard/OccupancyPieChart";
|
||||
import {
|
||||
Users,
|
||||
CalendarCheck,
|
||||
Clock,
|
||||
TrendingUp,
|
||||
UserCheck
|
||||
} from "lucide-react";
|
||||
|
||||
export default function DashboardHomePage() {
|
||||
const { user } = useAuth();
|
||||
const { reservas, loading: loadingReservas } = useReservas();
|
||||
const { mesas, loading: loadingMesas } = useMesas();
|
||||
const { staff } = useStaff();
|
||||
|
||||
// 1. Calculate top stats
|
||||
const todayStr = new Date().toISOString().split('T')[0];
|
||||
const todayReservas = reservas.filter(r => r.data === todayStr || r.estado.startsWith("Confirmada"));
|
||||
const activeReservas = todayReservas.filter(r => r.estado.startsWith("Confirmada")).length;
|
||||
const pendingReservas = todayReservas.filter(r => r.estado === "Pendente").length;
|
||||
|
||||
const totalMesas = mesas.length;
|
||||
const occupiedMesas = mesas.filter(m => m.estado === "Ocupada").length;
|
||||
const reservedMesas = mesas.filter(m => m.estado === "Reservada").length;
|
||||
const freeMesas = totalMesas - occupiedMesas - reservedMesas;
|
||||
const occupancyRate = totalMesas > 0 ? Math.round(((occupiedMesas + reservedMesas) / totalMesas) * 100) : 0;
|
||||
|
||||
const stats = [
|
||||
{ name: "Reservas Hoje", value: todayReservas.length.toString(), icon: CalendarCheck, trend: `+${pendingReservas} pendentes` },
|
||||
{ name: "Mesas Ocupadas", value: `${occupiedMesas} / ${totalMesas}`, icon: Clock, trend: `${freeMesas} livres` },
|
||||
{ name: "Staff Ativo", value: staff.length.toString(), icon: UserCheck, trend: "Equipa total" },
|
||||
{ name: "Ocupação", value: `${occupancyRate}%`, icon: TrendingUp, trend: "Tempo real" },
|
||||
];
|
||||
|
||||
// 2. Process data for Overview Chart (Last 7 days)
|
||||
const last7Days = Array.from({ length: 7 }, (_, i) => {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
return d.toISOString().split('T')[0];
|
||||
}).reverse();
|
||||
|
||||
const chartData = last7Days.map(date => {
|
||||
// Usar formato YYYY/MM/DD para compatibilidade total entre browsers
|
||||
const safeDate = date.replace(/-/g, '/');
|
||||
const dayLabel = new Date(safeDate).toLocaleDateString('pt-PT', { weekday: 'short' });
|
||||
const count = reservas.filter(r => r.data === date).length;
|
||||
return { name: dayLabel, total: count };
|
||||
});
|
||||
|
||||
// 3. Process data for Pie Chart
|
||||
const pieData = [
|
||||
{ name: "Livre", value: freeMesas, color: "#2A261E" },
|
||||
{ name: "Ocupada", value: occupiedMesas, color: "#D4891A" },
|
||||
{ name: "Reservada", value: reservedMesas, color: "#E8A832" },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">
|
||||
Bem-vindo, {user?.establishmentName || "Restaurante"}
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1 text-lg">
|
||||
Monitorize o desempenho do seu estabelecimento em tempo real.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
{stats.map((stat) => (
|
||||
<Card key={stat.name} className="overflow-hidden border-border/50 shadow-sm hover:shadow-md transition-shadow duration-200">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||
{stat.name}
|
||||
</CardTitle>
|
||||
<stat.icon className="h-5 w-5 text-primary opacity-80" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-3xl font-display font-bold">
|
||||
{loadingReservas || loadingMesas ? "..." : stat.value}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||
<span className="text-primary font-medium">{stat.trend}</span>
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<Card className="lg:col-span-2">
|
||||
<CardHeader>
|
||||
<CardTitle>Volume de Reservas</CardTitle>
|
||||
<CardDescription>Fluxo de clientes nos últimos 7 dias</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="pl-2">
|
||||
<OverviewChart data={chartData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Ocupação das Mesas</CardTitle>
|
||||
<CardDescription>Estado atual do restaurante</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<OccupancyPieChart data={pieData} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card className="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Últimas Reservas</CardTitle>
|
||||
<CardDescription>Atividade mais recente</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{reservas.length > 0 ? (
|
||||
<div className="space-y-4">
|
||||
{reservas.slice(0, 5).map((r) => (
|
||||
<div key={r.id} className="flex items-center justify-between border-b border-border/50 pb-3 last:border-0 last:pb-0">
|
||||
<div>
|
||||
<p className="font-medium">{r.clienteEmail}</p>
|
||||
<p className="text-xs text-muted-foreground">{r.data} às {r.hora} • {r.pessoas} pessoas</p>
|
||||
</div>
|
||||
<div className={`px-2 py-1 rounded-full text-[10px] font-bold uppercase ${
|
||||
r.estado.startsWith("Confirmada") ? "bg-green-500/10 text-green-500" :
|
||||
r.estado === "Pendente" ? "bg-amber-500/10 text-amber-500" :
|
||||
"bg-muted text-muted-foreground"
|
||||
}`}>
|
||||
{r.estado}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center text-muted-foreground">
|
||||
<p>Nenhuma atividade registada.</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="col-span-1">
|
||||
<CardHeader>
|
||||
<CardTitle>Mesas Críticas</CardTitle>
|
||||
<CardDescription>Mesas que requerem atenção</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{mesas.filter(m => m.estado !== "Livre").length > 0 ? (
|
||||
<div className="grid grid-cols-3 sm:grid-cols-4 gap-3">
|
||||
{mesas.filter(m => m.estado !== "Livre").map((m) => (
|
||||
<div key={m.id} className={`flex flex-col items-center justify-center p-3 rounded-lg border transition-colors ${
|
||||
m.estado === "Ocupada" ? "bg-primary/10 border-primary/30 text-primary shadow-sm" :
|
||||
"bg-amber-500/10 border-amber-500/30 text-amber-500"
|
||||
}`}>
|
||||
<span className="text-xs font-bold uppercase tracking-tighter">Mesa</span>
|
||||
<span className="text-xl font-display font-bold">{m.numero}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center py-10 text-center text-muted-foreground">
|
||||
<p>Todas as mesas estão livres.</p>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
137
reserva-mesa-dashboard/app/(dashboard)/reservas/page.tsx
Normal file
137
reserva-mesa-dashboard/app/(dashboard)/reservas/page.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
"use client";
|
||||
|
||||
import { useReservas } from "@/hooks/useReservas";
|
||||
import { useMesas } from "@/hooks/useMesas";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CalendarDays, Check, X, Clock, User } from "lucide-react";
|
||||
import { Reserva } from "@/types/reserva";
|
||||
import { Mesa } from "@/types/mesa";
|
||||
import { useToast } from "@/components/ui/toast";
|
||||
|
||||
export default function ReservasPage() {
|
||||
const { reservas, loading, updateReservaEstado, concluirReserva, confirmarComMesa } = useReservas();
|
||||
const { mesas } = useMesas();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleConcluir = async (reserva: Reserva) => {
|
||||
// Tentar encontrar a mesa se o estado for "Confirmada (Mesa X)"
|
||||
let mesaId: string | undefined;
|
||||
if (reserva.estado.includes("Mesa")) {
|
||||
const match = reserva.estado.match(/Mesa (\d+)/);
|
||||
if (match) {
|
||||
const num = parseInt(match[1]);
|
||||
const mesa = mesas.find(m => m.numero === num);
|
||||
mesaId = mesa?.id;
|
||||
}
|
||||
}
|
||||
const res = await concluirReserva(reserva.id, mesaId);
|
||||
if (res.success) {
|
||||
toast("Reserva concluída e mesa libertada.", "success");
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpdate = async (id: string, estado: any) => {
|
||||
const res = await updateReservaEstado(id, estado);
|
||||
if (res.success) {
|
||||
toast(`Reserva marcada como ${estado}`, "info");
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (estado: string) => {
|
||||
if (estado.startsWith("Confirmada")) return "bg-green-500/10 text-green-500 border-green-500/20";
|
||||
switch (estado) {
|
||||
case "Pendente": return "bg-amber-500/10 text-amber-500 border-amber-500/20";
|
||||
case "Recusada":
|
||||
case "Cancelada": return "bg-destructive/10 text-destructive border-destructive/20";
|
||||
default: return "bg-muted text-muted-foreground border-border";
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-display font-bold text-foreground">Gestão de Reservas</h1>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground bg-card px-3 py-1.5 rounded-full border border-border">
|
||||
<CalendarDays className="h-4 w-4 text-primary" />
|
||||
<span>{new Date().toLocaleDateString('pt-PT')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
) : reservas.length > 0 ? (
|
||||
reservas.map((reserva) => (
|
||||
<Card key={reserva.id} className="overflow-hidden border-border/50 hover:border-primary/30 transition-all duration-200 shadow-sm">
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col md:flex-row md:items-center p-6 gap-6">
|
||||
<div className="flex-1 space-y-2">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`px-2.5 py-0.5 rounded-full text-[11px] font-bold uppercase border ${getStatusColor(reserva.estado)}`}>
|
||||
{reserva.estado}
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" /> {reserva.data} às {reserva.hora}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="text-xl font-bold">{reserva.clienteEmail}</h3>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" /> {reserva.pessoas} pessoas
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{reserva.estado === "Pendente" && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => handleUpdate(reserva.id, "Confirmada")}
|
||||
className="bg-green-600 hover:bg-green-700 text-white"
|
||||
size="sm"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" /> Aceitar
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:bg-destructive/10"
|
||||
onClick={() => handleUpdate(reserva.id, "Recusada")}
|
||||
>
|
||||
<X className="h-4 w-4 mr-1" /> Recusar
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{reserva.estado.startsWith("Confirmada") && (
|
||||
<Button
|
||||
onClick={() => handleConcluir(reserva)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="hover:bg-primary/10 hover:text-primary hover:border-primary/30"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" /> Marcar como Concluída
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card className="border-dashed border-2">
|
||||
<CardContent className="flex flex-col items-center justify-center py-20 text-center">
|
||||
<CalendarDays className="h-16 w-16 text-muted-foreground/20 mb-4" />
|
||||
<h3 className="text-xl font-medium">Sem reservas registadas</h3>
|
||||
<p className="text-muted-foreground max-w-xs mx-auto">
|
||||
As reservas feitas pelos clientes através da App Android aparecerão aqui em tempo real.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user