345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
|
|
import { useLocalSearchParams } from 'expo-router';
|
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
|
|
export interface Service {
|
|
id: string;
|
|
nomePt: string;
|
|
nomeEs: string;
|
|
precoPt: number;
|
|
precoEs: number;
|
|
duracao: number; // em minutos
|
|
}
|
|
|
|
export interface Barber {
|
|
id: string;
|
|
nome: string;
|
|
foto: string;
|
|
commission: number;
|
|
email?: string;
|
|
password?: string;
|
|
permissions?: {
|
|
canViewFinance: boolean;
|
|
canEditConfig: boolean;
|
|
canEditAgenda: boolean;
|
|
};
|
|
}
|
|
|
|
export interface Appointment {
|
|
id: string;
|
|
clientName: string;
|
|
serviceIds: string[];
|
|
barberId: string;
|
|
date: string;
|
|
time: string;
|
|
status: 'pending' | 'accepted' | 'rejected';
|
|
totalPt: number;
|
|
totalEs: number;
|
|
}
|
|
|
|
export interface BlockedSlot {
|
|
id: string;
|
|
barberId: string;
|
|
date: string;
|
|
time: string; // Pode ser 'all-day' ou um horário específico ex: '09:00'
|
|
}
|
|
|
|
export interface BarbeariaData {
|
|
id: string;
|
|
nome: string;
|
|
slug: string;
|
|
logo: string;
|
|
endereco: string;
|
|
cidade: string;
|
|
numero: string;
|
|
services: Service[];
|
|
barbers: Barber[];
|
|
appointments: Appointment[];
|
|
paymentMethods: string[];
|
|
blockedSlots: BlockedSlot[];
|
|
colors: {
|
|
primary: string;
|
|
secondary: string;
|
|
accent: string;
|
|
background: string;
|
|
card: string;
|
|
text: string;
|
|
textMuted: string;
|
|
};
|
|
}
|
|
|
|
interface BarbeariaContextType {
|
|
barbearia: BarbeariaData | null;
|
|
isLoading: boolean;
|
|
error: string | null;
|
|
activeBarberId: string | null;
|
|
loginBarber: (id: string | null) => void;
|
|
updateBarbearia: (data: Partial<BarbeariaData>) => Promise<void>;
|
|
addService: (service: Omit<Service, 'id'>) => Promise<void>;
|
|
updateService: (id: string, service: Omit<Service, 'id'>) => Promise<void>;
|
|
removeService: (id: string) => Promise<void>;
|
|
addBarber: (barber: Omit<Barber, 'id'>) => Promise<void>;
|
|
updateBarber: (id: string, barber: Omit<Barber, 'id'>) => Promise<void>;
|
|
removeBarber: (id: string) => Promise<void>;
|
|
addAppointment: (appointment: Omit<Appointment, 'id' | 'status'>) => Promise<void>;
|
|
updateAppointmentStatus: (id: string, status: 'accepted' | 'rejected') => Promise<void>;
|
|
updateBlockedSlots: (slots: {barberId: string, date: string, time: string}[], action: 'block' | 'unblock') => Promise<void>;
|
|
}
|
|
|
|
const BarbeariaContext = createContext<BarbeariaContextType | undefined>(undefined);
|
|
|
|
const STORAGE_KEY = '@barber_flow_barbearia_data';
|
|
|
|
const DEFAULT_COLORS = {
|
|
primary: '#EAB308',
|
|
secondary: '#1A1A1A',
|
|
accent: '#8B4513',
|
|
background: '#0F0F0F',
|
|
card: '#1E1E1E',
|
|
text: '#FFFFFF',
|
|
textMuted: '#A0A0A0',
|
|
};
|
|
|
|
export function BarbeariaProvider({ children }: { children: ReactNode }) {
|
|
const { slug } = useLocalSearchParams<{ slug: string }>();
|
|
const [barbearia, setBarbearia] = useState<BarbeariaData | null>(null);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [activeBarberId, setActiveBarberId] = useState<string | null>(null);
|
|
|
|
const loginBarber = (id: string | null) => {
|
|
setActiveBarberId(id);
|
|
};
|
|
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
setIsLoading(true);
|
|
try {
|
|
const stored = await AsyncStorage.getItem(STORAGE_KEY);
|
|
|
|
if (stored) {
|
|
const parsed = JSON.parse(stored) as BarbeariaData;
|
|
if (!slug || parsed.slug === slug) {
|
|
setBarbearia(parsed);
|
|
} else if (slug === 'vintage-barber') {
|
|
setBarbearia(parsed);
|
|
} else {
|
|
// Se o slug for diferente, podemos recriar um mock com esse slug para fins de demonstração
|
|
const newMock = { ...parsed, slug: slug };
|
|
setBarbearia(newMock);
|
|
}
|
|
} else {
|
|
// MOCK INICIAL (Primeira vez que o app abre em um dispositivo novo)
|
|
// Isso garante que o link funcione em qualquer celular mesmo sem banco de dados real
|
|
const mock: BarbeariaData = {
|
|
id: '1',
|
|
nome: slug ? slug.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') : 'Barbearia Modelo',
|
|
slug: slug || 'vintage-barber',
|
|
logo: 'https://images.unsplash.com/photo-1503951914875-452162b0f3f1?q=80&w=200&h=200&auto=format&fit=crop',
|
|
endereco: 'Rua das Flores, 123',
|
|
cidade: 'São Paulo',
|
|
numero: 'Sede',
|
|
services: [
|
|
{ id: '1', nomePt: 'Corte de Cabelo', nomeEs: 'Corte de Cabello', precoPt: 50, precoEs: 70000, duracao: 30 },
|
|
{ id: '2', nomePt: 'Barba Completa', nomeEs: 'Barba Completa', precoPt: 35, precoEs: 50000, duracao: 20 },
|
|
{ id: '3', nomePt: 'Combo (Corte + Barba)', nomeEs: 'Combo (Corte + Barba)', precoPt: 75, precoEs: 100000, duracao: 50 },
|
|
],
|
|
barbers: [
|
|
{
|
|
id: '1',
|
|
nome: 'Marcus Silva',
|
|
foto: 'https://images.unsplash.com/photo-1503443207922-dff7d543fd0e?w=400',
|
|
commission: 50,
|
|
email: 'marcus@barber.com',
|
|
password: '123',
|
|
permissions: { canViewFinance: false, canEditConfig: false, canEditAgenda: true }
|
|
},
|
|
],
|
|
appointments: [],
|
|
paymentMethods: ['money', 'pix', 'card', 'alias'],
|
|
blockedSlots: [],
|
|
colors: DEFAULT_COLORS,
|
|
};
|
|
setBarbearia(mock);
|
|
// Salva no novo dispositivo para que ele também tenha uma base de dados local
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(mock));
|
|
}
|
|
} catch (err) {
|
|
console.error('Erro ao carregar dados:', err);
|
|
setError('Erro ao carregar dados');
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}
|
|
|
|
loadData();
|
|
}, [slug]);
|
|
|
|
const updateBarbearia = async (data: Partial<BarbeariaData>) => {
|
|
const updated = barbearia
|
|
? { ...barbearia, ...data }
|
|
: {
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
nome: '',
|
|
slug: '',
|
|
logo: '',
|
|
endereco: '',
|
|
cidade: '',
|
|
numero: '',
|
|
services: [],
|
|
barbers: [],
|
|
appointments: [],
|
|
colors: DEFAULT_COLORS,
|
|
...data
|
|
} as BarbeariaData;
|
|
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const addService = async (service: Omit<Service, 'id'>) => {
|
|
const newService = { ...service, id: Math.random().toString(36).substr(2, 9) };
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
services: [...(barbearia.services || []), newService]
|
|
} as BarbeariaData;
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const updateService = async (id: string, service: Omit<Service, 'id'>) => {
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
services: barbearia.services.map(s => s.id === id ? { ...service, id } : s)
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const removeService = async (id: string) => {
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
services: barbearia.services.filter(s => s.id !== id)
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const addBarber = async (barber: Omit<Barber, 'id'>) => {
|
|
const newBarber = { ...barber, id: Math.random().toString(36).substr(2, 9) };
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
barbers: [...(barbearia.barbers || []), newBarber]
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const updateBarber = async (id: string, barber: Omit<Barber, 'id'>) => {
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
barbers: barbearia.barbers.map(b => b.id === id ? { ...barber, id } : b)
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const removeBarber = async (id: string) => {
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
barbers: barbearia.barbers.filter(b => b.id !== id)
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const addAppointment = async (appointment: Omit<Appointment, 'id' | 'status'>) => {
|
|
const newAppointment: Appointment = {
|
|
...appointment,
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
status: 'pending'
|
|
};
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
appointments: [...(barbearia.appointments || []), newAppointment]
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const updateAppointmentStatus = async (id: string, status: 'accepted' | 'rejected') => {
|
|
if (!barbearia) return;
|
|
const updated = {
|
|
...barbearia,
|
|
appointments: barbearia.appointments.map(a => a.id === id ? { ...a, status } : a)
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
const updateBlockedSlots = async (slots: {barberId: string, date: string, time: string}[], action: 'block' | 'unblock') => {
|
|
if (!barbearia) return;
|
|
|
|
let newBlockedSlots = [...(barbearia.blockedSlots || [])];
|
|
|
|
slots.forEach(slot => {
|
|
const existingIndex = newBlockedSlots.findIndex(
|
|
s => s.barberId === slot.barberId && s.date === slot.date && s.time === slot.time
|
|
);
|
|
|
|
if (action === 'block' && existingIndex === -1) {
|
|
newBlockedSlots.push({
|
|
id: Math.random().toString(36).substr(2, 9),
|
|
...slot
|
|
});
|
|
} else if (action === 'unblock' && existingIndex >= 0) {
|
|
newBlockedSlots.splice(existingIndex, 1);
|
|
}
|
|
});
|
|
|
|
const updated = {
|
|
...barbearia,
|
|
blockedSlots: newBlockedSlots
|
|
};
|
|
setBarbearia(updated);
|
|
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(updated));
|
|
};
|
|
|
|
return (
|
|
<BarbeariaContext.Provider value={{
|
|
barbearia,
|
|
isLoading,
|
|
error,
|
|
activeBarberId,
|
|
loginBarber,
|
|
updateBarbearia,
|
|
addService,
|
|
updateService,
|
|
removeService,
|
|
addBarber,
|
|
updateBarber,
|
|
removeBarber,
|
|
addAppointment,
|
|
updateAppointmentStatus,
|
|
updateBlockedSlots
|
|
}}>
|
|
{children}
|
|
</BarbeariaContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const useBarbearia = () => {
|
|
const context = useContext(BarbeariaContext);
|
|
if (context === undefined) {
|
|
throw new Error('useBarbearia deve ser usado dentro de um BarbeariaProvider');
|
|
}
|
|
return context;
|
|
};
|