feat: atualizar tema para indigo, corrigir textos e implementar AppContext
This commit is contained in:
@@ -3,7 +3,7 @@ import { View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
color?: 'amber' | 'slate' | 'green' | 'red' | 'blue';
|
||||
color?: 'amber' | 'slate' | 'green' | 'red' | 'blue' | 'indigo';
|
||||
variant?: 'solid' | 'soft';
|
||||
style?: ViewStyle;
|
||||
};
|
||||
@@ -15,6 +15,7 @@ const colorMap = {
|
||||
green: { bg: '#10b981', text: '#fff' },
|
||||
red: { bg: '#ef4444', text: '#fff' },
|
||||
blue: { bg: '#3b82f6', text: '#fff' },
|
||||
indigo: { bg: '#6366f1', text: '#fff' },
|
||||
},
|
||||
soft: {
|
||||
amber: { bg: '#fef3c7', text: '#92400e' },
|
||||
@@ -22,10 +23,11 @@ const colorMap = {
|
||||
green: { bg: '#d1fae5', text: '#065f46' },
|
||||
red: { bg: '#fee2e2', text: '#991b1b' },
|
||||
blue: { bg: '#dbeafe', text: '#1e40af' },
|
||||
indigo: { bg: '#e0e7ff', text: '#4338ca' },
|
||||
},
|
||||
};
|
||||
|
||||
export const Badge = ({ children, color = 'amber', variant = 'soft', style }: Props) => {
|
||||
export const Badge = ({ children, color = 'indigo', variant = 'soft', style }: Props) => {
|
||||
const colors = colorMap[variant][color];
|
||||
|
||||
return (
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Button = ({ children, onPress, variant = 'solid', size = 'md', disa
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#f59e0b'} />
|
||||
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#6366f1'} />
|
||||
) : (
|
||||
<Text style={textStyles}>{children}</Text>
|
||||
)}
|
||||
@@ -52,12 +52,12 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
},
|
||||
solid: {
|
||||
backgroundColor: '#f59e0b',
|
||||
backgroundColor: '#6366f1',
|
||||
},
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderColor: '#f59e0b',
|
||||
borderColor: '#6366f1',
|
||||
},
|
||||
ghost: {
|
||||
backgroundColor: 'transparent',
|
||||
@@ -84,10 +84,10 @@ const styles = StyleSheet.create({
|
||||
color: '#fff',
|
||||
},
|
||||
text_outline: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
text_ghost: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
textSize_sm: {
|
||||
fontSize: 12,
|
||||
|
||||
@@ -7,23 +7,34 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { supabase } from '../lib/supabase';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
// Tipo interno determinando as propriedades globais partilhadas (Estados e Funções)
|
||||
type State = {
|
||||
user?: User;
|
||||
shops: BarberShop[];
|
||||
cart: CartItem[];
|
||||
appointments: Appointment[];
|
||||
orders: Order[];
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => boolean;
|
||||
logout: () => void;
|
||||
register: (payload: any) => boolean;
|
||||
addToCart: (item: CartItem) => void;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Promise<Appointment | null>;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => Promise<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>;
|
||||
addProduct: (shopId: string, product: Omit<Product, 'id'>) => Promise<void>;
|
||||
updateProduct: (shopId: string, product: Product) => Promise<void>;
|
||||
deleteProduct: (shopId: string, productId: string) => Promise<void>;
|
||||
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => Promise<void>;
|
||||
updateBarber: (shopId: string, barber: Barber) => Promise<void>;
|
||||
deleteBarber: (shopId: string, barberId: string) => Promise<void>;
|
||||
refreshShops: () => Promise<void>;
|
||||
};
|
||||
|
||||
@@ -32,135 +43,96 @@ const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [shops, setShops] = useState<BarberShop[]>([]);
|
||||
const [appointments, setAppointments] = useState<Appointment[]>([]);
|
||||
const [orders, setOrders] = useState<Order[]>([]);
|
||||
const [cart, setCart] = useState<CartItem[]>([]);
|
||||
const [user, setUser] = useState<User | undefined>(undefined);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
/**
|
||||
* Hook executado no carregamento (mount) inicial.
|
||||
* Valida através de `supabase.auth.getUser()` se existe um token de sessão válido
|
||||
* (identificando o utilizador sem necessidade de o cliente refazer login ativamente).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const loadUser = async () => {
|
||||
// Pedido restrito à API de autenticação do Supabase
|
||||
const { data } = await supabase.auth.getUser();
|
||||
if (data.user) {
|
||||
let shopId: string | undefined = undefined;
|
||||
|
||||
// Vai buscar o shop_id mapeado na tabela profiles
|
||||
const { data: prof } = await supabase
|
||||
.from('profiles')
|
||||
.select('shop_id')
|
||||
.select('shop_id, role, name')
|
||||
.eq('id', data.user.id)
|
||||
.single();
|
||||
|
||||
shopId = prof?.shop_id || undefined;
|
||||
|
||||
setUser({
|
||||
id: data.user.id,
|
||||
name: prof?.name || data.user.email?.split('@')[0] || 'Utilizador',
|
||||
email: data.user.email || '',
|
||||
role: 'barbearia', // assumido estaticamente na V1, deve vir de profiles
|
||||
shopId
|
||||
role: (prof?.role as any) || 'cliente',
|
||||
shopId: prof?.shop_id || undefined,
|
||||
} as User);
|
||||
}
|
||||
};
|
||||
loadUser();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Consulta mestra (Query) - Refresca todo o ecossistema de dados das barbearias.
|
||||
* Faz 2 queries (`supabase.from('shops').select('*')` e `services`) e depois
|
||||
* executa um JOIN manual via Javascript para injetar os serviços dentro
|
||||
* dos objetos da barbearia respetiva.
|
||||
*/
|
||||
const refreshShops = async () => {
|
||||
console.log("A buscar shops...");
|
||||
try {
|
||||
const { data: shopsData } = await supabase.from('shops').select('*');
|
||||
const { data: servicesData } = await supabase.from('services').select('*');
|
||||
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('*');
|
||||
|
||||
// Query 1: Obtém a listagem completa (tabela 'shops')
|
||||
const { data: shopsData, error: shopsError } = await supabase
|
||||
.from('shops')
|
||||
.select('*');
|
||||
if (shopsData) {
|
||||
const merged: BarberShop[] = shopsData.map((shop: any) => ({
|
||||
...shop,
|
||||
services: (servicesData || []).filter((s: any) => s.shop_id === shop.id).map((s: any) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
price: s.price,
|
||||
duration: s.duration,
|
||||
barberIds: s.barber_ids || [],
|
||||
})),
|
||||
products: (productsData || []).filter((p: any) => p.shop_id === shop.id),
|
||||
barbers: (barbersData || []).filter((b: any) => b.shop_id === shop.id).map((b: any) => ({
|
||||
id: b.id,
|
||||
name: b.name,
|
||||
specialties: b.specialties || [],
|
||||
schedule: b.schedule || [],
|
||||
})),
|
||||
}));
|
||||
setShops(merged);
|
||||
}
|
||||
|
||||
if (shopsError) {
|
||||
console.error("Erro ao buscar shops:", shopsError);
|
||||
return;
|
||||
if (appointmentsData) {
|
||||
setAppointments(
|
||||
appointmentsData.map((a: any) => ({
|
||||
id: a.id,
|
||||
shopId: a.shop_id,
|
||||
serviceId: a.service_id,
|
||||
barberId: a.barber_id,
|
||||
customerId: a.customer_id,
|
||||
date: a.date,
|
||||
status: a.status as Appointment['status'],
|
||||
total: a.total,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
if (ordersData) {
|
||||
setOrders(
|
||||
ordersData.map((o: any) => ({
|
||||
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,
|
||||
}))
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error refreshing shops:', err);
|
||||
}
|
||||
|
||||
// Query 2: Obtém a listagem associada globalmente (tabela 'services')
|
||||
const { data: servicesData, error: servicesError } = await supabase
|
||||
.from('services')
|
||||
.select('*');
|
||||
|
||||
if (servicesError) {
|
||||
console.error("Erro ao buscar services:", servicesError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 3: Obtém a listagem global de Barbeiros (tabela 'barbers')
|
||||
const { data: barbersData, error: barbersError } = await supabase
|
||||
.from('barbers')
|
||||
.select('*');
|
||||
|
||||
if (barbersError) {
|
||||
console.error("Erro ao buscar barbers:", barbersError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 4: Obtém a listagem global de Produtos (tabela 'products')
|
||||
const { data: productsData, error: productsError } = await supabase
|
||||
.from('products')
|
||||
.select('*');
|
||||
|
||||
if (productsError) {
|
||||
console.error("Erro ao buscar products:", productsError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Query 5: Obtém a listagem global de Marcações (tabela 'appointments')
|
||||
const { data: appointmentsData, error: appointmentsError } = await supabase
|
||||
.from('appointments')
|
||||
.select('*');
|
||||
|
||||
if (appointmentsError) {
|
||||
console.error("Erro ao buscar appointments:", appointmentsError);
|
||||
return;
|
||||
}
|
||||
|
||||
if (appointmentsData) {
|
||||
setAppointments(
|
||||
appointmentsData.map((a: any) => ({
|
||||
id: a.id,
|
||||
shopId: a.shop_id,
|
||||
serviceId: a.service_id,
|
||||
barberId: a.barber_id,
|
||||
customerId: a.customer_id,
|
||||
date: a.date,
|
||||
status: a.status as Appointment['status'],
|
||||
total: a.total,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
// Associar serviços, barbeiros e produtos às respetivas shops, simulando um INNER JOIN nativo do SQL
|
||||
const shopsWithServices = shopsData.map((shop) => ({
|
||||
...shop,
|
||||
// Relaciona a 'foreign key' (shop_id) com o resgistro primário (shop.id)
|
||||
services: servicesData.filter((s) => s.shop_id === shop.id),
|
||||
products: productsData.filter((p) => p.shop_id === shop.id),
|
||||
barbers: barbersData.filter((b) => b.shop_id === shop.id),
|
||||
}));
|
||||
|
||||
console.log("Shops carregadas:", shopsWithServices);
|
||||
|
||||
setShops(shopsWithServices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook de Inicialização Master.
|
||||
* Aciona a função de preenchimento do Contexto assincronamente e liberta
|
||||
* a interface UI da view de Loading (`setLoading(false)`).
|
||||
*/
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await refreshShops();
|
||||
@@ -169,150 +141,136 @@ export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
init();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* Encerra a sessão JWT ativa com o Supabase Auth.
|
||||
* Limpa integralmente a interface do User local (estado React vazio).
|
||||
*/
|
||||
const login = (email: string, password: string) => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const logout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
setUser(undefined);
|
||||
};
|
||||
|
||||
// Funções elementares do fluxo transacional não persistido (Estado do Carrinho transitório/local)
|
||||
const register = (payload: any) => {
|
||||
const id = nanoid();
|
||||
const newUser: User = { ...payload, id };
|
||||
setUser(newUser);
|
||||
return true;
|
||||
};
|
||||
|
||||
const addToCart = (item: CartItem) => {
|
||||
setCart((prev) => [...prev, item]);
|
||||
setCart((prev: CartItem[]) => [...prev, item]);
|
||||
};
|
||||
|
||||
const clearCart = () => setCart([]);
|
||||
|
||||
// 🔹 CRUD SERVICES (SUPABASE REAL)
|
||||
|
||||
/**
|
||||
* Executa um INSERT na BD (via API REST gerada) protegendo interações com a tabela estrita 'services'.
|
||||
* @param {string} shopId - A foreign key relacionando o estabelecimento.
|
||||
* @param {Omit<Service, 'id'>} service - O DTO (Data Transfer Object) sem a primary key autonumerável.
|
||||
*/
|
||||
const addService = async (shopId: string, service: Omit<Service, 'id'>) => {
|
||||
// Insere os campos exatos formatados estritamente na query Supabase
|
||||
const { error } = await supabase.from('services').insert([
|
||||
{
|
||||
shop_id: shopId,
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
},
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao adicionar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Para manter integridade reativa pura, força refetch dos dados pós-mutação
|
||||
await supabase.from('services').insert([{ shop_id: shopId, ...service }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executa um UPDATE num tuplo específico filtrando analiticamente pela primary key `(eq('id', service.id))`.
|
||||
*/
|
||||
const updateService = async (shopId: string, service: Service) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.update({
|
||||
name: service.name,
|
||||
price: service.price,
|
||||
duration: service.duration,
|
||||
})
|
||||
.eq('id', service.id); // Identificador vital do update
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao atualizar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
const { id, ...data } = service;
|
||||
await supabase.from('services').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
/**
|
||||
* Executa uma instrução SQL DELETE remota rígida, baseada no ID unívoco do tuplo.
|
||||
*/
|
||||
const deleteService = async (shopId: string, serviceId: string) => {
|
||||
const { error } = await supabase
|
||||
.from('services')
|
||||
.delete()
|
||||
.eq('id', serviceId);
|
||||
|
||||
if (error) {
|
||||
console.error("Erro ao apagar serviço:", error);
|
||||
return;
|
||||
}
|
||||
|
||||
await supabase.from('services').delete().eq('id', serviceId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const createAppointment: AppContextValue['createAppointment'] = async (input) => {
|
||||
const shop = shops.find((s) => s.id === input.shopId);
|
||||
if (!shop) return null;
|
||||
const svc = shop.services.find((s) => s.id === input.serviceId);
|
||||
if (!svc) return null;
|
||||
|
||||
const { data: newRow, error } = await supabase.from('appointments').insert([
|
||||
{
|
||||
shop_id: input.shopId,
|
||||
service_id: input.serviceId,
|
||||
barber_id: input.barberId,
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total: svc.price,
|
||||
}
|
||||
]).select().single();
|
||||
|
||||
if (error || !newRow) {
|
||||
console.error("Erro ao criar marcação na BD:", error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const addProduct = async (shopId: string, product: Omit<Product, 'id'>) => {
|
||||
await supabase.from('products').insert([{ shop_id: shopId, ...product }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateProduct = async (shopId: string, product: Product) => {
|
||||
const { id, ...data } = product;
|
||||
await supabase.from('products').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const deleteProduct = async (shopId: string, productId: string) => {
|
||||
await supabase.from('products').delete().eq('id', productId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const addBarber = async (shopId: string, barber: Omit<Barber, 'id'>) => {
|
||||
await supabase.from('barbers').insert([{ shop_id: shopId, ...barber }]);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateBarber = async (shopId: string, barber: Barber) => {
|
||||
const { id, ...data } = barber;
|
||||
await supabase.from('barbers').update(data).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const deleteBarber = async (shopId: string, barberId: string) => {
|
||||
await supabase.from('barbers').delete().eq('id', barberId);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const createAppointment = async (input: Omit<Appointment, 'id' | 'status' | 'total'>) => {
|
||||
const svc = shops.flatMap(s => s.services).find(s => s.id === input.serviceId);
|
||||
const total = svc ? svc.price : 0;
|
||||
const { data } = await supabase.from('appointments').insert([{
|
||||
shop_id: input.shopId,
|
||||
service_id: input.serviceId,
|
||||
barber_id: input.barberId,
|
||||
customer_id: input.customerId,
|
||||
date: input.date,
|
||||
status: 'pendente',
|
||||
total
|
||||
}]).select().single();
|
||||
await refreshShops();
|
||||
return data as any as Appointment;
|
||||
};
|
||||
|
||||
const updateAppointmentStatus = async (id: string, status: Appointment['status']) => {
|
||||
await supabase.from('appointments').update({ status }).eq('id', id);
|
||||
await refreshShops();
|
||||
};
|
||||
|
||||
const updateOrderStatus = async (id: string, status: Order['status']) => {
|
||||
await supabase.from('orders').update({ status }).eq('id', id);
|
||||
await refreshShops();
|
||||
|
||||
return {
|
||||
id: newRow.id,
|
||||
shopId: newRow.shop_id,
|
||||
serviceId: newRow.service_id,
|
||||
barberId: newRow.barber_id,
|
||||
customerId: newRow.customer_id,
|
||||
date: newRow.date,
|
||||
status: newRow.status as Appointment['status'],
|
||||
total: newRow.total,
|
||||
};
|
||||
};
|
||||
|
||||
// Empacotamento em objeto estabilizado memoizado face renderizações espúrias (React Context Pattern)
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
shops,
|
||||
cart,
|
||||
appointments,
|
||||
orders,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
refreshShops,
|
||||
}),
|
||||
[user, shops, cart, appointments]
|
||||
[user, shops, cart, appointments, orders]
|
||||
);
|
||||
|
||||
// Loading Shield evita quebra generalizada se o app renderizar sem BD disponível
|
||||
if (loading) return null;
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
// Hook prático de acesso central sem import múltiplo do 'useContext' em toda aplicação
|
||||
export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
|
||||
7
src/lib/supabase.ts
Normal file
7
src/lib/supabase.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const supabaseUrl = 'https://jqklhhpyykzrktikjnmb.supabase.co'
|
||||
const supabaseAnonKey =
|
||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Impxa2xoaHB5eWt6cmt0aWtqbm1iIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjgzODQ0MDgsImV4cCI6MjA4Mzk2MDQwOH0.QsPuBnyUtRPSavlqKj3IGR9c8juT02LY_hSi-j3c6M0'
|
||||
|
||||
export const supabase = createClient(supabaseUrl, supabaseAnonKey)
|
||||
@@ -22,7 +22,7 @@ export default function AppNavigator() {
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: '#f59e0b' },
|
||||
headerStyle: { backgroundColor: '#6366f1' },
|
||||
headerTintColor: '#fff',
|
||||
headerTitleStyle: { fontWeight: 'bold' },
|
||||
}}
|
||||
|
||||
@@ -161,7 +161,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: 14,
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ export default function AuthRegister() {
|
||||
label="Nome da barbearia"
|
||||
value={shopName}
|
||||
onChangeText={setShopName}
|
||||
placeholder="Barbearia XPTO"
|
||||
placeholder="Ex: Minha Barbearia"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -170,8 +170,8 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
roleButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#fef3c7',
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: '#c7d2fe',
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 14,
|
||||
@@ -179,7 +179,7 @@ const styles = StyleSheet.create({
|
||||
color: '#64748b',
|
||||
},
|
||||
roleTextActive: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
@@ -199,7 +199,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: 14,
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -248,8 +248,8 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 8,
|
||||
},
|
||||
serviceButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#fef3c7',
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: '#e0e7ff',
|
||||
},
|
||||
serviceText: {
|
||||
fontSize: 14,
|
||||
@@ -258,7 +258,7 @@ const styles = StyleSheet.create({
|
||||
marginBottom: 4,
|
||||
},
|
||||
serviceTextActive: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
servicePrice: {
|
||||
fontSize: 12,
|
||||
@@ -278,8 +278,8 @@ const styles = StyleSheet.create({
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
barberButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#f59e0b',
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: '#6366f1',
|
||||
},
|
||||
barberText: {
|
||||
fontSize: 14,
|
||||
@@ -303,8 +303,8 @@ const styles = StyleSheet.create({
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
slotButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#f59e0b',
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: '#6366f1',
|
||||
},
|
||||
slotText: {
|
||||
fontSize: 14,
|
||||
@@ -339,7 +339,7 @@ const styles = StyleSheet.create({
|
||||
summaryTotal: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
marginTop: 8,
|
||||
},
|
||||
submitButton: {
|
||||
|
||||
@@ -181,7 +181,7 @@ const styles = StyleSheet.create({
|
||||
total: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
|
||||
@@ -126,7 +126,7 @@ export default function Dashboard() {
|
||||
* Atualiza a quantidade de inventário de um produto iterando a variação (+1/-1).
|
||||
* Desencadeia um update para a tabela products (ex: `supabase.from('products').update(...)`) evitando stocks negativos.
|
||||
* @param {string} productId - O identificador único do produto afetado.
|
||||
* @param {number} delta - A quantidade exata matemática a variar da realidade material.
|
||||
* @param {number} delta - A quantidade exata matemática a variar do inventário.
|
||||
*/
|
||||
const updateProductStock = (productId: string, delta: number) => {
|
||||
const product = shop.products.find((p) => p.id === productId);
|
||||
@@ -206,7 +206,7 @@ export default function Dashboard() {
|
||||
<Text style={styles.itemName}>{svc?.name ?? 'Serviço'}</Text>
|
||||
<Text style={styles.itemDesc}>{barber?.name} · {a.date}</Text>
|
||||
</View>
|
||||
<Badge color={a.status === 'pendente' ? 'amber' : a.status === 'confirmado' ? 'green' : 'red'}>
|
||||
<Badge color={a.status === 'pendente' ? 'indigo' : a.status === 'confirmado' ? 'green' : 'red'}>
|
||||
{a.status}
|
||||
</Badge>
|
||||
</View>
|
||||
@@ -247,7 +247,7 @@ export default function Dashboard() {
|
||||
<Text style={styles.itemName}>{currency(o.total)}</Text>
|
||||
<Text style={styles.itemDesc}>{new Date(o.createdAt).toLocaleString('pt-BR')}</Text>
|
||||
</View>
|
||||
<Badge color={o.status === 'pendente' ? 'amber' : o.status === 'confirmado' ? 'green' : 'red'}>
|
||||
<Badge color={o.status === 'pendente' ? 'indigo' : o.status === 'confirmado' ? 'green' : 'red'}>
|
||||
{o.status}
|
||||
</Badge>
|
||||
</View>
|
||||
@@ -438,7 +438,7 @@ const styles = StyleSheet.create({
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
tabActive: {
|
||||
borderBottomColor: '#f59e0b',
|
||||
borderBottomColor: '#6366f1',
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
@@ -446,7 +446,7 @@ const styles = StyleSheet.create({
|
||||
color: '#64748b',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
@@ -476,15 +476,15 @@ const styles = StyleSheet.create({
|
||||
color: '#0f172a',
|
||||
},
|
||||
statValueWarning: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
itemCard: {
|
||||
marginBottom: 12,
|
||||
padding: 16,
|
||||
},
|
||||
itemCardWarning: {
|
||||
borderColor: '#fbbf24',
|
||||
backgroundColor: '#fef3c7',
|
||||
borderColor: '#c7d2fe',
|
||||
backgroundColor: '#e0e7ff',
|
||||
},
|
||||
itemHeader: {
|
||||
flexDirection: 'row',
|
||||
@@ -506,7 +506,7 @@ const styles = StyleSheet.create({
|
||||
itemPrice: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
statusSelector: {
|
||||
marginTop: 8,
|
||||
@@ -534,15 +534,15 @@ const styles = StyleSheet.create({
|
||||
color: '#64748b',
|
||||
},
|
||||
alertCard: {
|
||||
backgroundColor: '#fef3c7',
|
||||
borderColor: '#fbbf24',
|
||||
backgroundColor: '#e0e7ff',
|
||||
borderColor: '#c7d2fe',
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
},
|
||||
alertText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#92400e',
|
||||
color: '#4338ca',
|
||||
},
|
||||
formCard: {
|
||||
marginTop: 16,
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function Explore() {
|
||||
<Card style={styles.shopCard}>
|
||||
<View style={styles.shopHeader}>
|
||||
<Text style={styles.shopName}>{shop.name}</Text>
|
||||
<Badge color="amber">{shop.rating.toFixed(1)} ⭐</Badge>
|
||||
<Badge color="indigo">{shop.rating.toFixed(1)} ⭐</Badge>
|
||||
</View>
|
||||
<Text style={styles.shopAddress}>{shop.address}</Text>
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export default function Landing() {
|
||||
Explorar barbearias
|
||||
</Button>
|
||||
|
||||
{/* Botão nativo focado à inserção de utilizadores - Cria sessão no ecositema de Auth/BD */}
|
||||
{/* Botão focado no registo de novos utilizadores */}
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Register' as never)}
|
||||
variant="outline"
|
||||
@@ -80,7 +80,7 @@ const styles = StyleSheet.create({
|
||||
padding: 16,
|
||||
},
|
||||
hero: {
|
||||
backgroundColor: '#f59e0b',
|
||||
backgroundColor: '#6366f1',
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
marginBottom: 24,
|
||||
@@ -99,7 +99,7 @@ const styles = StyleSheet.create({
|
||||
},
|
||||
heroDesc: {
|
||||
fontSize: 16,
|
||||
color: '#fef3c7',
|
||||
color: '#e0e7ff',
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttons: {
|
||||
@@ -126,5 +126,3 @@ const styles = StyleSheet.create({
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ const styles = StyleSheet.create({
|
||||
itemTotal: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
emptyCard: {
|
||||
padding: 32,
|
||||
|
||||
@@ -164,7 +164,7 @@ const styles = StyleSheet.create({
|
||||
alignItems: 'center',
|
||||
},
|
||||
tabActive: {
|
||||
borderColor: '#f59e0b',
|
||||
borderColor: '#6366f1',
|
||||
backgroundColor: '#fef3c7',
|
||||
},
|
||||
tabText: {
|
||||
@@ -173,7 +173,7 @@ const styles = StyleSheet.create({
|
||||
color: '#64748b',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
list: {
|
||||
gap: 12,
|
||||
@@ -195,7 +195,7 @@ const styles = StyleSheet.create({
|
||||
itemPrice: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
color: '#6366f1',
|
||||
},
|
||||
itemDesc: {
|
||||
fontSize: 14,
|
||||
|
||||
Reference in New Issue
Block a user