mudanças

This commit is contained in:
2026-03-11 12:35:44 +00:00
parent e89211cd33
commit 06491423a6
4 changed files with 96 additions and 121 deletions

View File

@@ -14,9 +14,18 @@ export const CartPanel = () => {
return acc;
}, {});
const handleCheckout = (shopId: string) => {
const handleCheckout = async (shopId: string) => {
if (!user) return;
placeOrder(user.id, shopId);
try {
const order = await placeOrder(user.id, shopId);
if (order) {
alert("O seu pedido foi enviado à Barbearia com sucesso! Irá ser notificado brevemente.");
} else {
alert("Desculpe, ocorreu um erro a processar a sua encomenda.");
}
} catch (e) {
alert("Falha técnica no processo de Checkout.");
}
};
return (

View File

@@ -30,9 +30,9 @@ type AppContextValue = State & {
removeFromCart: (refId: string) => void;
clearCart: () => void;
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
placeOrder: (customerId: string, shopId?: string) => Order | null;
placeOrder: (customerId: string, shopId?: string) => Promise<Order | null>;
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<void>;
updateOrderStatus: (id: string, status: Order['status']) => void;
updateOrderStatus: (id: string, status: Order['status']) => Promise<void>;
addService: (shopId: string, service: Omit<Service, 'id'>) => Promise<void>;
updateService: (shopId: string, service: Service) => Promise<void>;
deleteService: (shopId: string, serviceId: string) => Promise<void>;
@@ -97,6 +97,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const { data: barbersData } = await supabase.from('barbers').select('*');
const { data: productsData } = await supabase.from('products').select('*');
const { data: appointmentsData } = await supabase.from('appointments').select('*');
const { data: ordersData } = await supabase.from('orders').select('*');
const fetchedShops: BarberShop[] = shopsData.map((shop) => ({
id: shop.id,
@@ -142,6 +143,16 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
total: a.total,
}));
const formattedOrders: Order[] = (ordersData ?? []).map((o) => ({
id: o.id,
shopId: o.shop_id,
customerId: o.customer_id,
items: o.items,
total: o.total,
status: o.status as Order['status'],
createdAt: o.created_at,
}));
setState((s) => {
// A BD é agora a única fonte de verdade.
// Como o CRUD já insere na BD antes do refresh, a sobreposição com 'localVersion' foi
@@ -165,7 +176,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return true;
});
return { ...s, shops: dedupedShops, appointments: formattedAppointments, shopsReady: true };
return { ...s, shops: dedupedShops, appointments: formattedAppointments, orders: formattedOrders, shopsReady: true };
});
} catch (err) {
console.error('refreshShops error:', err);
@@ -402,7 +413,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
};
};
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
const placeOrder: AppContextValue['placeOrder'] = async (customerId, onlyShopId) => {
if (!state.cart.length) return null;
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
acc[item.shopId] = acc[item.shopId] || [];
@@ -412,7 +423,7 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
const newOrders: Order[] = entries.map(([shopId, items]) => {
const newOrders = await Promise.all(entries.map(async ([shopId, items]) => {
const total = items.reduce((sum, item) => {
const shop = state.shops.find((s) => s.id === item.shopId);
if (!shop) return sum;
@@ -423,11 +434,41 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
return sum + price * item.qty;
}, 0);
return { id: nanoid(), shopId, customerId, items, total, status: 'pendente', createdAt: new Date().toISOString() };
});
const dbOrder = {
shop_id: shopId,
customer_id: customerId,
items: items,
total: total,
status: 'pendente'
};
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
return newOrders[0] ?? null;
const { data, error } = await supabase.from('orders').insert([dbOrder]).select().single();
if (error || !data) {
console.error("Erro ao criar encomenda na BD:", error);
return null;
}
return {
id: data.id,
shopId: data.shop_id,
customerId: data.customer_id,
items: data.items,
total: data.total,
status: data.status,
createdAt: data.created_at
} as Order;
}));
const successfulOrders = newOrders.filter((o): o is Order => o !== null);
if (successfulOrders.length > 0) {
await refreshShops(); // Refresh app state to include new orders
setState((s) => ({ ...s, cart: [] }));
return successfulOrders[0];
}
return null;
};
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = async (id, status) => {
@@ -439,8 +480,13 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
await refreshShops();
};
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
setState((s) => ({ ...s, orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)) }));
const updateOrderStatus: AppContextValue['updateOrderStatus'] = async (id, status) => {
const { error } = await supabase.from('orders').update({ status }).eq('id', id);
if (error) {
console.error("Erro ao atualizar status da encomenda:", error);
return;
}
await refreshShops();
};
const addService: AppContextValue['addService'] = async (shopId, service) => {

View File

@@ -50,6 +50,15 @@ import {
CheckCircle2,
} from 'lucide-react';
type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings';
interface SidebarTab {
id: TabId;
label: string;
icon: React.FC<any>;
badge?: number;
}
const periods: Record<string, (date: Date) => boolean> = {
hoje: (d) => {
const now = new Date();
@@ -69,7 +78,7 @@ const periods: Record<string, (date: Date) => boolean> = {
const parseDate = (value: string) => new Date(value.replace(' ', 'T'));
type TabId = 'overview' | 'appointments' | 'history' | 'orders' | 'services' | 'products' | 'barbers' | 'settings';
export default function Dashboard() {
const { user, shops, shopsReady } = useApp();
@@ -148,12 +157,16 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
// Pedidos apenas com produtos (não serviços)
const shopOrders = orders.filter(
(o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt)) && o.items.some((item) => item.type === 'product')
(o) => o.shopId === shop.id && periodMatch(new Date(o.createdAt))
);
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const pendingOrders = orders.filter((o) => o.shopId === shop.id && o.status === 'pendente').length;
const pendingAppts = shopAppointments.filter((a) => a.status === 'pendente').length;
const confirmedAppts = shopAppointments.filter((a) => a.status === 'confirmado').length;
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
const lowStock = shop.products.filter((p) => p.stock <= 3);
const comparisonData = useMemo(() => {
@@ -292,15 +305,15 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
}
};
const tabs = [
{ id: 'overview' as TabId, label: 'Visão Geral', icon: BarChart3 },
{ id: 'appointments' as TabId, label: 'Agendamentos', icon: Calendar },
{ id: 'history' as TabId, label: 'Histórico', icon: History },
{ id: 'orders' as TabId, label: 'Pedidos', icon: ShoppingBag },
{ id: 'services' as TabId, label: 'Serviços', icon: Scissors },
{ id: 'products' as TabId, label: 'Produtos', icon: Package },
{ id: 'barbers' as TabId, label: 'Barbeiros', icon: Users },
{ id: 'settings' as TabId, label: 'Definições', icon: Settings },
const sidebarTabs: SidebarTab[] = [
{ id: 'overview', label: 'Visão Geral', icon: BarChart3 },
{ id: 'appointments', label: 'Agendamentos', icon: Calendar, badge: pendingAppts > 0 ? pendingAppts : undefined },
{ id: 'history', label: 'Histórico', icon: History },
{ id: 'orders', label: 'Pedidos', icon: ShoppingBag, badge: pendingOrders > 0 ? pendingOrders : undefined },
{ id: 'services', label: 'Serviços', icon: Scissors },
{ id: 'products', label: 'Produtos', icon: Package },
{ id: 'barbers', label: 'Barbeiros', icon: Users },
{ id: 'settings', label: 'Definições', icon: Settings },
];
return (
@@ -328,7 +341,7 @@ function DashboardInner({ shop }: { shop: BarberShop }) {
</div>
{/* Tabs */}
<Tabs tabs={tabs.map((t) => ({ id: t.id, label: t.label }))} active={activeTab} onChange={(v) => setActiveTab(v as TabId)} />
<Tabs tabs={sidebarTabs.map((t) => ({ id: t.id, label: t.label, badge: t.badge }))} active={activeTab} onChange={(v) => setActiveTab(v as TabId)} />
{/* Tab Content */}
{activeTab === 'overview' && (

View File

@@ -10,7 +10,6 @@ import { Badge } from '../components/ui/badge'
import { currency } from '../lib/format'
import { useApp } from '../context/AppContext'
import { Calendar, ShoppingBag, User, Clock } from 'lucide-react'
import { listEvents, type EventRow } from '../lib/events'
import { supabase } from '../lib/supabase'
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
@@ -37,23 +36,11 @@ export default function Profile() {
const [authId, setAuthId] = useState<string>('')
const [loadingAuth, setLoadingAuth] = useState(true)
// ✅ Eventos Supabase
const [events, setEvents] = useState<EventRow[]>([])
const [loadingEvents, setLoadingEvents] = useState(true)
const [eventsError, setEventsError] = useState('')
/**
* Obtém de forma segura o objeto de "getUser" fornecido pelo Supabase Auth.
* Não despoleta "flash" de erros se o utilizador não tiver credenciais, mas
* em caso positivo armazena Auth Id e Email globalmente na View.
*/
useEffect(() => {
let mounted = true
; (async () => {
const { data, error } = await supabase.auth.getUser()
if (!mounted) return
if (error || !data.user) {
setAuthEmail('')
setAuthId('')
@@ -61,42 +48,11 @@ export default function Profile() {
setAuthEmail(data.user.email ?? '')
setAuthId(data.user.id)
}
setLoadingAuth(false)
})()
return () => {
mounted = false
}
return () => { mounted = false }
}, [])
async function loadEvents() {
try {
setLoadingEvents(true)
setEventsError('')
const data = await listEvents()
setEvents(data)
} catch (e: any) {
setEventsError(e?.message || 'Erro ao carregar eventos')
} finally {
setLoadingEvents(false)
}
}
// Carrega eventos ao entrar no perfil
useEffect(() => {
loadEvents()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
// ✅ Recarrega eventos sempre que voltares para /perfil
useEffect(() => {
if (location.pathname === '/perfil') {
loadEvents()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname])
const myAppointments = useMemo(() => {
if (!authId) return []
return appointments.filter((a) => a.customerId === authId)
@@ -144,61 +100,12 @@ export default function Profile() {
<h1 className="text-2xl font-bold text-slate-900">Olá, {displayName}</h1>
<p className="text-sm text-slate-600">{authEmail}</p>
<Badge color="amber" variant="soft" className="mt-2">
Utilizador
Cliente Barber
</Badge>
</div>
</div>
</Card>
{/* ✅ Supabase Events Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">
<Calendar size={20} className="text-amber-600" />
<h2 className="text-xl font-bold text-slate-900">Eventos (Supabase)</h2>
<Badge color="slate" variant="soft">
{loadingEvents ? '…' : events.length}
</Badge>
<Link
to="/eventos/novo"
className="ml-auto text-sm font-semibold text-amber-700 hover:text-amber-800 transition-colors"
>
+ Criar evento
</Link>
</div>
{loadingEvents ? (
<Card className="p-8 text-center">
<p className="text-slate-600 font-medium">A carregar eventos</p>
</Card>
) : eventsError ? (
<Card className="p-8 text-center">
<p className="text-rose-600 font-medium">{eventsError}</p>
</Card>
) : events.length === 0 ? (
<Card className="p-8 text-center">
<Calendar size={48} className="mx-auto text-slate-300 mb-3" />
<p className="text-slate-600 font-medium">Nenhum evento ainda</p>
<p className="text-sm text-slate-500 mt-1">Cria um evento para aparecer aqui.</p>
</Card>
) : (
<div className="space-y-3">
{events.map((ev) => (
<Card key={ev.id} hover className="p-5">
<div className="flex items-start justify-between gap-4">
<div className="flex-1 space-y-2">
<h3 className="font-bold text-slate-900">{ev.title}</h3>
{ev.description && <p className="text-sm text-slate-600">{ev.description}</p>}
<p className="text-xs text-slate-500">{new Date(ev.date).toLocaleString()}</p>
</div>
</div>
</Card>
))}
</div>
)}
</section>
{/* Appointments Section */}
<section className="space-y-4">
<div className="flex items-center gap-2">