mudanças
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user