arbritage/context/CRMContext.tsx

852 lines
32 KiB
TypeScript
Raw Permalink Normal View History

2026-01-26 14:20:25 +00:00
import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
import { Product, ShoppingListItem, Order, InventoryItem, Supplier, User, PlatformFee, PLATFORMS, Customer, Transaction, FinancialSummary, AuctionLot, BiddingTender, Sale, SalesChannel, PaymentMethod, OrderStatus } from '../types';
import { searchProducts } from '../services/geminiService';
import { supabase } from '../services/supabase';
import { Session, User as SupabaseUser } from '@supabase/supabase-js';
interface CRMContextType {
// Auth State
user: User | null;
session: Session | null;
authLoading: boolean;
isAdmin: boolean; // New
signIn: (email: string, password: string) => Promise<{ error: any }>;
signOut: () => Promise<void>;
updateUserRole: (userId: string, role: 'admin' | 'user') => Promise<void>; // New action
// Data State
products: Product[];
shoppingList: ShoppingListItem[];
orders: Order[];
inventory: InventoryItem[];
suppliers: Supplier[];
users: User[];
customers: Customer[];
transactions: Transaction[];
searchTerm: string;
loading: boolean;
error: string | null;
searchLoading: boolean; // New separate loading state for search
searchError: string | null; // New separate error state for search
selectedProduct: Product | null;
searchType: 'specific' | 'opportunity';
setSearchType: (type: 'specific' | 'opportunity') => void;
overheadPercent: number;
exchangeRate: number;
activeOrderId: string | null;
useOverhead: boolean;
setUseOverhead: (use: boolean) => void;
// Actions
setSearchTerm: (term: string) => void;
setSelectedProduct: (product: Product | null) => void;
handleSearch: (e: React.FormEvent) => Promise<void>;
handleOpportunitySearch: (category: string) => Promise<void>;
addToShoppingList: (product: Product) => void;
removeFromShoppingList: (id: string) => void;
updateShoppingItemQuantity: (id: string, delta: number) => void;
updateOrderStatus: (orderId: string, newStatus: OrderStatus) => void;
saveOrderAsQuotation: () => void;
resumeOrder: (orderId: string) => void;
deleteOrder: (orderId: string) => Promise<void>;
calculateShoppingTotals: () => {
totalParaguayBRL: number;
totalCostWithOverhead: number;
totalUSD: number;
totalApproxProfit: number;
};
// New Actions
addTransaction: (tx: Omit<Transaction, 'id'>) => Promise<void>;
updateTransaction: (id: string, updates: Partial<Transaction>) => Promise<void>;
deleteTransaction: (id: string) => Promise<void>;
addCustomer: (customer: Omit<Customer, 'id' | 'totalPurchased'>) => Promise<Customer | null>; // Updated return type
updateCustomer: (id: string, updates: Partial<Customer>) => Promise<void>;
deleteCustomer: (id: string) => void;
getFinancialSummary: () => FinancialSummary;
// Sales Management
sales: Sale[];
importSales: (channel: SalesChannel) => Promise<void>;
updateSale: (id: string, updates: Partial<Sale>) => void;
// Supplier Actions
addSupplier: (supplier: Omit<Supplier, 'id'>) => void;
updateSupplier: (id: string, updates: Partial<Supplier>) => void;
deleteSupplier: (id: string) => void;
registerSale: (items: { id: string, quantity: number, salePrice: number }[], customerId?: string, paymentMethod?: PaymentMethod) => Promise<void>;
// Inventory Management
addProduct: (product: Omit<InventoryItem, 'id'>) => Promise<void>;
updateProduct: (id: string, updates: Partial<InventoryItem>) => Promise<void>;
deleteProduct: (id: string) => Promise<void>;
// Settings
settings: any; // We will define a stronger type later if needed
updateSettings: (newSettings: any) => Promise<void>;
}
const CRMContext = createContext<CRMContextType | undefined>(undefined);
export const CRMProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
// --- AUTH STATE ---
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [authLoading, setAuthLoading] = useState(true);
const [isAdmin, setIsAdmin] = useState(false); // New State
// --- APP STATE ---
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchLoading, setSearchLoading] = useState(false);
const [searchError, setSearchError] = useState<string | null>(null);
const [products, setProducts] = useState<Product[]>([]);
const [selectedProduct, setSelectedProduct] = useState<Product | null>(null);
const [searchType, setSearchType] = useState<'specific' | 'opportunity'>('specific');
const [shoppingList, setShoppingList] = useState<ShoppingListItem[]>([]);
const [activeOrderId, setActiveOrderId] = useState<string | null>(null);
const [useOverhead, setUseOverhead] = useState(true);
// SUPABASE DATA
const [orders, setOrders] = useState<Order[]>([]);
const [inventory, setInventory] = useState<InventoryItem[]>([]);
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [customers, setCustomers] = useState<Customer[]>([]);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [sales, setSales] = useState<Sale[]>([]);
const [settings, setSettings] = useState<any>({
defaultOverhead: 20,
defaultExchange: 5.65,
companyName: 'Arbitra System'
});
// MOCKED DATA
const [users, setUsers] = useState<User[]>([]);
const overheadPercent = settings.defaultOverhead || 20;
const exchangeRate = settings.defaultExchange || 5.65;
// --- AUTH & INITIAL LOAD ---
useEffect(() => {
const handleAuthSession = async (currentSession: Session | null) => {
setSession(currentSession);
if (currentSession?.user) {
// Fetch Role from profiles table
const { data: profile, error: profileError } = await supabase
.from('profiles')
.select('role')
.eq('id', currentSession.user.id)
.maybeSingle();
2026-01-26 14:20:25 +00:00
if (profileError) {
console.error("Error fetching user profile:", profileError);
}
const role = profile?.role || 'user';
setIsAdmin(role === 'admin');
setUser({
id: currentSession.user.id,
email: currentSession.user.email!,
name: currentSession.user.user_metadata?.full_name || currentSession.user.email!,
role: role as 'admin' | 'user',
status: 'Active', // Default or fetch from profile
avatar: currentSession.user.user_metadata?.avatar_url || "https://api.dicebear.com/7.x/avataaars/svg?seed=Felix"
});
fetchData();
} else {
setUser(null);
setIsAdmin(false);
// Optional: Clear state on logout
setOrders([]);
setInventory([]);
}
setAuthLoading(false);
};
// Check active session on mount
supabase.auth.getSession().then(({ data: { session } }) => {
handleAuthSession(session);
});
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
handleAuthSession(session);
});
return () => subscription.unsubscribe();
}, []);
const signIn = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({ email, password });
return { error };
};
const signOut = async () => {
await supabase.auth.signOut();
setUser(null);
setIsAdmin(false);
};
const updateUserRole = async (userId: string, role: 'admin' | 'user') => {
if (!isAdmin) {
alert("Você não tem permissão para atualizar funções de usuário.");
return;
}
const { error } = await supabase.from('profiles').update({ role }).eq('id', userId);
if (error) {
console.error("Error updating role:", error);
alert("Erro ao atualizar função do usuário.");
} else {
alert("Função atualizada com sucesso.");
// Update local state immediately
setUsers(prev => prev.map(u => u.id === userId ? { ...u, role } : u));
}
};
// --- SUPABASE FETCH ---
const fetchData = async () => {
setLoading(true);
try {
// Orders
const { data: ordersData } = await supabase.from('orders').select('*').order('created_at', { ascending: false });
if (ordersData) {
const parsedOrders = ordersData.map(o => ({
id: o.id,
date: o.date || o.created_at,
items: typeof o.items === 'string' ? JSON.parse(o.items) : (o.items || []),
totalUSD: o.total_usd || 0,
totalBRL: o.total_brl || 0,
totalCostWithOverhead: o.total_cost_with_overhead || 0,
estimatedProfit: o.estimated_profit || 0,
status: o.status,
supplierName: o.supplier_name || 'Desconhecido'
}));
setOrders(parsedOrders || []);
}
// Inventory
const { data: inventoryData } = await supabase.from('inventory').select('*').order('created_at', { ascending: false });
if (inventoryData) {
setInventory(inventoryData.map((i: any) => ({
id: i.id,
name: i.name,
sku: i.sku,
ean: i.ean,
quantity: i.quantity,
avgCostBRL: i.avg_cost_brl || 0,
marketPriceBRL: i.market_price_brl || 0,
lastSupplier: i.last_supplier
})));
}
// Users (Admin Only) - Moved logic inside check
if (isAdmin) {
const { data: usersData } = await supabase.from('profiles').select('*').order('created_at', { ascending: false });
if (usersData) {
setUsers(usersData.map((u: any) => ({
id: u.id,
name: u.full_name || u.email, // Fallback if full_name is empty
email: u.email,
role: u.role,
status: u.status,
avatar: u.avatar_url || `https://api.dicebear.com/7.x/initials/svg?seed=${u.email}`,
lastAccess: u.last_access
})));
}
}
// Settings
const { data: settingsData } = await supabase.from('settings').select('*').single();
if (settingsData) {
setSettings({
companyName: settingsData.company_name,
cnpj: settingsData.cnpj,
ie: settingsData.ie,
defaultOverhead: settingsData.default_overhead,
defaultExchange: settingsData.default_exchange,
geminiKey: settingsData.gemini_key,
melhorEnvioToken: settingsData.melhor_envio_token,
blingToken: settingsData.bling_token,
tinyToken: settingsData.tiny_token,
certificatePassword: settingsData.certificate_password,
nfeSerie: settingsData.nfe_serie,
nfeNumber: settingsData.nfe_number,
nfeEnvironment: settingsData.nfe_environment,
smtpHost: settingsData.smtp_host,
smtpPort: settingsData.smtp_port,
smtpUser: settingsData.smtp_user,
smtpPass: settingsData.smtp_pass,
autoSyncSales: settingsData.auto_sync_sales,
autoSyncStock: settingsData.auto_sync_stock,
sourcingWebhook: settingsData.sourcing_webhook,
electricityCostKwh: settingsData.electricity_cost_kwh,
2026-01-26 14:20:25 +00:00
});
}
} catch (err) {
console.error("Error fetching data:", err);
} finally {
setLoading(false);
}
};
const updateSettings = async (newSettings: any) => {
// Determine if we need to insert or update (assuming single row for settings for now per user or global)
// For simplicity, we'll try to update the first row found, or insert if empty.
const dbSettings = {
company_name: newSettings.companyName,
cnpj: newSettings.cnpj,
ie: newSettings.ie,
default_overhead: newSettings.defaultOverhead,
default_exchange: newSettings.defaultExchange,
gemini_key: newSettings.geminiKey,
melhor_envio_token: newSettings.melhorEnvioToken,
bling_token: newSettings.blingToken,
tiny_token: newSettings.tinyToken,
certificate_password: newSettings.certificatePassword,
nfe_serie: newSettings.nfeSerie,
nfe_number: newSettings.nfeNumber,
nfe_environment: newSettings.nfeEnvironment,
smtp_host: newSettings.smtpHost,
smtp_port: newSettings.smtpPort,
smtp_user: newSettings.smtpUser,
smtp_pass: newSettings.smtpPass,
auto_sync_sales: newSettings.autoSyncSales,
auto_sync_stock: newSettings.autoSyncStock,
2026-01-26 14:20:25 +00:00
};
// Check if settings exist
const { data } = await supabase.from('settings').select('id').single();
if (data) {
// Update
await supabase.from('settings').update(dbSettings).eq('id', data.id);
} else {
// Insert
await supabase.from('settings').insert([dbSettings]);
}
setSettings(newSettings);
};
// --- ACTIONS ---
const handleSearch = async (e: React.FormEvent) => {
e.preventDefault();
if (!searchTerm.trim()) return;
setSearchLoading(true);
setSearchError(null);
setSelectedProduct(null);
setProducts([]); // Clear previous results
try {
const { products: result } = await searchProducts(searchTerm, exchangeRate);
2026-01-26 14:20:25 +00:00
setProducts(result);
if (result.length === 0) {
setSearchError("Não encontramos nenhum produto com esse nome. Tente termos mais gerais (ex: 'iPhone' em vez de 'iPhone 15 Pro Max 256GB Azul').");
}
} catch (err: any) {
console.error(err);
if (err.message && err.message.includes("VITE_GOOGLE_API_KEY")) {
setSearchError("Erro de Configuração: API Key do Google não encontrada. Verifique o arquivo .env");
} else if (err.message && (err.message.includes("fetch") || err.message.includes("network"))) {
setSearchError("Erro de Conexão: Não foi possível conectar ao servidor. Verifique sua internet.");
} else {
setSearchError("Ocorreu um erro ao consultar a inteligência artificial. Por favor, tente novamente em alguns instantes.");
}
} finally {
setSearchLoading(false);
}
};
const handleOpportunitySearch = async (category: string) => {
if (!category.trim()) return;
setSearchLoading(true);
setSearchError(null);
setSelectedProduct(null);
setProducts([]); // Clear previous results
try {
const { searchOpportunities } = await import('../services/geminiService');
const { products: result } = await searchOpportunities(category, useOverhead, exchangeRate);
2026-01-26 14:20:25 +00:00
setProducts(result);
if (result.length === 0) {
setSearchError("Nenhuma oportunidade encontrada com >25% de margem nesta categoria no momento. Tente outra categoria.");
}
} catch (err: any) {
console.error(err);
if (err.message && err.message.includes("VITE_GOOGLE_API_KEY")) {
setSearchError("Erro de Configuração: API Key do Google não encontrada. Verifique o arquivo .env");
} else if (err.message && (err.message.includes("fetch") || err.message.includes("network"))) {
setSearchError("Erro de Conexão: Não foi possível conectar ao servidor. Verifique sua internet.");
} else {
setSearchError("A IA demorou muito para responder ou encontrou um erro. Tente novamente, geralmente funciona na segunda tentativa.");
}
} finally {
setSearchLoading(false);
}
};
const addToShoppingList = (p: Product) => {
setShoppingList(prev => {
const existing = prev.find(item => item.name === p.name && item.store === p.store);
if (existing) return prev.map(item => item.id === existing.id ? { ...item, quantity: item.quantity + 1 } : item);
return [...prev, {
id: crypto.randomUUID(),
name: p.name,
store: p.store,
priceUSD: p.priceUSD,
priceBRL: p.priceBRL,
quantity: 1,
marketPriceBRL: p.marketPriceBRL || (p.priceBRL * 1.6)
}];
});
};
const removeFromShoppingList = (id: string) => {
setShoppingList(prev => prev.filter(i => i.id !== id));
};
const updateShoppingItemQuantity = (id: string, delta: number) => {
setShoppingList(prev => prev.map(i => i.id === id ? { ...i, quantity: Math.max(1, i.quantity + delta) } : i));
};
const updateOrderStatus = async (orderId: string, newStatus: OrderStatus) => {
const order = orders.find(o => o.id === orderId);
if (!order) return;
// Database Update
await supabase.from('orders').update({ status: newStatus }).eq('id', orderId);
// Inventory Logic if Received
if (newStatus === 'Received' && order.status !== 'Received') {
const newInventoryItems: InventoryItem[] = order.items.map(item => ({
id: crypto.randomUUID(),
name: item.name,
sku: `SKU-${Math.random().toString(36).substr(2, 4).toUpperCase()}`,
quantity: item.quantity,
avgCostBRL: item.priceBRL * (1 + overheadPercent / 100),
marketPriceBRL: item.marketPriceBRL,
lastSupplier: item.store
}));
await supabase.from('inventory').insert(newInventoryItems.map(i => ({
id: i.id, name: i.name, sku: i.sku, quantity: i.quantity,
avg_cost_brl: i.avgCostBRL, market_price_brl: i.marketPriceBRL, last_supplier: i.lastSupplier
})));
// Refresh Inventory
const { data } = await supabase.from('inventory').select('*').order('created_at', { ascending: false });
if (data) setInventory(data);
}
// Local Update
setOrders(prev => prev.map(o => o.id === orderId ? { ...o, status: newStatus } : o));
};
const deleteOrder = async (orderId: string) => {
// Database Delete
const { error } = await supabase.from('orders').delete().eq('id', orderId);
if (error) {
console.error("Error deleting order:", error);
alert("Erro ao excluir pedido.");
} else {
// Local Update
setOrders(prev => prev.filter(o => o.id !== orderId));
}
};
const calculateShoppingTotals = () => {
const totalParaguayBRL = shoppingList.reduce((acc, item) => acc + (item.priceBRL * item.quantity), 0);
const overheadMultiplier = useOverhead ? (1 + overheadPercent / 100) : 1;
const totalCostWithOverhead = totalParaguayBRL * overheadMultiplier;
const totalUSD = shoppingList.reduce((acc, item) => acc + (item.priceUSD * item.quantity), 0);
const totalApproxProfit = shoppingList.reduce((acc, item) => {
const costWithOverhead = item.priceBRL * overheadMultiplier;
const profitPerUnit = item.marketPriceBRL - costWithOverhead;
return acc + (profitPerUnit * item.quantity);
}, 0);
return { totalParaguayBRL, totalCostWithOverhead, totalUSD, totalApproxProfit };
};
const resumeOrder = (orderId: string) => {
const order = orders.find(o => o.id === orderId);
if (!order) return;
setShoppingList(order.items);
setActiveOrderId(order.id);
};
const saveOrderAsQuotation = async () => {
if (shoppingList.length === 0) return;
const totals = calculateShoppingTotals();
if (activeOrderId) {
// Update
const updatePayload = {
items: shoppingList, // Helper handles jsonb
total_usd: totals.totalUSD,
total_brl: totals.totalParaguayBRL,
total_cost_with_overhead: totals.totalCostWithOverhead,
estimated_profit: totals.totalApproxProfit,
supplier_name: shoppingList[0]?.store || 'Múltiplos'
};
await supabase.from('orders').update(updatePayload).eq('id', activeOrderId);
setOrders(prev => prev.map(o => o.id === activeOrderId ? {
...o, items: shoppingList, totalUSD: totals.totalUSD, totalBRL: totals.totalParaguayBRL,
totalCostWithOverhead: totals.totalCostWithOverhead, estimatedProfit: totals.totalApproxProfit,
supplierName: updatePayload.supplier_name
} : o));
setActiveOrderId(null);
} else {
// Create
const newOrder: Order = {
id: crypto.randomUUID(),
date: new Date().toISOString(),
items: [...shoppingList],
totalUSD: totals.totalUSD,
totalBRL: totals.totalParaguayBRL,
totalCostWithOverhead: totals.totalCostWithOverhead,
estimatedProfit: totals.totalApproxProfit,
status: 'Pending',
supplierName: shoppingList[0]?.store || 'Múltiplos'
};
// Map to DB columns
const dbOrder = {
id: newOrder.id,
date: newOrder.date,
status: newOrder.status,
total_usd: newOrder.totalUSD,
total_brl: newOrder.totalBRL,
total_cost_with_overhead: newOrder.totalCostWithOverhead,
estimated_profit: newOrder.estimatedProfit,
items: newOrder.items,
supplier_name: newOrder.supplierName
};
const { error } = await supabase.from('orders').insert([dbOrder]);
if (error) {
console.error("Error saving order:", error);
alert(`Erro ao salvar pedido: ${error.message} (${error.details || ''})`);
return;
}
setOrders([newOrder, ...orders]);
alert("Pedido salvo com sucesso!");
}
setShoppingList([]);
};
// --- FINANCIAL ACTIONS ---
const addTransaction = async (tx: Omit<Transaction, 'id'>) => {
const id = crypto.randomUUID();
const newTx = {
...tx,
id,
user_id: user?.id,
status: tx.status || 'Pending',
payment_method: tx.paymentMethod || 'Cash',
due_date: tx.dueDate || new Date().toISOString()
};
// Map camelCase to snake_case for DB if needed, but Supabase JS client handles it if keys match DB columns
// or if we map explicitly. Let's map explicitly to be safe.
const dbTx = {
id: newTx.id,
date: newTx.date,
type: newTx.type,
category: newTx.category,
amount: newTx.amount,
description: newTx.description,
status: newTx.status,
payment_method: newTx.paymentMethod, // DB column: payment_method
due_date: newTx.dueDate, // DB column: due_date
user_id: newTx.user_id
};
const { error } = await supabase.from('transactions').insert([dbTx]);
if (error) console.error("Error adding transaction:", error);
// We update local state with the camelCase version for UI
setTransactions(prev => [{ ...tx, id: newTx.id, status: newTx.status, paymentMethod: newTx.paymentMethod, dueDate: newTx.dueDate }, ...prev]);
};
const updateTransaction = async (id: string, updates: Partial<Transaction>) => {
// Map updates
const dbUpdates: any = { ...updates };
if (updates.paymentMethod) dbUpdates.payment_method = updates.paymentMethod;
if (updates.dueDate) dbUpdates.due_date = updates.dueDate;
const { error } = await supabase.from('transactions').update(dbUpdates).eq('id', id);
if (error) {
console.error("Error updating transaction:", error);
alert("Erro ao atualizar transação");
} else {
setTransactions(prev => prev.map(t => t.id === id ? { ...t, ...updates } : t));
}
};
const deleteTransaction = async (id: string) => {
await supabase.from('transactions').delete().eq('id', id);
setTransactions(prev => prev.filter(t => t.id !== id));
};
const addCustomer = async (customer: Omit<Customer, 'id' | 'totalPurchased'>): Promise<Customer | null> => {
const id = crypto.randomUUID();
const newCustomer = { ...customer, id, totalPurchased: 0 };
const dbCustomer = {
id: newCustomer.id,
name: newCustomer.name,
email: newCustomer.email,
phone: newCustomer.phone,
city: newCustomer.city,
status: newCustomer.status,
total_purchased: 0,
user_id: user?.id
};
const { data, error } = await supabase.from('customers').insert([dbCustomer]).select();
if (error) {
console.error("Error adding customer:", error);
return null;
}
setCustomers(prev => [...prev, newCustomer]);
return newCustomer; // Return the object
};
const updateCustomer = async (id: string, updates: Partial<Customer>) => {
// Map updates to snake_case if necessary, or pass mostly as is if Keys match
const dbUpdates: any = { ...updates };
if (updates.totalPurchased) dbUpdates.total_purchased = updates.totalPurchased;
await supabase.from('customers').update(dbUpdates).eq('id', id);
setCustomers(prev => prev.map(c => c.id === id ? { ...c, ...updates } : c));
};
const deleteCustomer = async (id: string) => {
await supabase.from('customers').delete().eq('id', id);
setCustomers(prev => prev.filter(c => c.id !== id));
};
const getFinancialSummary = () => {
const totalIncome = transactions.filter(t => t.type === 'Income').reduce((acc, t) => acc + t.amount, 0);
const totalExpense = transactions.filter(t => t.type === 'Expense').reduce((acc, t) => acc + t.amount, 0);
return {
totalIncome,
totalExpense,
balance: totalIncome - totalExpense,
recentTransactions: transactions.slice(0, 5)
};
};
// --- SUPPLIER ACTIONS ---
const addSupplier = async (supplier: Omit<Supplier, 'id'>) => {
const id = crypto.randomUUID();
const newSupplier = { ...supplier, id, user_id: user?.id };
await supabase.from('suppliers').insert([newSupplier]);
setSuppliers(prev => [...prev, newSupplier]);
};
const updateSupplier = async (id: string, updates: Partial<Supplier>) => {
await supabase.from('suppliers').update(updates).eq('id', id);
setSuppliers(prev => prev.map(s => s.id === id ? { ...s, ...updates } : s));
};
const deleteSupplier = async (id: string) => {
await supabase.from('suppliers').delete().eq('id', id);
setSuppliers(prev => prev.filter(s => s.id !== id));
};
// PRODUCT ACTIONS
const addProduct = async (product: Omit<InventoryItem, 'id'>) => {
if (!user) {
alert("Erro: Usuário não autenticado. O sistema perdeu a conexão ou você não está logado.");
return;
}
console.log("Attempting to add product:", product);
const id = crypto.randomUUID();
const newProduct = { ...product, id };
// In a real app, 'products' and 'inventory' might be separate tables.
// Here we treat 'inventory' as the master product list for simplicity as per current structure.
const { data, error } = await supabase.from('inventory').insert([{
id: newProduct.id,
name: newProduct.name,
sku: newProduct.sku,
ean: newProduct.ean,
quantity: newProduct.quantity,
avg_cost_brl: newProduct.avgCostBRL,
market_price_brl: newProduct.marketPriceBRL,
last_supplier: newProduct.lastSupplier,
user_id: user?.id
}]).select();
if (error) {
console.error("Supabase Error adding product:", error);
throw error;
}
console.log("Product added successfully, DB response:", data);
setInventory(prev => [newProduct, ...prev]);
};
const updateProduct = async (id: string, updates: Partial<InventoryItem>) => {
// Map to snake_case for DB
const dbUpdates: any = { ...updates };
if (updates.avgCostBRL) dbUpdates.avg_cost_brl = updates.avgCostBRL;
if (updates.marketPriceBRL) dbUpdates.market_price_brl = updates.marketPriceBRL;
if (updates.lastSupplier) dbUpdates.last_supplier = updates.lastSupplier;
await supabase.from('inventory').update(dbUpdates).eq('id', id);
setInventory(prev => prev.map(p => p.id === id ? { ...p, ...updates } : p));
};
const deleteProduct = async (id: string) => {
await supabase.from('inventory').delete().eq('id', id);
setInventory(prev => prev.filter(p => p.id !== id));
};
// SALES LOGIC & MANAGEMENT
const importSales = async (channel: SalesChannel) => {
setLoading(true);
// MOCK DELAY & DATA
await new Promise(r => setTimeout(r, 1500));
const newSales: Sale[] = Array.from({ length: Math.floor(Math.random() * 3) + 2 }).map((_, i) => ({
id: crypto.randomUUID(),
date: new Date().toISOString(),
customerName: `Cliente ${channel} ${Math.floor(Math.random() * 1000)}`,
items: [
{ id: 'mock-item', name: 'Produto Importado ' + (i + 1), quantity: 1, salePrice: Math.random() * 200 + 50 }
],
total: 0, // Calculated below
status: 'Pending' as const,
channel: channel,
externalId: `MLB-${Math.floor(Math.random() * 1000000)}`,
isStockLaunched: false,
isFinancialLaunched: false
})).map(s => ({ ...s, total: s.items.reduce((acc, i) => acc + (i.quantity * i.salePrice), 0) }));
setSales(prev => [...newSales, ...prev]); // Add to top
setLoading(false);
};
const updateSale = (id: string, updates: Partial<Sale>) => {
setSales(prev => prev.map(s => {
if (s.id !== id) return s;
const updatedSale = { ...s, ...updates };
// Sincronização Automática (Mock logic linking checkboxes to actual actions)
if (updates.isStockLaunched && !s.isStockLaunched) {
// Trigger stock deduction logic theoretically
console.log("Stock launched for sale", id);
}
if (updates.isFinancialLaunched && !s.isFinancialLaunched) {
addTransaction({
date: new Date().toISOString(),
type: 'Income',
category: 'Venda Marketplace',
amount: updatedSale.total,
description: `Venda ${updatedSale.channel} #${updatedSale.externalId || id}`,
status: 'Paid',
paymentMethod: 'Other'
});
}
return updatedSale;
}));
};
const registerSale = async (items: { id: string, quantity: number, salePrice: number }[], customerId?: string, paymentMethod: PaymentMethod = 'Cash') => {
// 1. Deduct from Inventory
const updatedInventory = inventory.map(invItem => {
const soldItem = items.find(i => i.id === invItem.id);
if (soldItem) {
return { ...invItem, quantity: Math.max(0, invItem.quantity - soldItem.quantity) };
}
return invItem;
});
setInventory(updatedInventory);
// 2. Add Transaction (Income)
const totalAmount = items.reduce((acc, item) => acc + (item.quantity * item.salePrice), 0);
const cust = customers.find(c => c.id === customerId);
const transaction: Omit<Transaction, 'id'> = {
date: new Date().toISOString(),
type: 'Income',
category: 'Venda de Produtos',
amount: totalAmount,
description: `Venda balcão - Cliente: ${cust?.name || 'Não Id.'}`,
status: 'Paid',
paymentMethod: paymentMethod, // Now correctly using the argument
dueDate: new Date().toISOString()
};
await addTransaction(transaction); // This is good, but Sales.tsx is calling registerSale which handles logic.
// ACTUALLY: registerSale needs to accept payment method or we need to update the transaction logic inside it.
// Modifying registerSale signature to accept PaymentMethod would be best practice.
// 3. Register as a Sale Record
const newSale: Sale = {
id: crypto.randomUUID(),
date: new Date().toISOString(),
customerId: customerId,
customerName: cust ? cust.name : 'Cliente Balcão',
items: items.map(i => {
const inv = inventory.find(inv => inv.id === i.id);
return {
id: i.id,
name: inv?.name || 'Item',
quantity: i.quantity,
salePrice: i.salePrice,
costPrice: inv?.avgCostBRL || 0 // Capture cost at moment of sale
};
}),
total: totalAmount,
status: 'Completed',
channel: 'Local',
isStockLaunched: true, // Auto-launched for POS
isFinancialLaunched: true // Auto-launched for POS
};
setSales(prev => [newSale, ...prev]);
// 4. Update Customer (if exists) - Mocked for now
if (customerId) {
console.log(`Updated customer ${customerId} total purchased`);
}
};
const value = {
// Auth
user, session, authLoading, signIn, signOut, isAdmin, updateUserRole,
// Data
products, shoppingList, orders, inventory, suppliers, users, searchTerm, loading, error, selectedProduct, overheadPercent, exchangeRate, activeOrderId,
searchLoading, searchError, searchType, setSearchType,
customers, transactions,
setSearchTerm, setSelectedProduct, handleSearch, handleOpportunitySearch, addToShoppingList, removeFromShoppingList, updateShoppingItemQuantity, updateOrderStatus, saveOrderAsQuotation, calculateShoppingTotals, resumeOrder, deleteOrder,
addTransaction, deleteTransaction, updateTransaction, addCustomer, updateCustomer, deleteCustomer, getFinancialSummary,
addSupplier, updateSupplier, deleteSupplier, registerSale,
addProduct, updateProduct, deleteProduct, settings, updateSettings,
sales, importSales, updateSale,
useOverhead, setUseOverhead
};
return <CRMContext.Provider value={value}>{children}</CRMContext.Provider>;
};
export const useCRM = () => {
const context = useContext(CRMContext);
if (!context) throw new Error('useCRM must be used within a CRMProvider');
return context;
};