first commit
This commit is contained in:
51
src/components/ui/Badge.tsx
Normal file
51
src/components/ui/Badge.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ViewStyle } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
color?: 'amber' | 'slate' | 'green' | 'red' | 'blue';
|
||||
variant?: 'solid' | 'soft';
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
const colorMap = {
|
||||
solid: {
|
||||
amber: { bg: '#f59e0b', text: '#fff' },
|
||||
slate: { bg: '#475569', text: '#fff' },
|
||||
green: { bg: '#10b981', text: '#fff' },
|
||||
red: { bg: '#ef4444', text: '#fff' },
|
||||
blue: { bg: '#3b82f6', text: '#fff' },
|
||||
},
|
||||
soft: {
|
||||
amber: { bg: '#fef3c7', text: '#92400e' },
|
||||
slate: { bg: '#f1f5f9', text: '#475569' },
|
||||
green: { bg: '#d1fae5', text: '#065f46' },
|
||||
red: { bg: '#fee2e2', text: '#991b1b' },
|
||||
blue: { bg: '#dbeafe', text: '#1e40af' },
|
||||
},
|
||||
};
|
||||
|
||||
export const Badge = ({ children, color = 'amber', variant = 'soft', style }: Props) => {
|
||||
const colors = colorMap[variant][color];
|
||||
|
||||
return (
|
||||
<View style={[styles.badge, { backgroundColor: colors.bg }, style]}>
|
||||
<Text style={[styles.text, { color: colors.text }]}>{children}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
badge: {
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
borderRadius: 12,
|
||||
alignSelf: 'flex-start',
|
||||
},
|
||||
text: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
103
src/components/ui/Button.tsx
Normal file
103
src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import React from 'react';
|
||||
import { TouchableOpacity, Text, StyleSheet, ActivityIndicator, ViewStyle, TextStyle } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
onPress?: () => void;
|
||||
variant?: 'solid' | 'outline' | 'ghost';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
disabled?: boolean;
|
||||
loading?: boolean;
|
||||
style?: ViewStyle;
|
||||
textStyle?: TextStyle;
|
||||
};
|
||||
|
||||
export const Button = ({ children, onPress, variant = 'solid', size = 'md', disabled, loading, style, textStyle }: Props) => {
|
||||
const buttonStyle = [
|
||||
styles.base,
|
||||
styles[variant],
|
||||
styles[`size_${size}`],
|
||||
(disabled || loading) && styles.disabled,
|
||||
style,
|
||||
];
|
||||
|
||||
const textStyles = [
|
||||
styles.text,
|
||||
styles[`text_${variant}`],
|
||||
styles[`textSize_${size}`],
|
||||
textStyle,
|
||||
];
|
||||
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={buttonStyle}
|
||||
onPress={onPress}
|
||||
disabled={disabled || loading}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
{loading ? (
|
||||
<ActivityIndicator color={variant === 'solid' ? '#fff' : '#f59e0b'} />
|
||||
) : (
|
||||
<Text style={textStyles}>{children}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
base: {
|
||||
borderRadius: 8,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
solid: {
|
||||
backgroundColor: '#f59e0b',
|
||||
},
|
||||
outline: {
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 2,
|
||||
borderColor: '#f59e0b',
|
||||
},
|
||||
ghost: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
size_sm: {
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
},
|
||||
size_md: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
},
|
||||
size_lg: {
|
||||
paddingHorizontal: 24,
|
||||
paddingVertical: 14,
|
||||
},
|
||||
disabled: {
|
||||
opacity: 0.5,
|
||||
},
|
||||
text: {
|
||||
fontWeight: '600',
|
||||
},
|
||||
text_solid: {
|
||||
color: '#fff',
|
||||
},
|
||||
text_outline: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
text_ghost: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
textSize_sm: {
|
||||
fontSize: 12,
|
||||
},
|
||||
textSize_md: {
|
||||
fontSize: 14,
|
||||
},
|
||||
textSize_lg: {
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
28
src/components/ui/Card.tsx
Normal file
28
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { View, StyleSheet, ViewStyle } from 'react-native';
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
style?: ViewStyle;
|
||||
};
|
||||
|
||||
export const Card = ({ children, style }: Props) => {
|
||||
return <View style={[styles.card, style]}>{children}</View>;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
card: {
|
||||
backgroundColor: '#fff',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
shadowColor: '#000',
|
||||
shadowOffset: { width: 0, height: 1 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 2,
|
||||
elevation: 2,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
53
src/components/ui/Input.tsx
Normal file
53
src/components/ui/Input.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React from 'react';
|
||||
import { TextInput, View, Text, StyleSheet, TextInputProps } from 'react-native';
|
||||
|
||||
type Props = TextInputProps & {
|
||||
label?: string;
|
||||
error?: string;
|
||||
};
|
||||
|
||||
export const Input = ({ label, error, style, ...props }: Props) => {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
{label && <Text style={styles.label}>{label}</Text>}
|
||||
<TextInput
|
||||
style={[styles.input, error && styles.inputError, style]}
|
||||
placeholderTextColor="#94a3b8"
|
||||
{...props}
|
||||
/>
|
||||
{error && <Text style={styles.error}>{error}</Text>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
label: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#334155',
|
||||
marginBottom: 6,
|
||||
},
|
||||
input: {
|
||||
borderWidth: 1,
|
||||
borderColor: '#cbd5e1',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 10,
|
||||
fontSize: 14,
|
||||
color: '#0f172a',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
inputError: {
|
||||
borderColor: '#ef4444',
|
||||
},
|
||||
error: {
|
||||
fontSize: 12,
|
||||
color: '#ef4444',
|
||||
marginTop: 4,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
323
src/context/AppContext.tsx
Normal file
323
src/context/AppContext.tsx
Normal file
@@ -0,0 +1,323 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { Appointment, Barber, BarberShop, CartItem, Order, Product, Service, User } from '../types';
|
||||
import { mockShops, mockUsers } from '../data/mock';
|
||||
import { storage } from '../lib/storage';
|
||||
|
||||
type State = {
|
||||
user?: User;
|
||||
users: User[];
|
||||
shops: BarberShop[];
|
||||
appointments: Appointment[];
|
||||
orders: Order[];
|
||||
cart: CartItem[];
|
||||
};
|
||||
|
||||
type AppContextValue = State & {
|
||||
login: (email: string, password: string) => boolean;
|
||||
logout: () => void;
|
||||
register: (payload: Omit<User, 'id' | 'shopId'> & { shopName?: string }) => boolean;
|
||||
addToCart: (item: CartItem) => void;
|
||||
removeFromCart: (refId: string) => void;
|
||||
clearCart: () => void;
|
||||
createAppointment: (input: Omit<Appointment, 'id' | 'status' | 'total'>) => Appointment | null;
|
||||
placeOrder: (customerId: string, shopId?: string) => Order | null;
|
||||
updateAppointmentStatus: (id: string, status: Appointment['status']) => void;
|
||||
updateOrderStatus: (id: string, status: Order['status']) => void;
|
||||
addService: (shopId: string, service: Omit<Service, 'id'>) => void;
|
||||
updateService: (shopId: string, service: Service) => void;
|
||||
deleteService: (shopId: string, serviceId: string) => void;
|
||||
addProduct: (shopId: string, product: Omit<Product, 'id'>) => void;
|
||||
updateProduct: (shopId: string, product: Product) => void;
|
||||
deleteProduct: (shopId: string, productId: string) => void;
|
||||
addBarber: (shopId: string, barber: Omit<Barber, 'id'>) => void;
|
||||
updateBarber: (shopId: string, barber: Barber) => void;
|
||||
deleteBarber: (shopId: string, barberId: string) => void;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
user: undefined,
|
||||
users: mockUsers,
|
||||
shops: mockShops,
|
||||
appointments: [],
|
||||
orders: [],
|
||||
cart: [],
|
||||
};
|
||||
|
||||
const AppContext = createContext<AppContextValue | undefined>(undefined);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [state, setState] = useState<State>(initialState);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const saved = await storage.get('smart-agenda', initialState);
|
||||
setState(saved);
|
||||
} catch (err) {
|
||||
console.error('Error loading data:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
storage.set('smart-agenda', state);
|
||||
}
|
||||
}, [state, isLoading]);
|
||||
|
||||
const login = (email: string, password: string) => {
|
||||
const found = state.users.find((u) => u.email === email && u.password === password);
|
||||
if (found) {
|
||||
setState((s) => ({ ...s, user: found }));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const logout = () => setState((s) => ({ ...s, user: undefined }));
|
||||
|
||||
const register: AppContextValue['register'] = ({ shopName, ...payload }) => {
|
||||
const exists = state.users.some((u) => u.email === payload.email);
|
||||
if (exists) return false;
|
||||
|
||||
if (payload.role === 'barbearia') {
|
||||
const shopId = nanoid();
|
||||
const shop: BarberShop = {
|
||||
id: shopId,
|
||||
name: shopName || `Barbearia ${payload.name}`,
|
||||
address: 'Endereço a definir',
|
||||
rating: 0,
|
||||
barbers: [],
|
||||
services: [],
|
||||
products: [],
|
||||
};
|
||||
const user: User = { ...payload, id: nanoid(), role: 'barbearia', shopId };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
user,
|
||||
users: [...s.users, user],
|
||||
shops: [...s.shops, shop],
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
const user: User = { ...payload, id: nanoid(), role: 'cliente' };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
user,
|
||||
users: [...s.users, user],
|
||||
}));
|
||||
return true;
|
||||
};
|
||||
|
||||
const addToCart: AppContextValue['addToCart'] = (item) => {
|
||||
setState((s) => {
|
||||
const cart = [...s.cart];
|
||||
const idx = cart.findIndex((c) => c.refId === item.refId && c.type === item.type && c.shopId === item.shopId);
|
||||
if (idx >= 0) cart[idx].qty += item.qty;
|
||||
else cart.push(item);
|
||||
return { ...s, cart };
|
||||
});
|
||||
};
|
||||
|
||||
const removeFromCart: AppContextValue['removeFromCart'] = (refId) => {
|
||||
setState((s) => ({ ...s, cart: s.cart.filter((c) => c.refId !== refId) }));
|
||||
};
|
||||
|
||||
const clearCart = () => setState((s) => ({ ...s, cart: [] }));
|
||||
|
||||
const createAppointment: AppContextValue['createAppointment'] = (input) => {
|
||||
const shop = state.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 exists = state.appointments.find(
|
||||
(ap) => ap.barberId === input.barberId && ap.date === input.date && ap.status !== 'cancelado'
|
||||
);
|
||||
if (exists) return null;
|
||||
|
||||
const appointment: Appointment = {
|
||||
...input,
|
||||
id: nanoid(),
|
||||
status: 'pendente',
|
||||
total: svc.price,
|
||||
};
|
||||
|
||||
setState((s) => ({ ...s, appointments: [...s.appointments, appointment] }));
|
||||
return appointment;
|
||||
};
|
||||
|
||||
const placeOrder: AppContextValue['placeOrder'] = (customerId, onlyShopId) => {
|
||||
if (!state.cart.length) return null;
|
||||
const grouped = state.cart.reduce<Record<string, CartItem[]>>((acc, item) => {
|
||||
acc[item.shopId] = acc[item.shopId] || [];
|
||||
acc[item.shopId].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const entries = Object.entries(grouped).filter(([shopId]) => (onlyShopId ? shopId === onlyShopId : true));
|
||||
|
||||
const newOrders: Order[] = entries.map(([shopId, items]) => {
|
||||
const total = items.reduce((sum, item) => {
|
||||
const shop = state.shops.find((s) => s.id === item.shopId);
|
||||
if (!shop) return sum;
|
||||
const price =
|
||||
item.type === 'service'
|
||||
? shop.services.find((s) => s.id === item.refId)?.price ?? 0
|
||||
: shop.products.find((p) => p.id === item.refId)?.price ?? 0;
|
||||
return sum + price * item.qty;
|
||||
}, 0);
|
||||
|
||||
return {
|
||||
id: nanoid(),
|
||||
shopId,
|
||||
customerId,
|
||||
items,
|
||||
total,
|
||||
status: 'pendente',
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
setState((s) => ({ ...s, orders: [...s.orders, ...newOrders], cart: [] }));
|
||||
return newOrders[0] ?? null;
|
||||
};
|
||||
|
||||
const updateAppointmentStatus: AppContextValue['updateAppointmentStatus'] = (id, status) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
appointments: s.appointments.map((a) => (a.id === id ? { ...a, status } : a)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateOrderStatus: AppContextValue['updateOrderStatus'] = (id, status) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
orders: s.orders.map((o) => (o.id === id ? { ...o, status } : o)),
|
||||
}));
|
||||
};
|
||||
|
||||
const addService: AppContextValue['addService'] = (shopId, service) => {
|
||||
const entry: Service = { ...service, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, services: [...shop.services, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateService: AppContextValue['updateService'] = (shopId, service) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, services: shop.services.map((sv) => (sv.id === service.id ? service : sv)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteService: AppContextValue['deleteService'] = (shopId, serviceId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, services: shop.services.filter((sv) => sv.id !== serviceId) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addProduct: AppContextValue['addProduct'] = (shopId, product) => {
|
||||
const entry: Product = { ...product, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, products: [...shop.products, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateProduct: AppContextValue['updateProduct'] = (shopId, product) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, products: shop.products.map((p) => (p.id === product.id ? product : p)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteProduct: AppContextValue['deleteProduct'] = (shopId, productId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, products: shop.products.filter((p) => p.id !== productId) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const addBarber: AppContextValue['addBarber'] = (shopId, barber) => {
|
||||
const entry: Barber = { ...barber, id: nanoid() };
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) => (shop.id === shopId ? { ...shop, barbers: [...shop.barbers, entry] } : shop)),
|
||||
}));
|
||||
};
|
||||
|
||||
const updateBarber: AppContextValue['updateBarber'] = (shopId, barber) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, barbers: shop.barbers.map((b) => (b.id === barber.id ? barber : b)) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const deleteBarber: AppContextValue['deleteBarber'] = (shopId, barberId) => {
|
||||
setState((s) => ({
|
||||
...s,
|
||||
shops: s.shops.map((shop) =>
|
||||
shop.id === shopId ? { ...shop, barbers: shop.barbers.filter((b) => b.id !== barberId) } : shop
|
||||
),
|
||||
}));
|
||||
};
|
||||
|
||||
const value: AppContextValue = useMemo(
|
||||
() => ({
|
||||
...state,
|
||||
login,
|
||||
logout,
|
||||
register,
|
||||
addToCart,
|
||||
removeFromCart,
|
||||
clearCart,
|
||||
createAppointment,
|
||||
placeOrder,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
updateService,
|
||||
deleteService,
|
||||
addProduct,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
addBarber,
|
||||
updateBarber,
|
||||
deleteBarber,
|
||||
}),
|
||||
[state]
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
return null; // Ou um componente de loading
|
||||
}
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
export const useApp = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
if (!ctx) throw new Error('useApp deve ser usado dentro de AppProvider');
|
||||
return ctx;
|
||||
};
|
||||
|
||||
|
||||
40
src/data/mock.ts
Normal file
40
src/data/mock.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BarberShop, User } from '../types';
|
||||
|
||||
export const mockUsers: User[] = [
|
||||
{ id: 'u1', name: 'Cliente Demo', email: 'cliente@demo.com', password: '123', role: 'cliente' },
|
||||
{ id: 'u2', name: 'Barbearia Demo', email: 'barber@demo.com', password: '123', role: 'barbearia', shopId: 's1' },
|
||||
];
|
||||
|
||||
export const mockShops: BarberShop[] = [
|
||||
{
|
||||
id: 's1',
|
||||
name: 'Barbearia Central',
|
||||
address: 'Rua Principal, 123',
|
||||
rating: 4.7,
|
||||
barbers: [
|
||||
{ id: 'b1', name: 'João', specialties: ['Fade', 'Navalha'], schedule: [{ day: '2025-01-01', slots: ['09:00', '10:00', '11:00'] }] },
|
||||
{ id: 'b2', name: 'Carlos', specialties: ['Barba', 'Clássico'], schedule: [{ day: '2025-01-01', slots: ['14:00', '15:00'] }] },
|
||||
],
|
||||
services: [
|
||||
{ id: 'sv1', name: 'Corte Fade', price: 60, duration: 45, barberIds: ['b1'] },
|
||||
{ id: 'sv2', name: 'Barba Completa', price: 40, duration: 30, barberIds: ['b2'] },
|
||||
],
|
||||
products: [
|
||||
{ id: 'p1', name: 'Pomada Matte', price: 35, stock: 8 },
|
||||
{ id: 'p2', name: 'Óleo para Barba', price: 45, stock: 5 },
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
name: 'Barbearia Bairro',
|
||||
address: 'Av. Verde, 45',
|
||||
rating: 4.5,
|
||||
barbers: [
|
||||
{ id: 'b3', name: 'Miguel', specialties: ['Clássico', 'Fade'], schedule: [{ day: '2025-01-01', slots: ['09:30', '10:30'] }] },
|
||||
],
|
||||
services: [{ id: 'sv3', name: 'Corte Clássico', price: 50, duration: 40, barberIds: ['b3'] }],
|
||||
products: [{ id: 'p3', name: 'Shampoo Masculino', price: 30, stock: 10 }],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
3
src/lib/format.ts
Normal file
3
src/lib/format.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const currency = (v: number) => v.toLocaleString('pt-BR', { style: 'currency', currency: 'BRL' });
|
||||
|
||||
|
||||
23
src/lib/storage.ts
Normal file
23
src/lib/storage.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
|
||||
export const storage = {
|
||||
async get<T>(key: string, fallback: T): Promise<T> {
|
||||
try {
|
||||
const raw = await AsyncStorage.getItem(key);
|
||||
if (!raw) return fallback;
|
||||
return JSON.parse(raw) as T;
|
||||
} catch (err) {
|
||||
console.error('storage parse error', err);
|
||||
return fallback;
|
||||
}
|
||||
},
|
||||
async set<T>(key: string, value: T): Promise<void> {
|
||||
try {
|
||||
await AsyncStorage.setItem(key, JSON.stringify(value));
|
||||
} catch (err) {
|
||||
console.error('storage set error', err);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
59
src/navigation/AppNavigator.tsx
Normal file
59
src/navigation/AppNavigator.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import Landing from '../pages/Landing';
|
||||
import AuthLogin from '../pages/AuthLogin';
|
||||
import AuthRegister from '../pages/AuthRegister';
|
||||
import Explore from '../pages/Explore';
|
||||
import ShopDetails from '../pages/ShopDetails';
|
||||
import Booking from '../pages/Booking';
|
||||
import Cart from '../pages/Cart';
|
||||
import Profile from '../pages/Profile';
|
||||
import Dashboard from '../pages/Dashboard';
|
||||
import { RootStackParamList } from './types';
|
||||
|
||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||
|
||||
export default function AppNavigator() {
|
||||
const { user } = useApp();
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerStyle: { backgroundColor: '#f59e0b' },
|
||||
headerTintColor: '#fff',
|
||||
headerTitleStyle: { fontWeight: 'bold' },
|
||||
}}
|
||||
>
|
||||
{!user ? (
|
||||
<>
|
||||
<Stack.Screen name="Landing" component={Landing} options={{ headerShown: false }} />
|
||||
<Stack.Screen name="Login" component={AuthLogin} options={{ title: 'Entrar' }} />
|
||||
<Stack.Screen name="Register" component={AuthRegister} options={{ title: 'Criar Conta' }} />
|
||||
<Stack.Screen name="Explore" component={Explore} options={{ title: 'Explorar' }} />
|
||||
<Stack.Screen name="ShopDetails" component={ShopDetails} options={{ title: 'Detalhes' }} />
|
||||
<Stack.Screen name="Booking" component={Booking} options={{ title: 'Agendar' }} />
|
||||
<Stack.Screen name="Cart" component={Cart} options={{ title: 'Carrinho' }} />
|
||||
</>
|
||||
) : user.role === 'barbearia' ? (
|
||||
<>
|
||||
<Stack.Screen name="Dashboard" component={Dashboard} options={{ title: 'Painel', headerShown: false }} />
|
||||
<Stack.Screen name="Profile" component={Profile} options={{ title: 'Perfil' }} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Stack.Screen name="Explore" component={Explore} options={{ title: 'Explorar' }} />
|
||||
<Stack.Screen name="ShopDetails" component={ShopDetails} options={{ title: 'Detalhes' }} />
|
||||
<Stack.Screen name="Booking" component={Booking} options={{ title: 'Agendar' }} />
|
||||
<Stack.Screen name="Cart" component={Cart} options={{ title: 'Carrinho' }} />
|
||||
<Stack.Screen name="Profile" component={Profile} options={{ title: 'Perfil' }} />
|
||||
</>
|
||||
)}
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
19
src/navigation/types.ts
Normal file
19
src/navigation/types.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export type RootStackParamList = {
|
||||
Landing: undefined;
|
||||
Login: undefined;
|
||||
Register: undefined;
|
||||
Explore: undefined;
|
||||
ShopDetails: { shopId: string };
|
||||
Booking: { shopId: string };
|
||||
Cart: undefined;
|
||||
Profile: undefined;
|
||||
Dashboard: undefined;
|
||||
};
|
||||
|
||||
declare global {
|
||||
namespace ReactNavigation {
|
||||
interface RootParamList extends RootStackParamList {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
148
src/pages/AuthLogin.tsx
Normal file
148
src/pages/AuthLogin.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Card } from '../components/ui/Card';
|
||||
|
||||
export default function AuthLogin() {
|
||||
const navigation = useNavigation();
|
||||
const { login } = useApp();
|
||||
const [email, setEmail] = useState('cliente@demo.com');
|
||||
const [password, setPassword] = useState('123');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
setError('');
|
||||
const ok = login(email, password);
|
||||
if (!ok) {
|
||||
setError('Credenciais inválidas');
|
||||
Alert.alert('Erro', 'Credenciais inválidas');
|
||||
} else {
|
||||
navigation.navigate('Explore' as never);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Card style={styles.card}>
|
||||
<Text style={styles.title}>Bem-vindo de volta</Text>
|
||||
<Text style={styles.subtitle}>Entre na sua conta para continuar</Text>
|
||||
|
||||
<View style={styles.demoBox}>
|
||||
<Text style={styles.demoTitle}>💡 Conta demo:</Text>
|
||||
<Text style={styles.demoText}>Cliente: cliente@demo.com / 123</Text>
|
||||
<Text style={styles.demoText}>Barbearia: barber@demo.com / 123</Text>
|
||||
</View>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
value={email}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('');
|
||||
}}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
placeholder="seu@email.com"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
value={password}
|
||||
onChangeText={(text) => {
|
||||
setPassword(text);
|
||||
setError('');
|
||||
}}
|
||||
secureTextEntry
|
||||
placeholder="••••••••"
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
|
||||
Entrar
|
||||
</Button>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Não tem conta? </Text>
|
||||
<Text
|
||||
style={styles.footerLink}
|
||||
onPress={() => navigation.navigate('Register' as never)}
|
||||
>
|
||||
Criar conta
|
||||
</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
justifyContent: 'center',
|
||||
minHeight: '100%',
|
||||
},
|
||||
card: {
|
||||
padding: 24,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
demoBox: {
|
||||
backgroundColor: '#fef3c7',
|
||||
borderWidth: 1,
|
||||
borderColor: '#fbbf24',
|
||||
borderRadius: 8,
|
||||
padding: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
demoTitle: {
|
||||
fontSize: 12,
|
||||
fontWeight: '600',
|
||||
color: '#92400e',
|
||||
marginBottom: 4,
|
||||
},
|
||||
demoText: {
|
||||
fontSize: 11,
|
||||
color: '#92400e',
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 24,
|
||||
paddingTop: 24,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e2e8f0',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: 14,
|
||||
color: '#f59e0b',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
182
src/pages/AuthRegister.tsx
Normal file
182
src/pages/AuthRegister.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, Alert, TouchableOpacity } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Card } from '../components/ui/Card';
|
||||
|
||||
export default function AuthRegister() {
|
||||
const navigation = useNavigation();
|
||||
const { register } = useApp();
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [role, setRole] = useState<'cliente' | 'barbearia'>('cliente');
|
||||
const [shopName, setShopName] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const handleSubmit = () => {
|
||||
setError('');
|
||||
const ok = register({ name, email, password, role, shopName });
|
||||
if (!ok) {
|
||||
setError('Email já registado');
|
||||
Alert.alert('Erro', 'Email já registado');
|
||||
} else {
|
||||
navigation.navigate('Explore' as never);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Card style={styles.card}>
|
||||
<Text style={styles.title}>Criar conta</Text>
|
||||
<Text style={styles.subtitle}>Escolha o tipo de acesso</Text>
|
||||
|
||||
<View style={styles.roleContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.roleButton, role === 'cliente' && styles.roleButtonActive]}
|
||||
onPress={() => setRole('cliente')}
|
||||
>
|
||||
<Text style={[styles.roleText, role === 'cliente' && styles.roleTextActive]}>
|
||||
Cliente
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.roleButton, role === 'barbearia' && styles.roleButtonActive]}
|
||||
onPress={() => setRole('barbearia')}
|
||||
>
|
||||
<Text style={[styles.roleText, role === 'barbearia' && styles.roleTextActive]}>
|
||||
Barbearia
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<Input
|
||||
label="Nome completo"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
placeholder="João Silva"
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Email"
|
||||
value={email}
|
||||
onChangeText={(text) => {
|
||||
setEmail(text);
|
||||
setError('');
|
||||
}}
|
||||
keyboardType="email-address"
|
||||
autoCapitalize="none"
|
||||
placeholder="seu@email.com"
|
||||
error={error}
|
||||
/>
|
||||
|
||||
<Input
|
||||
label="Senha"
|
||||
value={password}
|
||||
onChangeText={setPassword}
|
||||
secureTextEntry
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
|
||||
{role === 'barbearia' && (
|
||||
<Input
|
||||
label="Nome da barbearia"
|
||||
value={shopName}
|
||||
onChangeText={setShopName}
|
||||
placeholder="Barbearia XPTO"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Button onPress={handleSubmit} style={styles.submitButton} size="lg">
|
||||
Criar conta
|
||||
</Button>
|
||||
|
||||
<View style={styles.footer}>
|
||||
<Text style={styles.footerText}>Já tem conta? </Text>
|
||||
<Text
|
||||
style={styles.footerLink}
|
||||
onPress={() => navigation.navigate('Login' as never)}
|
||||
>
|
||||
Entrar
|
||||
</Text>
|
||||
</View>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
card: {
|
||||
padding: 24,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
textAlign: 'center',
|
||||
},
|
||||
subtitle: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 24,
|
||||
textAlign: 'center',
|
||||
},
|
||||
roleContainer: {
|
||||
flexDirection: 'row',
|
||||
gap: 12,
|
||||
marginBottom: 20,
|
||||
},
|
||||
roleButton: {
|
||||
flex: 1,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
borderWidth: 2,
|
||||
borderColor: '#e2e8f0',
|
||||
alignItems: 'center',
|
||||
},
|
||||
roleButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#fef3c7',
|
||||
},
|
||||
roleText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748b',
|
||||
},
|
||||
roleTextActive: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
footer: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'center',
|
||||
marginTop: 24,
|
||||
paddingTop: 24,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: '#e2e8f0',
|
||||
},
|
||||
footerText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
},
|
||||
footerLink: {
|
||||
fontSize: 14,
|
||||
color: '#f59e0b',
|
||||
fontWeight: '600',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
298
src/pages/Booking.tsx
Normal file
298
src/pages/Booking.tsx
Normal file
@@ -0,0 +1,298 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function Booking() {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation();
|
||||
const { shopId } = route.params as { shopId: string };
|
||||
const { shops, createAppointment, user, appointments } = useApp();
|
||||
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
|
||||
|
||||
const [serviceId, setService] = useState('');
|
||||
const [barberId, setBarber] = useState('');
|
||||
const [date, setDate] = useState('');
|
||||
const [slot, setSlot] = useState('');
|
||||
|
||||
if (!shop) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Barbearia não encontrada</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const selectedService = shop.services.find((s) => s.id === serviceId);
|
||||
const selectedBarber = shop.barbers.find((b) => b.id === barberId);
|
||||
|
||||
const generateDefaultSlots = (): string[] => {
|
||||
const slots: string[] = [];
|
||||
for (let hour = 9; hour <= 18; hour++) {
|
||||
slots.push(`${hour.toString().padStart(2, '0')}:00`);
|
||||
}
|
||||
return slots;
|
||||
};
|
||||
|
||||
const availableSlots = useMemo(() => {
|
||||
if (!selectedBarber || !date) return [];
|
||||
const specificSchedule = selectedBarber.schedule.find((s) => s.day === date);
|
||||
let slots = specificSchedule && specificSchedule.slots.length > 0
|
||||
? [...specificSchedule.slots]
|
||||
: generateDefaultSlots();
|
||||
|
||||
const bookedSlots = appointments
|
||||
.filter((apt) =>
|
||||
apt.barberId === barberId &&
|
||||
apt.status !== 'cancelado' &&
|
||||
apt.date.startsWith(date)
|
||||
)
|
||||
.map((apt) => {
|
||||
const parts = apt.date.split(' ');
|
||||
return parts.length > 1 ? parts[1] : '';
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return slots.filter((slot) => !bookedSlots.includes(slot));
|
||||
}, [selectedBarber, date, barberId, appointments]);
|
||||
|
||||
const canSubmit = serviceId && barberId && date && slot;
|
||||
|
||||
const submit = () => {
|
||||
if (!user) {
|
||||
Alert.alert('Login necessário', 'Faça login para agendar');
|
||||
navigation.navigate('Login' as never);
|
||||
return;
|
||||
}
|
||||
if (!canSubmit) return;
|
||||
const appt = createAppointment({ shopId: shop.id, serviceId, barberId, customerId: user.id, date: `${date} ${slot}` });
|
||||
if (appt) {
|
||||
Alert.alert('Sucesso', 'Agendamento criado com sucesso!');
|
||||
navigation.navigate('Profile' as never);
|
||||
} else {
|
||||
Alert.alert('Erro', 'Horário indisponível');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.title}>Agendar em {shop.name}</Text>
|
||||
|
||||
<Card style={styles.card}>
|
||||
<Text style={styles.sectionTitle}>1. Escolha o serviço</Text>
|
||||
<View style={styles.grid}>
|
||||
{shop.services.map((s) => (
|
||||
<TouchableOpacity
|
||||
key={s.id}
|
||||
style={[styles.serviceButton, serviceId === s.id && styles.serviceButtonActive]}
|
||||
onPress={() => setService(s.id)}
|
||||
>
|
||||
<Text style={[styles.serviceText, serviceId === s.id && styles.serviceTextActive]}>{s.name}</Text>
|
||||
<Text style={styles.servicePrice}>{currency(s.price)}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>2. Escolha o barbeiro</Text>
|
||||
<View style={styles.barberContainer}>
|
||||
{shop.barbers.map((b) => (
|
||||
<TouchableOpacity
|
||||
key={b.id}
|
||||
style={[styles.barberButton, barberId === b.id && styles.barberButtonActive]}
|
||||
onPress={() => setBarber(b.id)}
|
||||
>
|
||||
<Text style={[styles.barberText, barberId === b.id && styles.barberTextActive]}>
|
||||
{b.name}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>3. Escolha a data</Text>
|
||||
<Input
|
||||
value={date}
|
||||
onChangeText={setDate}
|
||||
placeholder="YYYY-MM-DD"
|
||||
/>
|
||||
|
||||
<Text style={styles.sectionTitle}>4. Escolha o horário</Text>
|
||||
<View style={styles.slotsContainer}>
|
||||
{availableSlots.length > 0 ? (
|
||||
availableSlots.map((h) => (
|
||||
<TouchableOpacity
|
||||
key={h}
|
||||
style={[styles.slotButton, slot === h && styles.slotButtonActive]}
|
||||
onPress={() => setSlot(h)}
|
||||
>
|
||||
<Text style={[styles.slotText, slot === h && styles.slotTextActive]}>{h}</Text>
|
||||
</TouchableOpacity>
|
||||
))
|
||||
) : (
|
||||
<Text style={styles.noSlots}>Escolha primeiro o barbeiro e a data</Text>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{canSubmit && selectedService && (
|
||||
<View style={styles.summary}>
|
||||
<Text style={styles.summaryTitle}>Resumo</Text>
|
||||
<Text style={styles.summaryText}>Serviço: {selectedService.name}</Text>
|
||||
<Text style={styles.summaryText}>Barbeiro: {selectedBarber?.name}</Text>
|
||||
<Text style={styles.summaryText}>Data: {date} às {slot}</Text>
|
||||
<Text style={styles.summaryTotal}>Total: {currency(selectedService.price)}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
<Button onPress={submit} disabled={!canSubmit} style={styles.submitButton} size="lg">
|
||||
{user ? 'Confirmar agendamento' : 'Entrar para agendar'}
|
||||
</Button>
|
||||
</Card>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 16,
|
||||
},
|
||||
card: {
|
||||
padding: 20,
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginTop: 16,
|
||||
marginBottom: 12,
|
||||
},
|
||||
grid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
serviceButton: {
|
||||
flex: 1,
|
||||
minWidth: '45%',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: '#e2e8f0',
|
||||
marginBottom: 8,
|
||||
},
|
||||
serviceButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#fef3c7',
|
||||
},
|
||||
serviceText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#0f172a',
|
||||
marginBottom: 4,
|
||||
},
|
||||
serviceTextActive: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
servicePrice: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
},
|
||||
barberContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
barberButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 20,
|
||||
borderWidth: 2,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
barberButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#f59e0b',
|
||||
},
|
||||
barberText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748b',
|
||||
},
|
||||
barberTextActive: {
|
||||
color: '#fff',
|
||||
},
|
||||
slotsContainer: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
slotButton: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: '#e2e8f0',
|
||||
},
|
||||
slotButtonActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#f59e0b',
|
||||
},
|
||||
slotText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748b',
|
||||
},
|
||||
slotTextActive: {
|
||||
color: '#fff',
|
||||
},
|
||||
noSlots: {
|
||||
fontSize: 14,
|
||||
color: '#94a3b8',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
summary: {
|
||||
backgroundColor: '#f1f5f9',
|
||||
padding: 16,
|
||||
borderRadius: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
summaryTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
summaryText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 4,
|
||||
},
|
||||
summaryTotal: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
marginTop: 8,
|
||||
},
|
||||
submitButton: {
|
||||
width: '100%',
|
||||
marginTop: 16,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
167
src/pages/Cart.tsx
Normal file
167
src/pages/Cart.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, Alert } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function Cart() {
|
||||
const navigation = useNavigation();
|
||||
const { cart, shops, removeFromCart, placeOrder, user } = useApp();
|
||||
|
||||
if (!cart.length) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Card style={styles.emptyCard}>
|
||||
<Text style={styles.emptyText}>Carrinho vazio</Text>
|
||||
</Card>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const grouped = cart.reduce<Record<string, typeof cart>>((acc, item) => {
|
||||
acc[item.shopId] = acc[item.shopId] || [];
|
||||
acc[item.shopId].push(item);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const handleCheckout = (shopId: string) => {
|
||||
if (!user) {
|
||||
Alert.alert('Login necessário', 'Faça login para finalizar o pedido');
|
||||
navigation.navigate('Login' as never);
|
||||
return;
|
||||
}
|
||||
const order = placeOrder(user.id, shopId);
|
||||
if (order) {
|
||||
Alert.alert('Sucesso', 'Pedido criado com sucesso!');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Text style={styles.title}>Carrinho</Text>
|
||||
{Object.entries(grouped).map(([shopId, items]) => {
|
||||
const shop = shops.find((s) => s.id === shopId);
|
||||
const total = items.reduce((sum, i) => {
|
||||
const price =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)?.price ?? 0
|
||||
: shop?.products.find((p) => p.id === i.refId)?.price ?? 0;
|
||||
return sum + price * i.qty;
|
||||
}, 0);
|
||||
|
||||
return (
|
||||
<Card key={shopId} style={styles.shopCard}>
|
||||
<View style={styles.shopHeader}>
|
||||
<View>
|
||||
<Text style={styles.shopName}>{shop?.name ?? 'Barbearia'}</Text>
|
||||
<Text style={styles.shopAddress}>{shop?.address}</Text>
|
||||
</View>
|
||||
<Text style={styles.total}>{currency(total)}</Text>
|
||||
</View>
|
||||
{items.map((i) => {
|
||||
const ref =
|
||||
i.type === 'service'
|
||||
? shop?.services.find((s) => s.id === i.refId)
|
||||
: shop?.products.find((p) => p.id === i.refId);
|
||||
return (
|
||||
<View key={i.refId} style={styles.item}>
|
||||
<Text style={styles.itemText}>
|
||||
{i.type === 'service' ? 'Serviço: ' : 'Produto: '}
|
||||
{ref?.name ?? 'Item'} x{i.qty}
|
||||
</Text>
|
||||
<Button
|
||||
onPress={() => removeFromCart(i.refId)}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
>
|
||||
Remover
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
{user ? (
|
||||
<Button onPress={() => handleCheckout(shopId)} style={styles.checkoutButton}>
|
||||
Finalizar pedido
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Login' as never)}
|
||||
style={styles.checkoutButton}
|
||||
>
|
||||
Entrar para finalizar
|
||||
</Button>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 16,
|
||||
},
|
||||
emptyCard: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 16,
|
||||
color: '#64748b',
|
||||
},
|
||||
shopCard: {
|
||||
marginBottom: 16,
|
||||
},
|
||||
shopHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 12,
|
||||
},
|
||||
shopName: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
},
|
||||
shopAddress: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
},
|
||||
total: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
item: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e2e8f0',
|
||||
},
|
||||
itemText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
flex: 1,
|
||||
},
|
||||
checkoutButton: {
|
||||
width: '100%',
|
||||
marginTop: 12,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
541
src/pages/Dashboard.tsx
Normal file
541
src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,541 @@
|
||||
import React, { useState } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity, Alert } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Input } from '../components/ui/Input';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function Dashboard() {
|
||||
const navigation = useNavigation();
|
||||
const {
|
||||
user,
|
||||
shops,
|
||||
appointments,
|
||||
orders,
|
||||
updateAppointmentStatus,
|
||||
updateOrderStatus,
|
||||
addService,
|
||||
addProduct,
|
||||
addBarber,
|
||||
updateProduct,
|
||||
deleteProduct,
|
||||
deleteService,
|
||||
deleteBarber,
|
||||
logout,
|
||||
} = useApp();
|
||||
|
||||
const shop = shops.find((s) => s.id === user?.shopId);
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'appointments' | 'orders' | 'services' | 'products' | 'barbers'>('overview');
|
||||
|
||||
const [svcName, setSvcName] = useState('');
|
||||
const [svcPrice, setSvcPrice] = useState('50');
|
||||
const [svcDuration, setSvcDuration] = useState('30');
|
||||
const [prodName, setProdName] = useState('');
|
||||
const [prodPrice, setProdPrice] = useState('30');
|
||||
const [prodStock, setProdStock] = useState('10');
|
||||
const [barberName, setBarberName] = useState('');
|
||||
const [barberSpecs, setBarberSpecs] = useState('');
|
||||
|
||||
if (!user || user.role !== 'barbearia') {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Área exclusiva para barbearias</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!shop) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Barbearia não encontrada</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const shopAppointments = appointments.filter((a) => a.shopId === shop.id);
|
||||
const shopOrders = orders.filter((o) => o.shopId === shop.id);
|
||||
const completedAppointments = shopAppointments.filter((a) => a.status === 'concluido');
|
||||
const activeAppointments = shopAppointments.filter((a) => a.status !== 'concluido');
|
||||
const productOrders = shopOrders.filter((o) => o.items.some((i) => i.type === 'product'));
|
||||
|
||||
const totalRevenue = shopOrders.reduce((s, o) => s + o.total, 0);
|
||||
const lowStock = shop.products.filter((p) => p.stock <= 3);
|
||||
|
||||
const addNewService = () => {
|
||||
if (!svcName.trim()) return;
|
||||
addService(shop.id, { name: svcName, price: Number(svcPrice) || 0, duration: Number(svcDuration) || 30, barberIds: [] });
|
||||
setSvcName('');
|
||||
setSvcPrice('50');
|
||||
setSvcDuration('30');
|
||||
Alert.alert('Sucesso', 'Serviço adicionado');
|
||||
};
|
||||
|
||||
const addNewProduct = () => {
|
||||
if (!prodName.trim()) return;
|
||||
addProduct(shop.id, { name: prodName, price: Number(prodPrice) || 0, stock: Number(prodStock) || 0 });
|
||||
setProdName('');
|
||||
setProdPrice('30');
|
||||
setProdStock('10');
|
||||
Alert.alert('Sucesso', 'Produto adicionado');
|
||||
};
|
||||
|
||||
const addNewBarber = () => {
|
||||
if (!barberName.trim()) return;
|
||||
addBarber(shop.id, {
|
||||
name: barberName,
|
||||
specialties: barberSpecs.split(',').map((s) => s.trim()).filter(Boolean),
|
||||
schedule: [],
|
||||
});
|
||||
setBarberName('');
|
||||
setBarberSpecs('');
|
||||
Alert.alert('Sucesso', 'Barbeiro adicionado');
|
||||
};
|
||||
|
||||
const updateProductStock = (productId: string, delta: number) => {
|
||||
const product = shop.products.find((p) => p.id === productId);
|
||||
if (!product) return;
|
||||
const next = { ...product, stock: Math.max(0, product.stock + delta) };
|
||||
updateProduct(shop.id, next);
|
||||
};
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', label: 'Visão Geral' },
|
||||
{ id: 'appointments', label: 'Agendamentos' },
|
||||
{ id: 'orders', label: 'Pedidos' },
|
||||
{ id: 'services', label: 'Serviços' },
|
||||
{ id: 'products', label: 'Produtos' },
|
||||
{ id: 'barbers', label: 'Barbeiros' },
|
||||
];
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{shop.name}</Text>
|
||||
<Button onPress={logout} variant="ghost" size="sm">
|
||||
Sair
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.tabsContainer}>
|
||||
{tabs.map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab.id}
|
||||
style={[styles.tab, activeTab === tab.id && styles.tabActive]}
|
||||
onPress={() => setActiveTab(tab.id as any)}
|
||||
>
|
||||
<Text style={[styles.tabText, activeTab === tab.id && styles.tabTextActive]}>
|
||||
{tab.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</ScrollView>
|
||||
|
||||
<ScrollView style={styles.content} contentContainerStyle={styles.contentInner}>
|
||||
{activeTab === 'overview' && (
|
||||
<View>
|
||||
<View style={styles.statsGrid}>
|
||||
<Card style={styles.statCard}>
|
||||
<Text style={styles.statLabel}>Faturamento</Text>
|
||||
<Text style={styles.statValue}>{currency(totalRevenue)}</Text>
|
||||
</Card>
|
||||
<Card style={styles.statCard}>
|
||||
<Text style={styles.statLabel}>Pendentes</Text>
|
||||
<Text style={styles.statValue}>{activeAppointments.length}</Text>
|
||||
</Card>
|
||||
<Card style={styles.statCard}>
|
||||
<Text style={styles.statLabel}>Concluídos</Text>
|
||||
<Text style={styles.statValue}>{completedAppointments.length}</Text>
|
||||
</Card>
|
||||
<Card style={styles.statCard}>
|
||||
<Text style={styles.statLabel}>Stock baixo</Text>
|
||||
<Text style={[styles.statValue, lowStock.length > 0 && styles.statValueWarning]}>
|
||||
{lowStock.length}
|
||||
</Text>
|
||||
</Card>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'appointments' && (
|
||||
<View>
|
||||
{activeAppointments.length > 0 ? (
|
||||
activeAppointments.map((a) => {
|
||||
const svc = shop.services.find((s) => s.id === a.serviceId);
|
||||
const barber = shop.barbers.find((b) => b.id === a.barberId);
|
||||
return (
|
||||
<Card key={a.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<View>
|
||||
<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'}>
|
||||
{a.status}
|
||||
</Badge>
|
||||
</View>
|
||||
<View style={styles.statusSelector}>
|
||||
<Text style={styles.selectorLabel}>Alterar status:</Text>
|
||||
<View style={styles.statusButtons}>
|
||||
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
|
||||
<Button
|
||||
key={s}
|
||||
onPress={() => updateAppointmentStatus(a.id, s as any)}
|
||||
variant={a.status === s ? 'solid' : 'outline'}
|
||||
size="sm"
|
||||
style={styles.statusButton}
|
||||
>
|
||||
{s}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Card style={styles.emptyCard}>
|
||||
<Text style={styles.emptyText}>Nenhum agendamento ativo</Text>
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'orders' && (
|
||||
<View>
|
||||
{productOrders.length > 0 ? (
|
||||
productOrders.map((o) => (
|
||||
<Card key={o.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<View>
|
||||
<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'}>
|
||||
{o.status}
|
||||
</Badge>
|
||||
</View>
|
||||
<View style={styles.statusButtons}>
|
||||
{['pendente', 'confirmado', 'concluido', 'cancelado'].map((s) => (
|
||||
<Button
|
||||
key={s}
|
||||
onPress={() => updateOrderStatus(o.id, s as any)}
|
||||
variant={o.status === s ? 'solid' : 'outline'}
|
||||
size="sm"
|
||||
style={styles.statusButton}
|
||||
>
|
||||
{s}
|
||||
</Button>
|
||||
))}
|
||||
</View>
|
||||
</Card>
|
||||
))
|
||||
) : (
|
||||
<Card style={styles.emptyCard}>
|
||||
<Text style={styles.emptyText}>Nenhum pedido de produtos</Text>
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'services' && (
|
||||
<View>
|
||||
{shop.services.map((s) => (
|
||||
<Card key={s.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<View>
|
||||
<Text style={styles.itemName}>{s.name}</Text>
|
||||
<Text style={styles.itemDesc}>Duração: {s.duration} min</Text>
|
||||
</View>
|
||||
<Text style={styles.itemPrice}>{currency(s.price)}</Text>
|
||||
</View>
|
||||
<Button
|
||||
onPress={() => {
|
||||
Alert.alert('Confirmar', 'Deseja remover este serviço?', [
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{ text: 'Remover', style: 'destructive', onPress: () => deleteService(shop.id, s.id) },
|
||||
]);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={styles.deleteButton}
|
||||
>
|
||||
Remover
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
<Card style={styles.formCard}>
|
||||
<Text style={styles.formTitle}>Adicionar serviço</Text>
|
||||
<Input label="Nome" value={svcName} onChangeText={setSvcName} placeholder="Ex: Corte Fade" />
|
||||
<Input label="Preço" value={svcPrice} onChangeText={setSvcPrice} keyboardType="numeric" placeholder="50" />
|
||||
<Input label="Duração (min)" value={svcDuration} onChangeText={setSvcDuration} keyboardType="numeric" placeholder="30" />
|
||||
<Button onPress={addNewService} style={styles.addButton}>
|
||||
Adicionar
|
||||
</Button>
|
||||
</Card>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'products' && (
|
||||
<View>
|
||||
{lowStock.length > 0 && (
|
||||
<Card style={styles.alertCard}>
|
||||
<Text style={styles.alertText}>
|
||||
⚠️ Atenção: {lowStock.length} {lowStock.length === 1 ? 'produto com stock baixo' : 'produtos com stock baixo'}
|
||||
</Text>
|
||||
</Card>
|
||||
)}
|
||||
{shop.products.map((p) => (
|
||||
<Card key={p.id} style={[styles.itemCard, p.stock <= 3 && styles.itemCardWarning]}>
|
||||
<View style={styles.itemHeader}>
|
||||
<View>
|
||||
<Text style={styles.itemName}>{p.name}</Text>
|
||||
<Text style={styles.itemDesc}>Stock: {p.stock} unidades</Text>
|
||||
</View>
|
||||
<Text style={styles.itemPrice}>{currency(p.price)}</Text>
|
||||
</View>
|
||||
<View style={styles.stockControls}>
|
||||
<Button onPress={() => updateProductStock(p.id, -1)} variant="outline" size="sm" style={styles.stockButton}>
|
||||
-1
|
||||
</Button>
|
||||
<Button onPress={() => updateProductStock(p.id, 1)} variant="outline" size="sm" style={styles.stockButton}>
|
||||
+1
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => {
|
||||
Alert.alert('Confirmar', 'Deseja remover este produto?', [
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{ text: 'Remover', style: 'destructive', onPress: () => deleteProduct(shop.id, p.id) },
|
||||
]);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={styles.stockButton}
|
||||
>
|
||||
Remover
|
||||
</Button>
|
||||
</View>
|
||||
</Card>
|
||||
))}
|
||||
<Card style={styles.formCard}>
|
||||
<Text style={styles.formTitle}>Adicionar produto</Text>
|
||||
<Input label="Nome" value={prodName} onChangeText={setProdName} placeholder="Ex: Pomada" />
|
||||
<Input label="Preço" value={prodPrice} onChangeText={setProdPrice} keyboardType="numeric" placeholder="30" />
|
||||
<Input label="Stock inicial" value={prodStock} onChangeText={setProdStock} keyboardType="numeric" placeholder="10" />
|
||||
<Button onPress={addNewProduct} style={styles.addButton}>
|
||||
Adicionar
|
||||
</Button>
|
||||
</Card>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{activeTab === 'barbers' && (
|
||||
<View>
|
||||
{shop.barbers.map((b) => (
|
||||
<Card key={b.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<View>
|
||||
<Text style={styles.itemName}>{b.name}</Text>
|
||||
<Text style={styles.itemDesc}>
|
||||
Especialidades: {b.specialties.length > 0 ? b.specialties.join(', ') : 'Nenhuma'}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Button
|
||||
onPress={() => {
|
||||
Alert.alert('Confirmar', 'Deseja remover este barbeiro?', [
|
||||
{ text: 'Cancelar', style: 'cancel' },
|
||||
{ text: 'Remover', style: 'destructive', onPress: () => deleteBarber(shop.id, b.id) },
|
||||
]);
|
||||
}}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
style={styles.deleteButton}
|
||||
>
|
||||
Remover
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
<Card style={styles.formCard}>
|
||||
<Text style={styles.formTitle}>Adicionar barbeiro</Text>
|
||||
<Input label="Nome" value={barberName} onChangeText={setBarberName} placeholder="Ex: João Silva" />
|
||||
<Input label="Especialidades" value={barberSpecs} onChangeText={setBarberSpecs} placeholder="Fade, Navalha, Barba" />
|
||||
<Button onPress={addNewBarber} style={styles.addButton}>
|
||||
Adicionar
|
||||
</Button>
|
||||
</Card>
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
header: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
padding: 16,
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e2e8f0',
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
},
|
||||
tabsContainer: {
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: '#e2e8f0',
|
||||
},
|
||||
tab: {
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: 'transparent',
|
||||
},
|
||||
tabActive: {
|
||||
borderBottomColor: '#f59e0b',
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748b',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
},
|
||||
contentInner: {
|
||||
padding: 16,
|
||||
},
|
||||
statsGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 12,
|
||||
marginBottom: 16,
|
||||
},
|
||||
statCard: {
|
||||
flex: 1,
|
||||
minWidth: '45%',
|
||||
padding: 16,
|
||||
},
|
||||
statLabel: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
marginBottom: 4,
|
||||
},
|
||||
statValue: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
},
|
||||
statValueWarning: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
itemCard: {
|
||||
marginBottom: 12,
|
||||
padding: 16,
|
||||
},
|
||||
itemCardWarning: {
|
||||
borderColor: '#fbbf24',
|
||||
backgroundColor: '#fef3c7',
|
||||
},
|
||||
itemHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
marginBottom: 12,
|
||||
},
|
||||
itemName: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
flex: 1,
|
||||
},
|
||||
itemDesc: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginTop: 4,
|
||||
},
|
||||
itemPrice: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
statusSelector: {
|
||||
marginTop: 8,
|
||||
},
|
||||
selectorLabel: {
|
||||
fontSize: 12,
|
||||
color: '#64748b',
|
||||
marginBottom: 8,
|
||||
},
|
||||
statusButtons: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: 8,
|
||||
},
|
||||
statusButton: {
|
||||
flex: 1,
|
||||
minWidth: '22%',
|
||||
},
|
||||
emptyCard: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
},
|
||||
alertCard: {
|
||||
backgroundColor: '#fef3c7',
|
||||
borderColor: '#fbbf24',
|
||||
marginBottom: 16,
|
||||
padding: 16,
|
||||
},
|
||||
alertText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#92400e',
|
||||
},
|
||||
formCard: {
|
||||
marginTop: 16,
|
||||
padding: 16,
|
||||
},
|
||||
formTitle: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 16,
|
||||
},
|
||||
addButton: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
deleteButton: {
|
||||
width: '100%',
|
||||
marginTop: 8,
|
||||
},
|
||||
stockControls: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
stockButton: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
109
src/pages/Explore.tsx
Normal file
109
src/pages/Explore.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, FlatList } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function Explore() {
|
||||
const navigation = useNavigation();
|
||||
const { shops } = useApp();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text style={styles.title}>Explorar barbearias</Text>
|
||||
<FlatList
|
||||
data={shops}
|
||||
keyExtractor={(item) => item.id}
|
||||
contentContainerStyle={styles.list}
|
||||
renderItem={({ item: shop }) => (
|
||||
<Card style={styles.shopCard}>
|
||||
<View style={styles.shopHeader}>
|
||||
<Text style={styles.shopName}>{shop.name}</Text>
|
||||
<Badge color="amber">{shop.rating.toFixed(1)} ⭐</Badge>
|
||||
</View>
|
||||
<Text style={styles.shopAddress}>{shop.address}</Text>
|
||||
<View style={styles.shopInfo}>
|
||||
<Text style={styles.shopInfoText}>{shop.services.length} serviços</Text>
|
||||
<Text style={styles.shopInfoText}>•</Text>
|
||||
<Text style={styles.shopInfoText}>{shop.barbers.length} barbeiros</Text>
|
||||
</View>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('ShopDetails' as never, { shopId: shop.id } as never)}
|
||||
variant="outline"
|
||||
style={styles.button}
|
||||
>
|
||||
Ver detalhes
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
|
||||
style={styles.button}
|
||||
>
|
||||
Agendar
|
||||
</Button>
|
||||
</View>
|
||||
</Card>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 16,
|
||||
},
|
||||
list: {
|
||||
gap: 16,
|
||||
},
|
||||
shopCard: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
shopHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
shopName: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
flex: 1,
|
||||
},
|
||||
shopAddress: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 8,
|
||||
},
|
||||
shopInfo: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
shopInfoText: {
|
||||
fontSize: 12,
|
||||
color: '#94a3b8',
|
||||
},
|
||||
buttons: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginTop: 8,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
119
src/pages/Landing.tsx
Normal file
119
src/pages/Landing.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Card } from '../components/ui/Card';
|
||||
|
||||
export default function Landing() {
|
||||
const navigation = useNavigation();
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.hero}>
|
||||
<Text style={styles.heroTitle}>Smart Agenda</Text>
|
||||
<Text style={styles.heroSubtitle}>
|
||||
Agendamentos, produtos e gestão em um único lugar.
|
||||
</Text>
|
||||
<Text style={styles.heroDesc}>
|
||||
Experiência mobile-first para clientes e painel completo para barbearias.
|
||||
</Text>
|
||||
<View style={styles.buttons}>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Explore' as never)}
|
||||
style={styles.button}
|
||||
size="lg"
|
||||
>
|
||||
Explorar barbearias
|
||||
</Button>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Register' as never)}
|
||||
variant="outline"
|
||||
style={styles.button}
|
||||
size="lg"
|
||||
>
|
||||
Criar conta
|
||||
</Button>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.features}>
|
||||
<Card style={styles.featureCard}>
|
||||
<Text style={styles.featureTitle}>Agendamentos</Text>
|
||||
<Text style={styles.featureDesc}>
|
||||
Escolha serviço, barbeiro, data e horário com validação de slots.
|
||||
</Text>
|
||||
</Card>
|
||||
<Card style={styles.featureCard}>
|
||||
<Text style={styles.featureTitle}>Carrinho</Text>
|
||||
<Text style={styles.featureDesc}>
|
||||
Produtos e serviços agrupados por barbearia, pagamento rápido.
|
||||
</Text>
|
||||
</Card>
|
||||
<Card style={styles.featureCard}>
|
||||
<Text style={styles.featureTitle}>Painel</Text>
|
||||
<Text style={styles.featureDesc}>
|
||||
Faturamento, agendamentos, pedidos, barbearia no controle.
|
||||
</Text>
|
||||
</Card>
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
hero: {
|
||||
backgroundColor: '#f59e0b',
|
||||
borderRadius: 16,
|
||||
padding: 24,
|
||||
marginBottom: 24,
|
||||
},
|
||||
heroTitle: {
|
||||
fontSize: 28,
|
||||
fontWeight: 'bold',
|
||||
color: '#fff',
|
||||
marginBottom: 8,
|
||||
},
|
||||
heroSubtitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
color: '#fff',
|
||||
marginBottom: 8,
|
||||
},
|
||||
heroDesc: {
|
||||
fontSize: 16,
|
||||
color: '#fef3c7',
|
||||
marginBottom: 20,
|
||||
},
|
||||
buttons: {
|
||||
gap: 12,
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
},
|
||||
features: {
|
||||
gap: 16,
|
||||
},
|
||||
featureCard: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
featureTitle: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
featureDesc: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
lineHeight: 20,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
165
src/pages/Profile.tsx
Normal file
165
src/pages/Profile.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
const statusColor: Record<string, 'amber' | 'green' | 'slate' | 'red'> = {
|
||||
pendente: 'amber',
|
||||
confirmado: 'green',
|
||||
concluido: 'green',
|
||||
cancelado: 'red',
|
||||
};
|
||||
|
||||
export default function Profile() {
|
||||
const navigation = useNavigation();
|
||||
const { user, appointments, orders, shops, logout } = useApp();
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Faça login para ver o perfil</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const myAppointments = appointments.filter((a) => a.customerId === user.id);
|
||||
const myOrders = orders.filter((o) => o.customerId === user.id);
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<Card style={styles.profileCard}>
|
||||
<Text style={styles.profileName}>Olá, {user.name}</Text>
|
||||
<Text style={styles.profileEmail}>{user.email}</Text>
|
||||
<Badge color="amber" style={styles.roleBadge}>
|
||||
{user.role === 'cliente' ? 'Cliente' : 'Barbearia'}
|
||||
</Badge>
|
||||
<Button onPress={logout} variant="outline" style={styles.logoutButton}>
|
||||
Sair
|
||||
</Button>
|
||||
</Card>
|
||||
|
||||
<Text style={styles.sectionTitle}>Agendamentos</Text>
|
||||
{myAppointments.length > 0 ? (
|
||||
myAppointments.map((a) => {
|
||||
const shop = shops.find((s) => s.id === a.shopId);
|
||||
return (
|
||||
<Card key={a.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<Text style={styles.itemName}>{shop?.name}</Text>
|
||||
<Badge color={statusColor[a.status]}>{a.status}</Badge>
|
||||
</View>
|
||||
<Text style={styles.itemDate}>{a.date}</Text>
|
||||
<Text style={styles.itemTotal}>{currency(a.total)}</Text>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Card style={styles.emptyCard}>
|
||||
<Text style={styles.emptyText}>Nenhum agendamento ainda</Text>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Text style={styles.sectionTitle}>Pedidos</Text>
|
||||
{myOrders.length > 0 ? (
|
||||
myOrders.map((o) => {
|
||||
const shop = shops.find((s) => s.id === o.shopId);
|
||||
return (
|
||||
<Card key={o.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<Text style={styles.itemName}>{shop?.name}</Text>
|
||||
<Badge color={statusColor[o.status]}>{o.status}</Badge>
|
||||
</View>
|
||||
<Text style={styles.itemDate}>
|
||||
{new Date(o.createdAt).toLocaleString('pt-BR')}
|
||||
</Text>
|
||||
<Text style={styles.itemTotal}>{currency(o.total)}</Text>
|
||||
</Card>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<Card style={styles.emptyCard}>
|
||||
<Text style={styles.emptyText}>Nenhum pedido ainda</Text>
|
||||
</Card>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
profileCard: {
|
||||
marginBottom: 24,
|
||||
padding: 20,
|
||||
},
|
||||
profileName: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 4,
|
||||
},
|
||||
profileEmail: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 12,
|
||||
},
|
||||
roleBadge: {
|
||||
alignSelf: 'flex-start',
|
||||
marginBottom: 16,
|
||||
},
|
||||
logoutButton: {
|
||||
width: '100%',
|
||||
},
|
||||
sectionTitle: {
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 12,
|
||||
marginTop: 8,
|
||||
},
|
||||
itemCard: {
|
||||
marginBottom: 12,
|
||||
padding: 16,
|
||||
},
|
||||
itemHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: 8,
|
||||
},
|
||||
itemName: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
flex: 1,
|
||||
},
|
||||
itemDate: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 4,
|
||||
},
|
||||
itemTotal: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
emptyCard: {
|
||||
padding: 32,
|
||||
alignItems: 'center',
|
||||
},
|
||||
emptyText: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
184
src/pages/ShopDetails.tsx
Normal file
184
src/pages/ShopDetails.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { View, Text, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
|
||||
import { useRoute, useNavigation } from '@react-navigation/native';
|
||||
import { useApp } from '../context/AppContext';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Badge } from '../components/ui/Badge';
|
||||
import { currency } from '../lib/format';
|
||||
|
||||
export default function ShopDetails() {
|
||||
const route = useRoute();
|
||||
const navigation = useNavigation();
|
||||
const { shopId } = route.params as { shopId: string };
|
||||
const { shops, addToCart } = useApp();
|
||||
const shop = useMemo(() => shops.find((s) => s.id === shopId), [shops, shopId]);
|
||||
const [tab, setTab] = useState<'servicos' | 'produtos'>('servicos');
|
||||
|
||||
if (!shop) {
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Text>Barbearia não encontrada</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView style={styles.container} contentContainerStyle={styles.content}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>{shop.name}</Text>
|
||||
<Text style={styles.address}>{shop.address}</Text>
|
||||
<Button
|
||||
onPress={() => navigation.navigate('Booking' as never, { shopId: shop.id } as never)}
|
||||
style={styles.bookButton}
|
||||
>
|
||||
Agendar
|
||||
</Button>
|
||||
</View>
|
||||
|
||||
<View style={styles.tabs}>
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, tab === 'servicos' && styles.tabActive]}
|
||||
onPress={() => setTab('servicos')}
|
||||
>
|
||||
<Text style={[styles.tabText, tab === 'servicos' && styles.tabTextActive]}>Serviços</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, tab === 'produtos' && styles.tabActive]}
|
||||
onPress={() => setTab('produtos')}
|
||||
>
|
||||
<Text style={[styles.tabText, tab === 'produtos' && styles.tabTextActive]}>Produtos</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{tab === 'servicos' ? (
|
||||
<View style={styles.list}>
|
||||
{shop.services.map((service) => (
|
||||
<Card key={service.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<Text style={styles.itemName}>{service.name}</Text>
|
||||
<Text style={styles.itemPrice}>{currency(service.price)}</Text>
|
||||
</View>
|
||||
<Text style={styles.itemDesc}>Duração: {service.duration} min</Text>
|
||||
<Button
|
||||
onPress={() => addToCart({ shopId: shop.id, type: 'service', refId: service.id, qty: 1 })}
|
||||
size="sm"
|
||||
style={styles.addButton}
|
||||
>
|
||||
Adicionar ao carrinho
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
) : (
|
||||
<View style={styles.list}>
|
||||
{shop.products.map((product) => (
|
||||
<Card key={product.id} style={styles.itemCard}>
|
||||
<View style={styles.itemHeader}>
|
||||
<Text style={styles.itemName}>{product.name}</Text>
|
||||
<Text style={styles.itemPrice}>{currency(product.price)}</Text>
|
||||
</View>
|
||||
<Text style={styles.itemDesc}>Stock: {product.stock} unidades</Text>
|
||||
{product.stock <= 3 && <Badge color="amber" style={styles.stockBadge}>Stock baixo</Badge>}
|
||||
<Button
|
||||
onPress={() => addToCart({ shopId: shop.id, type: 'product', refId: product.id, qty: 1 })}
|
||||
size="sm"
|
||||
style={styles.addButton}
|
||||
disabled={product.stock <= 0}
|
||||
>
|
||||
{product.stock > 0 ? 'Adicionar ao carrinho' : 'Sem stock'}
|
||||
</Button>
|
||||
</Card>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: '#f8fafc',
|
||||
},
|
||||
content: {
|
||||
padding: 16,
|
||||
},
|
||||
header: {
|
||||
marginBottom: 20,
|
||||
},
|
||||
title: {
|
||||
fontSize: 24,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
marginBottom: 8,
|
||||
},
|
||||
address: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 16,
|
||||
},
|
||||
bookButton: {
|
||||
width: '100%',
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
},
|
||||
tab: {
|
||||
flex: 1,
|
||||
padding: 12,
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e2e8f0',
|
||||
alignItems: 'center',
|
||||
},
|
||||
tabActive: {
|
||||
borderColor: '#f59e0b',
|
||||
backgroundColor: '#fef3c7',
|
||||
},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
fontWeight: '600',
|
||||
color: '#64748b',
|
||||
},
|
||||
tabTextActive: {
|
||||
color: '#f59e0b',
|
||||
},
|
||||
list: {
|
||||
gap: 12,
|
||||
},
|
||||
itemCard: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
itemHeader: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
marginBottom: 8,
|
||||
},
|
||||
itemName: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#0f172a',
|
||||
flex: 1,
|
||||
},
|
||||
itemPrice: {
|
||||
fontSize: 16,
|
||||
fontWeight: 'bold',
|
||||
color: '#f59e0b',
|
||||
},
|
||||
itemDesc: {
|
||||
fontSize: 14,
|
||||
color: '#64748b',
|
||||
marginBottom: 12,
|
||||
},
|
||||
stockBadge: {
|
||||
marginBottom: 8,
|
||||
},
|
||||
addButton: {
|
||||
width: '100%',
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
12
src/types.ts
Normal file
12
src/types.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
export type Barber = { id: string; name: string; specialties: string[]; schedule: { day: string; slots: string[] }[] };
|
||||
export type Service = { id: string; name: string; price: number; duration: number; barberIds: string[] };
|
||||
export type Product = { id: string; name: string; price: number; stock: number };
|
||||
export type BarberShop = { id: string; name: string; address: string; rating: number; services: Service[]; products: Product[]; barbers: Barber[] };
|
||||
export type AppointmentStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
|
||||
export type OrderStatus = 'pendente' | 'confirmado' | 'concluido' | 'cancelado';
|
||||
export type Appointment = { id: string; shopId: string; serviceId: string; barberId: string; customerId: string; date: string; status: AppointmentStatus; total: number };
|
||||
export type CartItem = { shopId: string; type: 'service' | 'product'; refId: string; qty: number };
|
||||
export type Order = { id: string; shopId: string; customerId: string; items: CartItem[]; total: number; status: OrderStatus; createdAt: string };
|
||||
export type User = { id: string; name: string; email: string; password: string; role: 'cliente' | 'barbearia'; shopId?: string };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user