From cc0b4daa2ccac17a8edb659f0569e0dbbcdd425f Mon Sep 17 00:00:00 2001 From: Rei Date: Fri, 4 Jul 2025 16:04:08 -0400 Subject: [PATCH 1/2] Rework modelos y codigo --- CoffeeUrbanTech/.gitignore | 2 + CoffeeUrbanTech/app/firebaseData.tsx | 47 +++ .../app/frontend/screens/DashboardScreen.tsx | 330 ++++++------------ CoffeeUrbanTech/firebaseConfigWeb.js | 85 ----- 4 files changed, 159 insertions(+), 305 deletions(-) create mode 100644 CoffeeUrbanTech/app/firebaseData.tsx delete mode 100644 CoffeeUrbanTech/firebaseConfigWeb.js diff --git a/CoffeeUrbanTech/.gitignore b/CoffeeUrbanTech/.gitignore index f610ec0..7e2b58f 100644 --- a/CoffeeUrbanTech/.gitignore +++ b/CoffeeUrbanTech/.gitignore @@ -36,4 +36,6 @@ yarn-error.* # typescript *.tsbuildinfo +# firebase info +firebaseConfigWeb.js app-example diff --git a/CoffeeUrbanTech/app/firebaseData.tsx b/CoffeeUrbanTech/app/firebaseData.tsx new file mode 100644 index 0000000..ab2a40d --- /dev/null +++ b/CoffeeUrbanTech/app/firebaseData.tsx @@ -0,0 +1,47 @@ +// Modelos de Datos +import { Timestamp } from 'firebase/firestore'; + +export type Product = { + id: string; + name: string; + description: string; + price: number; // precio_venta + unitCost: number; // costo -> unitCost + stock: number; + category: string; + productType: string; + suggestedPrice?: number; + inMenu?: boolean; + createdAt?: Timestamp; // fecha_creacion + updatedAt?: Timestamp; // ultima_actualizacion +}; + +export type PurchasedItem = { + productId: string; + productName: string; + quantity: number; + unitCost: number; // costo_unitario +}; + +export type Purchase= { + id: string; + purchaseDate: Timestamp; // fecha_compra/date + supplierName: string; + items: PurchasedItem[]; + total: number; +}; + +export type SoldItem = { + productId: string; + productName: string; + price: number; + quantity: number; + subtotal: number; +}; + +export type Sale = { + id: string; + date: Timestamp; + items: SoldItem[]; + total: number; +}; \ No newline at end of file diff --git a/CoffeeUrbanTech/app/frontend/screens/DashboardScreen.tsx b/CoffeeUrbanTech/app/frontend/screens/DashboardScreen.tsx index 55e4e9b..9343db7 100644 --- a/CoffeeUrbanTech/app/frontend/screens/DashboardScreen.tsx +++ b/CoffeeUrbanTech/app/frontend/screens/DashboardScreen.tsx @@ -1,53 +1,8 @@ +import { collection, getDocs, orderBy, query, Timestamp, where } from 'firebase/firestore'; import React, { useEffect, useState } from "react"; -import { ActivityIndicator, ScrollView, StyleSheet, Text, View } from "react-native"; -import { firestoreDb } from '../../../firebaseConfigWeb'; - -// Modelos de Datos -type Producto = { - id: string; - categoria: string; - costo_unitario: number; - descripcion: string; - en_menu: boolean; - fecha_creacion: any; - nombre: string; - precio_sugerido: number; - precio_venta: number; - stock: number; - tipo_producto: string; - ultima_actualizacion: any; -}; - -type ItemComprado = { - cantidad: number; - costo_unitario: number; - id_producto: string; - nombre_producto: string; -}; - -type Compra = { - id: string; - fecha_compra: any; - id_proveedor: string; - nombre_proveedor: string; - productos_comprados: ItemComprado[]; - total_compra: number; -}; - -type ItemVendido = { - cantidad: number; - id_producto: string; - nombre_producto: string; - precio_venta: number; -}; - -type Venta = { - id: string; - estado: string; - fecha_venta: any; - productos_vendidos: ItemVendido[]; - total_venta: number; -}; +import { ActivityIndicator, ScrollView, Text, View } from "react-native"; +import { db } from '../../../firebaseConfigWeb.js'; +import { Product, Purchase, Sale } from './../../firebaseData.js'; type Activity = { type: "sale" | "purchase"; @@ -56,174 +11,144 @@ type Activity = { amount: number; }; - export default function DashboardScreen() { - const [ventas, setVentas] = useState([]); - const [compras, setCompras] = useState([]); - const [productos, setProductos] = useState([]); + const [firebaseSales, setFirebaseSales] = useState([]); + const [firebasePurchases, setFirebasePurchases] = useState([]); + const [firebaseProducts, setFirebaseProducts] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [recentActivity, setRecentActivity] = useState([]); useEffect(() => { - //Carga de 'Venta' - const ventasSubscriber = firestoreDb - .collection('Venta') - .onSnapshot(querySnapshot => { - const fetchedVentas: Venta[] = []; - querySnapshot.forEach(documentSnapshot => { - const data = documentSnapshot.data(); - fetchedVentas.push({ - id: documentSnapshot.id, - estado: data.estado || '', - fecha_venta: data.fecha_venta ? data.fecha_venta.toDate() : new Date(), - productos_vendidos: data.productos_vendidos || [], - total_venta: data.total_venta || 0, - }); + const fetchDashboardData = async () => { + try { + setLoading(true); + setError(null); + + //Productos + const productsSnapshot = await getDocs(collection(db, "products")); + const productsList: Product[] = productsSnapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + name: data.name || '', + description: data.description || '', + price: data.price || 0, + unitCost: data.unitCost || 0, + stock: data.stock || 0, + category: data.category || '', + productType: data.productType || '', + suggestedPrice: data.suggestedPrice || null, + inMenu: data.inMenu || false, + createdAt: data.createdAt instanceof Timestamp ? data.createdAt : null, + updatedAt: data.updatedAt instanceof Timestamp ? data.updatedAt : null, + } as Product; // Casteo final }); - setVentas(fetchedVentas); - checkAllLoaded(); - }, err => { - console.error("Error:", err); - setError("No se cargaron las ventas."); - setLoading(false); - }); - - // Carga de 'Compra' - const comprasSubscriber = firestoreDb - .collection('Compra') - .onSnapshot(querySnapshot => { - const fetchedCompras: Compra[] = []; - querySnapshot.forEach(documentSnapshot => { - const data = documentSnapshot.data(); - fetchedCompras.push({ - id: documentSnapshot.id, - fecha_compra: data.fecha_compra ? data.fecha_compra.toDate() : new Date(), - id_proveedor: data.id_proveedor || '', - nombre_proveedor: data.nombre_proveedor || 'Desconocido', - productos_comprados: data.productos_comprados || [], - total_compra: data.total_compra || 0, - }); + setFirebaseProducts(productsList); + + // Ventas + const thirtyDaysAgo = Timestamp.fromDate(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)); + const salesQuery = query( + collection(db, "sales"), + where("date", ">=", thirtyDaysAgo), + orderBy("date", "desc") + ); + const salesSnapshot = await getDocs(salesQuery); + const salesList: Sale[] = salesSnapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + date: data.date instanceof Timestamp ? data.date : Timestamp.fromDate(new Date()), + items: data.items || [], + total: data.total || 0, + } as Sale; }); - setCompras(fetchedCompras); - checkAllLoaded(); - }, err => { - console.error("Error:", err); - setError("No se cargaron las compras."); - setLoading(false); - }); - - // Cargar 'Producto' - const productosSubscriber = firestoreDb - .collection('Producto') - .onSnapshot(querySnapshot => { - const fetchedProductos: Producto[] = []; - querySnapshot.forEach(documentSnapshot => { - const data = documentSnapshot.data(); - fetchedProductos.push({ - id: documentSnapshot.id, - categoria: data.categoria || '', - costo_unitario: data.costo_unitario || 0, - descripcion: data.descripcion || '', - en_menu: data.en_menu || false, - fecha_creacion: data.fecha_creacion ? data.fecha_creacion.toDate() : new Date(), - nombre: data.nombre || 'Sin Nombre', - precio_sugerido: data.precio_sugerido || 0, - precio_venta: data.precio_venta || 0, - stock: data.stock || 0, - tipo_producto: data.tipo_producto || '', - ultima_actualizacion: data.ultima_actualizacion ? data.ultima_actualizacion.toDate() : new Date(), - }); + setFirebaseSales(salesList); + + // Compras + const purchasesQuery = query( + collection(db, "purchases"), + where("purchaseDate", ">=", thirtyDaysAgo), + orderBy("purchaseDate", "desc") + ); + const purchasesSnapshot = await getDocs(purchasesQuery); + const purchasesList: Purchase[] = purchasesSnapshot.docs.map(doc => { + const data = doc.data(); + return { + id: doc.id, + purchaseDate: data.purchaseDate instanceof Timestamp ? data.purchaseDate : Timestamp.fromDate(new Date()), + supplierName: data.supplierName || 'N/A', + items: data.items || [], + total: data.total || 0, + } as Purchase; }); - setProductos(fetchedProductos); - checkAllLoaded(); - }, err => { - console.error("Error :", err); - setError("No se cargaron los productos."); - setLoading(false); - }); - - // Revisa todas las colecciones carguen - let loadedCount = 0; - const totalCollections = 3; // Compra+Venta+Producto + setFirebasePurchases(purchasesList); - const checkAllLoaded = () => { - loadedCount++; - if (loadedCount >= totalCollections) { + } catch (err: any) { + setError("Error: " + err.message); + } finally { setLoading(false); } }; - // Limpiar suscripciones al desmontar el componente - return () => { - ventasSubscriber(); - comprasSubscriber(); - productosSubscriber(); - }; - }, []); // Se ejecuta una sola vez al montar el componente + fetchDashboardData(); + }, []); + - // Actividad Reciente useEffect(() => { - if (ventas.length > 0 || compras.length > 0) { - const allActivity = [ - ...ventas.map((venta) => ({ - type: "sale" as const, - date: venta.fecha_venta.toISOString(), - description: `Venta #${venta.id.substring(0, 4)} - $${venta.total_venta.toFixed(2)}`, - amount: venta.total_venta, - })), - ...compras.map((compra) => ({ - type: "purchase" as const, - date: compra.fecha_compra.toISOString(), - description: `Compra de ${compra.productos_comprados.length > 0 ? compra.productos_comprados[0].nombre_producto : 'Múltiples'} - $${compra.total_compra.toFixed(2)}`, - amount: compra.total_compra, - })), - ] + if (!loading && !error) { + const allActivity = [ + ...firebaseSales.map((sale) => ({ + type: "sale" as const, + date: sale.date instanceof Timestamp ? sale.date.toDate().toISOString() : new Date().toISOString(), + description: `Venta #${sale.id || 'N/A'} - $${sale.total.toFixed(2)}`, + amount: sale.total, + })), + ...firebasePurchases.map((purchase) => ({ + type: "purchase" as const, + date: purchase.purchaseDate instanceof Timestamp ? purchase.purchaseDate.toDate().toISOString() : new Date().toISOString(), + description: `Compra de ${purchase.items.map(item => item.productName).join(', ')} - $${purchase.total.toFixed(2)}`, + amount: purchase.total, + })), + ] .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) .slice(0, 5); - setRecentActivity(allActivity); + setRecentActivity(allActivity); } - }, [ventas, compras]); + }, [firebaseSales, firebasePurchases, loading, error]); - // Cálculos de dashboard - const today = new Date(); - const todayDateString = today.toDateString(); - const totalSalesToday = ventas.reduce((sum, venta) => { - const saleDate = venta.fecha_venta instanceof Date ? venta.fecha_venta : new Date(venta.fecha_venta); - return saleDate.toDateString() === todayDateString ? sum + venta.total_venta : sum; - }, 0); + const today = new Date().toDateString(); + const totalSalesToday = firebaseSales.filter( + (sale) => sale.date instanceof Timestamp && sale.date.toDate().toDateString() === today + ).reduce((sum, sale) => sum + sale.total, 0); - const currentMonth = today.getMonth(); - const monthPurchases = compras.filter((compra) => { - const purchaseDate = compra.fecha_compra instanceof Date ? compra.fecha_compra : new Date(compra.fecha_compra); - return purchaseDate.getMonth() === currentMonth && purchaseDate.getFullYear() === today.getFullYear(); - }); - const totalPurchasesMonth = monthPurchases.reduce( - (sum, purchase) => sum + purchase.total_compra, - 0 - ); + const currentMonth = new Date().getMonth(); + const totalPurchasesMonth = firebasePurchases.filter( + (purchase) => purchase.purchaseDate instanceof Timestamp && purchase.purchaseDate.toDate().getMonth() === currentMonth + ).reduce((sum, purchase) => sum + purchase.total, 0); + + const lowStockItems = firebaseProducts.filter((product) => product.stock < 10); - const lowStockItems = productos.filter((producto) => producto.stock < 10); - // Renderizado condicional basado en loading y error if (loading) { return ( - + - Cargando datos... + Cargando data... ); } if (error) { return ( - - ¡Error! - {error} - Revisar la conexión con la BD. + + Error de Carga + {error} + + Error en la carga de Firebase. + ); } @@ -239,7 +164,7 @@ export default function DashboardScreen() { - {productos.length} + {firebaseProducts.length} Productos @@ -300,39 +225,4 @@ export default function DashboardScreen() { ); -} - -const styles = StyleSheet.create({ - loadingContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: '#fff', - }, - loadingText: { - marginTop: 10, - fontSize: 16, - color: '#555', - }, - errorContainer: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - padding: 20, - backgroundColor: '#fee', - borderRadius: 8, - margin: 20, - }, - errorTextTitle: { - fontSize: 20, - fontWeight: 'bold', - color: '#d32f2f', - marginBottom: 10, - }, - errorText: { - fontSize: 16, - color: '#d32f2f', - textAlign: 'center', - marginBottom: 5, - }, -}); \ No newline at end of file +}; \ No newline at end of file diff --git a/CoffeeUrbanTech/firebaseConfigWeb.js b/CoffeeUrbanTech/firebaseConfigWeb.js deleted file mode 100644 index 2cc707b..0000000 --- a/CoffeeUrbanTech/firebaseConfigWeb.js +++ /dev/null @@ -1,85 +0,0 @@ -import firebase from 'firebase/compat/app'; // Para la inicialización -import 'firebase/compat/firestore'; // Si vas a usar Firestore - - -const firebaseConfig = { - apiKey: "AIzaSyD6ayt-f-HbYATmzEWejkYoEH1FJntyU2Q", - authDomain: "coffee-urban-tech.firebaseapp.com", - projectId: "coffee-urban-tech", - storageBucket: "coffee-urban-tech.firebasestorage.app", - messagingSenderId: "177052798498", - appId: "1:177052798498:web:801c609c415e68ff64c47b" -}; - - -// Inicializa Firebase SOLO SI NO HA SIDO INICIALIZADO AÚN -if (!firebase.apps.length) { - firebase.initializeApp(firebaseConfig); - console.log('✅ Firebase SDK Inicializado Correctamente para WEB.'); -} else { - console.log('✅ Firebase SDK ya estaba inicializado.'); -} - -// Exporta la instancia de Firestore para que puedas usarla en tus componentes -const firestoreDb = firebase.firestore(); - -export { firestoreDb }; - -/* -// Producto -type Producto = { - id: string; // ID de Firestore - categoria: string; - costo_unitario: number; - descripcion: string; - en_menu: boolean; // Si esta en Menu o no - fecha_creacion: Timestamp; - nombre: string; - precio_sugerido: number; //Si tipo_producto = "Menu", otro tipo_producto no tiene este campo - precio_venta: number; //Si tipo_producto = "Menu", otro tipo_producto no tiene este campo - stock: number; - tipo_producto: string; - ultima_actualizacion: Timestamp; -}; - -// Compra -type Compra = { - id: string; // ID de Firestore - fecha_compra: Timestamp; - id_proveedor: string; - nombre_proveedor: string; - productos_comprados[]: { // Array de objetos - cantidad: number; - costo_unitario: number; - id_producto: string; - nombre_producto: string; - }; - total_compra: number; -}; - -// Proveedor -type Proveedor = { - id: string; // ID de Firestore - direccion: string; - email: string; - fecha_creacion: Timestamp; - nombre: string; - nombre_contacto: string; - telefono: string; -}; - -// Venta -type Venta = { - id: string; // ID de Firestore - estado: string; //Si se proceso la transacción "Completado | Cancelado" - fecha_venta: Timestamp; - productos_vendidos[]: // Array de objetos - { - cantidad: number; - id_producto: string; - nombre_producto: string; - precio_venta: number; - }; - total_venta: number; -}; -*/ From 568b42572ffb5e4bfdbf43aafaa331531c765f1c Mon Sep 17 00:00:00 2001 From: Rei Date: Sun, 6 Jul 2025 10:19:33 -0400 Subject: [PATCH 2/2] Inventario + Compras integrado con Firestore (olvide el add) --- CoffeeUrbanTech/app/firebaseData.tsx | 8 +- .../app/frontend/components/PurchaseForm.tsx | 48 +- .../frontend/components/PurchaseHistory.tsx | 46 +- .../app/frontend/screens/ComprasScreen.tsx | 187 +++++--- .../app/frontend/screens/InventarioScreen.tsx | 412 ++++++++++++------ 5 files changed, 450 insertions(+), 251 deletions(-) diff --git a/CoffeeUrbanTech/app/firebaseData.tsx b/CoffeeUrbanTech/app/firebaseData.tsx index ab2a40d..8756cde 100644 --- a/CoffeeUrbanTech/app/firebaseData.tsx +++ b/CoffeeUrbanTech/app/firebaseData.tsx @@ -10,10 +10,10 @@ export type Product = { stock: number; category: string; productType: string; - suggestedPrice?: number; + suggestedPrice?: number | null; inMenu?: boolean; - createdAt?: Timestamp; // fecha_creacion - updatedAt?: Timestamp; // ultima_actualizacion + createdAt?: Timestamp | null; // fecha_creacion + updatedAt?: Timestamp | null; // ultima_actualizacion }; export type PurchasedItem = { @@ -23,7 +23,7 @@ export type PurchasedItem = { unitCost: number; // costo_unitario }; -export type Purchase= { +export type Purchase = { id: string; purchaseDate: Timestamp; // fecha_compra/date supplierName: string; diff --git a/CoffeeUrbanTech/app/frontend/components/PurchaseForm.tsx b/CoffeeUrbanTech/app/frontend/components/PurchaseForm.tsx index 5c3d181..5591ed5 100644 --- a/CoffeeUrbanTech/app/frontend/components/PurchaseForm.tsx +++ b/CoffeeUrbanTech/app/frontend/components/PurchaseForm.tsx @@ -1,21 +1,25 @@ -import React, { useState, useEffect } from "react"; -import { View, Text, TextInput, Button, Pressable } from "react-native"; import { Picker } from "@react-native-picker/picker"; +import React, { useEffect, useState } from "react"; +import { Pressable, Text, TextInput, View } from "react-native"; + +// Importa el tipo Product desde tu archivo de modelos de datos +import { Product } from '../../firebaseData'; type Props = { - products: { id: number; name: string; stock: number }[]; + // products ahora es un array de tu tipo Product de Firebase + products: Product[]; + // onAddPurchase espera un productId y supplierName, alineado con ComprasScreen onAddPurchase: (purchase: { - supplier: string; - product: string; + productId: string; quantity: number; - unitCost: number; - total: number; + supplierName?: string; + unitCost?: number; }) => void; }; export default function PurchaseForm({ products, onAddPurchase }: Props) { - const [supplier, setSupplier] = useState(""); - const [product, setProduct] = useState(""); + const [supplierName, setSupplierName] = useState(""); + const [selectedProductId, setSelectedProductId] = useState(""); const [quantity, setQuantity] = useState(""); const [unitCost, setUnitCost] = useState(""); const [total, setTotal] = useState(null); @@ -31,22 +35,22 @@ export default function PurchaseForm({ products, onAddPurchase }: Props) { }, [quantity, unitCost]); const handleSubmit = () => { - if (!product) return; + if (!selectedProductId) return; const q = parseInt(quantity); const u = parseFloat(unitCost); - if (q <= 0 || u <= 0) return; + if (isNaN(q) || isNaN(u) || q <= 0 || u <= 0) return; onAddPurchase({ - supplier, - product, + supplierName, + productId: selectedProductId, quantity: q, unitCost: u, - total: q * u, }); setQuantity(""); setUnitCost(""); - setProduct(""); + setSelectedProductId(""); + setSupplierName(""); setTotal(null); }; @@ -57,11 +61,11 @@ export default function PurchaseForm({ products, onAddPurchase }: Props) { {/* Proveedor */} - Compra + Proveedor @@ -71,14 +75,14 @@ export default function PurchaseForm({ products, onAddPurchase }: Props) { Producto setProduct(value)} + selectedValue={selectedProductId} + onValueChange={(itemValue) => setSelectedProductId(String(itemValue))} > {products.map((p) => ( ))} @@ -127,4 +131,4 @@ export default function PurchaseForm({ products, onAddPurchase }: Props) { ); -} +}; \ No newline at end of file diff --git a/CoffeeUrbanTech/app/frontend/components/PurchaseHistory.tsx b/CoffeeUrbanTech/app/frontend/components/PurchaseHistory.tsx index d88dfd5..55770bf 100644 --- a/CoffeeUrbanTech/app/frontend/components/PurchaseHistory.tsx +++ b/CoffeeUrbanTech/app/frontend/components/PurchaseHistory.tsx @@ -1,17 +1,11 @@ import React from "react"; -import { View, Text, Pressable } from "react-native"; +import { Pressable, Text, View } from "react-native"; + +import { Purchase } from '../../firebaseData'; // Ajusta la ruta si es necesario type Props = { - purchases: { - id: number; - supplier: string; - product: string; - quantity: number; - unitCost: number; - total: number; - date: string; - }[]; - onDelete: (id: number) => void; + purchases: Purchase[]; + onDelete: (id: string) => void; // Un comentario para tus compañeros sobre el cambio de tipo de ID }; export default function PurchaseHistory({ purchases, onDelete }: Props) { @@ -32,6 +26,7 @@ export default function PurchaseHistory({ purchases, onDelete }: Props) { ) : ( <> + {/* Ordenar por fecha para mostrar las más recientes primero (aunque ya lo hacemos en ComprasScreen) */} {purchases.map((p) => ( - Compra #{p.id} - - - Producto: {p.product} - - - Proveedor: {p.supplier} + Compra # {p.id.substring(0, 8)}... {/* Mostrar solo parte del ID para legibilidad */} - Cantidad: {p.quantity} + Proveedor: {p.supplierName} - - Costo Unitario: ${p.unitCost.toFixed(2)} + + Fecha: {p.purchaseDate.toDate().toLocaleString()} {/* Convierte Timestamp a Date */} - - Fecha: {new Date(p.date).toLocaleString()} + + {/* Mostrar los items comprados dentro de esta compra */} + + Productos: + {p.items.map((item, index) => ( + + • {item.productName} (x{item.quantity}) - ${item.unitCost.toFixed(2)} c/u + + ))} - + ${p.total.toFixed(2)} @@ -75,4 +71,4 @@ export default function PurchaseHistory({ purchases, onDelete }: Props) { )} ); -} +} \ No newline at end of file diff --git a/CoffeeUrbanTech/app/frontend/screens/ComprasScreen.tsx b/CoffeeUrbanTech/app/frontend/screens/ComprasScreen.tsx index 009a249..75b7179 100644 --- a/CoffeeUrbanTech/app/frontend/screens/ComprasScreen.tsx +++ b/CoffeeUrbanTech/app/frontend/screens/ComprasScreen.tsx @@ -1,30 +1,17 @@ -import React, { useState, useEffect } from "react"; -import { View, ScrollView } from "react-native"; +import React, { useEffect, useState } from "react"; +import { Alert, ScrollView, View } from "react-native"; import PurchaseForm from "../components/PurchaseForm"; import PurchaseHistory from "../components/PurchaseHistory"; +import { collection, doc, onSnapshot, orderBy, query, runTransaction, Timestamp } from "firebase/firestore"; +import { db } from '../../../firebaseConfigWeb'; +import { Product, Purchase, PurchasedItem } from '../../firebaseData'; + + export default function ComprasScreen() { - const [products, setProducts] = useState([ - { id: 1, name: "Café Molido Premium", price: 2.5, stock: 50 }, - { id: 2, name: "Aguacate", price: 5.0, stock: 30 }, - { id: 3, name: "Lechuga", price: 8.5, stock: 25 }, - { id: 4, name: "Tomate", price: 4.0, stock: 15 }, - { id: 5, name: "Pescado", price: 2.0, stock: 40 }, - { id: 6, name: "Extracto de naranja", price: 3.5, stock: 35 }, - { id: 7, name: "Almendras", price: 6.0, stock: 20 }, - { id: 8, name: "Envolturas", price: 5.0, stock: 12 }, - ]); - - type Purchase = { - id: number; - supplier: string; - product: string; - quantity: number; - unitCost: number; - total: number; - date: string; - }; + const [products, setProducts] = useState([]); const [purchases, setPurchases] = useState([]); + const [notification, setNotification] = useState<{ text: string; type: "success" | "error"; @@ -37,57 +24,119 @@ export default function ComprasScreen() { setNotification({ text, type }); }; - const addPurchase = (newPurchase: { - product: string; + // Leemos productos + useEffect(() => { + const q = query(collection(db, "products")); + const unsubscribe = onSnapshot(q, (snapshot) => { + const productsData: Product[] = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + } as Product)); + setProducts(productsData); + }, (error) => { + Alert.alert("Error"); + }); + + return () => unsubscribe(); + }, []); + + // Leer Compras + useEffect(() => { + const q = query(collection(db, "purchases"), orderBy("purchaseDate", "desc")); + const unsubscribe = onSnapshot(q, (snapshot) => { + const purchasesData: Purchase[] = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + purchaseDate: doc.data().purchaseDate || Timestamp.now(), + } as Purchase)); + setPurchases(purchasesData); + }, (error) => { + Alert.alert("No se pudo cargar el historial de compras"); + }); + + return () => unsubscribe(); + }, []); + + + const addPurchase = async (newPurchaseData: { + productId: string; quantity: number; - supplier?: string; + supplierName?: string; unitCost?: number; }) => { - setProducts((prev) => - prev.map((p) => - p.name === newPurchase.product - ? { ...p, stock: p.stock + newPurchase.quantity } - : p - ) - ); - const productObj = products.find((p) => p.name === newPurchase.product); - const unitCost = newPurchase.unitCost ?? productObj?.price ?? 0; - const supplier = newPurchase.supplier ?? "N/A"; - const total = unitCost * newPurchase.quantity; - setPurchases((prev) => [ - { - id: Date.now(), - supplier, - product: newPurchase.product, - quantity: newPurchase.quantity, - unitCost, - total, - date: new Date().toISOString(), - }, - ...prev, - ]); - showNotification( - `Compra registrada: ${newPurchase.quantity} ${newPurchase.product}` - ); + try { + await runTransaction(db, async (transaction) => { + const productRef = doc(db, "products", newPurchaseData.productId); + const productDoc = await transaction.get(productRef); + + if (!productDoc.exists()) { + throw new Error("Producto no encontrado en inventario."); + } + + const productInInventory = productDoc.data() as Product; + const newStock = productInInventory.stock + newPurchaseData.quantity; + + transaction.update(productRef, { stock: newStock }); + + const unitCostAtPurchase = newPurchaseData.unitCost ?? productInInventory.unitCost; + const totalItemCost = unitCostAtPurchase * newPurchaseData.quantity; + + const purchasedItem: PurchasedItem = { + productId: newPurchaseData.productId, + productName: productInInventory.name, + quantity: newPurchaseData.quantity, + unitCost: unitCostAtPurchase, + }; + + const purchaseToAdd: Omit = { + purchaseDate: Timestamp.now(), + supplierName: newPurchaseData.supplierName ?? "Proveedor Desconocido", + items: [purchasedItem], + total: totalItemCost, + }; + + transaction.set(doc(collection(db, "purchases")), purchaseToAdd); + }); + + showNotification("Compra registrada."); + } catch (error) { + showNotification("Error al registrar la compra."); + } }; - const deletePurchase = (id: number) => { - setPurchases((prev) => { - const updated = [...prev]; - const index = updated.findIndex((p) => p.id === id); - if (index !== -1) { - const removed = updated.splice(index, 1)[0]; - setProducts((ps) => - ps.map((p) => - p.name === removed.product - ? { ...p, stock: Math.max(p.stock - removed.quantity, 0) } - : p - ) - ); - } - return updated; - }); - showNotification("Compra eliminada correctamente"); + const deletePurchase = async (id: string) => { + try { + await runTransaction(db, async (transaction) => { + const purchaseRef = doc(db, "purchases", id); + const purchaseDoc = await transaction.get(purchaseRef); + + if (!purchaseDoc.exists()) { + throw new Error("Compra no encontrada."); + } + + const removedPurchase = purchaseDoc.data() as Purchase; + + // Revertir stock? + for (const item of removedPurchase.items) { + const productRef = doc(db, "products", item.productId); + const productDoc = await transaction.get(productRef); + + if (productDoc.exists()) { + const productInInventory = productDoc.data() as Product; + const newStock = Math.max(productInInventory.stock - item.quantity, 0); + transaction.update(productRef, { stock: newStock }); + } else { + console.warn(`Error, no se encontro el producto: (${item.productName}) para revertir stock.`); + } + } + + transaction.delete(purchaseRef); + }); + + showNotification("Compra eliminada correctamente y stock revertido."); + } catch (error) { + showNotification("Error al eliminar la compra."); + } }; return ( @@ -98,4 +147,4 @@ export default function ComprasScreen() { ); -} +}; \ No newline at end of file diff --git a/CoffeeUrbanTech/app/frontend/screens/InventarioScreen.tsx b/CoffeeUrbanTech/app/frontend/screens/InventarioScreen.tsx index 01fa5f5..0128e7f 100644 --- a/CoffeeUrbanTech/app/frontend/screens/InventarioScreen.tsx +++ b/CoffeeUrbanTech/app/frontend/screens/InventarioScreen.tsx @@ -1,7 +1,9 @@ import { Picker } from "@react-native-picker/picker"; import { LinearGradient } from "expo-linear-gradient"; -import React, { useState } from "react"; +import { addDoc, collection, deleteDoc, doc, onSnapshot, orderBy, query, serverTimestamp, Timestamp, updateDoc } from "firebase/firestore"; +import React, { useEffect, useState } from "react"; import { + Alert, Modal, Pressable, ScrollView, @@ -10,48 +12,16 @@ import { TouchableOpacity, View, } from "react-native"; - -const initialProducts = [ - { - id: 1, - name: "Café", - description: "Café en grano premium", - price: 120, - costo: 80, - stock: 8, - category: "Bebidas", - tipo: "Materia prima", // Nuevo campo - }, - { - id: 2, - name: "Té", - description: "Té verde importado", - price: 80, - costo: 50, - stock: 15, - category: "Bebidas", - tipo: "Materia prima", - }, -]; - -type Product = { - id: number; - name: string; - description: string; - price: number; - costo: number; - stock: number; - category: string; - tipo: string; // Nuevo campo -}; +import { db } from '../../../firebaseConfigWeb.js'; +import { Product } from "../../firebaseData"; const categoriasPorTipo: Record = { - "Producto de cocina": ["Comida", "Postres", "Otros"], - "Materia prima": ["Bebidas", "Ingredientes", "Otros"], + "Producto de cocina": ["Comida", "Postres", "Bebidas Preparadas", "Otros"], + "Materia prima": ["Bebidas", "Ingredientes", "Envasados", "Otros"], }; export default function InventarioScreen() { - const [products, setProducts] = useState(initialProducts); + const [products, setProducts] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [showAddModal, setShowAddModal] = useState(false); const [editingProduct, setEditingProduct] = useState(null); @@ -59,61 +29,162 @@ export default function InventarioScreen() { name: "", description: "", price: "", - costo: "", + unitCost: "", stock: "", category: "Comida", - tipo: "Producto de cocina", // Valor por defecto + productType: "Producto de cocina", + inMenu: false, }); const SUGERENCIA_UTILIDAD = 0.4; // 40% de utilidad sugerida + // Nuevo: useEffect para leer productos de Firestore + useEffect(() => { + const q = query(collection(db, "products"), orderBy("createdAt", "desc")); + + const unsubscribe = onSnapshot(q, (querySnapshot) => { + const productsData: Product[] = []; + querySnapshot.forEach((doc) => { + const data = doc.data(); + productsData.push({ + id: doc.id, + name: data.name, + description: data.description, + price: data.price, + unitCost: data.unitCost, + stock: data.stock, + category: data.category, + productType: data.productType, + suggestedPrice: data.suggestedPrice || null, + inMenu: data.inMenu || false, + createdAt: data.createdAt instanceof Timestamp ? data.createdAt : null, + updatedAt: data.updatedAt instanceof Timestamp ? data.updatedAt : null, + }); + }); + setProducts(productsData); + }); + + return () => unsubscribe(); + }, []); + const filteredProducts = products.filter( (product) => product.name.toLowerCase().includes(searchTerm.toLowerCase()) || product.description.toLowerCase().includes(searchTerm.toLowerCase()) ); - // Agrupa productos por tipo const groupedProducts = { - "Producto de cocina": filteredProducts.filter((p) => p.tipo === "Producto de cocina"), - "Materia prima": filteredProducts.filter((p) => p.tipo === "Materia prima"), + "Producto de cocina": filteredProducts.filter((p) => p.productType === "Producto de cocina"), + "Materia prima": filteredProducts.filter((p) => p.productType === "Materia prima"), }; - const handleAddProduct = () => { - const product = { - id: Date.now(), - name: newProduct.name, - description: newProduct.description, - price: parseFloat(newProduct.price), - costo: parseFloat(newProduct.costo), - stock: parseInt(newProduct.stock), - category: newProduct.category, - tipo: newProduct.tipo, - }; - setProducts([...products, product]); - setNewProduct({ - name: "", - description: "", - price: "", - costo: "", - stock: "", - category: categoriasPorTipo[newProduct.tipo][0], - tipo: "Producto de cocina", - }); - setShowAddModal(false); + const handleAddProduct = async () => { + if (!newProduct.name || !newProduct.price || !newProduct.unitCost || !newProduct.stock) { + Alert.alert("Error", "Por favor, rellena todos los campos obligatorios: Nombre, Precio, Costo y Stock."); + return; + } + + try { + const productData = { + name: newProduct.name, + description: newProduct.description, + price: parseFloat(newProduct.price), + unitCost: parseFloat(newProduct.unitCost), + stock: parseInt(newProduct.stock), + category: newProduct.category, + productType: newProduct.productType, + inMenu: newProduct.inMenu, + createdAt: serverTimestamp(), + updatedAt: serverTimestamp(), + suggestedPrice: parseFloat(newProduct.price), + }; + + await addDoc(collection(db, "products"), productData); + Alert.alert("Producto agregado."); + + setNewProduct({ + name: "", + description: "", + price: "", + unitCost: "", + stock: "", + category: "Comida", + productType: "Producto de cocina", + inMenu: false, + }); + setShowAddModal(false); + } catch (error) { + Alert.alert("Error. Hubo un problema al agregar el producto."); + } }; - const handleEditProduct = () => { - if (!editingProduct) return; - const updatedProducts = products.map((p) => - p.id === editingProduct.id ? editingProduct : p - ); - setProducts(updatedProducts); - setEditingProduct(null); + const handleEditProduct = async () => { + if (!editingProduct || !editingProduct.id) { + Alert.alert("No hay producto seleccionado."); + return; + } + + if (!editingProduct.name || !editingProduct.price || !editingProduct.unitCost || !editingProduct.stock) { + Alert.alert("Error, rellena los campos: Nombre, Precio, Costo y Stock."); + return; + } + + try { + const productRef = doc(db, "products", editingProduct.id); + + const updatedData = { + name: editingProduct.name, + description: editingProduct.description, + price: parseFloat(editingProduct.price.toString()), + unitCost: parseFloat(editingProduct.unitCost.toString()), + stock: parseInt(editingProduct.stock.toString()), + category: editingProduct.category, + productType: editingProduct.productType, + inMenu: editingProduct.inMenu, + updatedAt: serverTimestamp(), + suggestedPrice: parseFloat(editingProduct.suggestedPrice?.toString() || '0'), + }; + + await updateDoc(productRef, updatedData); + Alert.alert("Producto actualizado."); + setEditingProduct(null); + } catch (error) { + Alert.alert("Error. Por favor, intenta de nuevo."); + } }; - const deleteProduct = (productId: number) => { - setProducts(products.filter((p) => p.id !== productId)); + + const deleteProduct = async (productId: string) => { + console.log("Eliminando..."); + Alert.alert( + "Confirmar Eliminación", + "¿Estás seguro de que quieres eliminar este producto? Esta acción es irreversible.", + [ + { + text: "Cancelar", + onPress: () => console.log("Eliminación cancelada"), + style: "cancel", + }, + { + text: "Eliminar", + onPress: async () => { + try { + // Referencia al documento en Firestore + const productRef = doc(db, "products", productId); + + // Elimina el documento de Firestore + await deleteDoc(productRef); + Alert.alert("Producto eliminado correctamente."); + + } catch (error) { + Alert.alert("Hubo un problema al eliminar el producto. Inténtalo de nuevo."); + } + }, + style: "destructive", + }, + ], + { cancelable: true } + ); }; function redondearPrecio(precio: number) { @@ -122,19 +193,39 @@ export default function InventarioScreen() { const handleCostoChange = (v: string) => { if (!v) { - setNewProduct((prev) => ({ ...prev, costo: "", price: "" })); + setNewProduct((prev) => ({ ...prev, unitCost: "", price: "" })); return; } const costo = parseFloat(v.replace(",", ".")); if (isNaN(costo)) { - setNewProduct((prev) => ({ ...prev, costo: v, price: "" })); + setNewProduct((prev) => ({ ...prev, unitCost: v, price: "" })); return; } const precioSugerido = redondearPrecio(costo * (1 + SUGERENCIA_UTILIDAD)).toString(); setNewProduct((prev) => ({ ...prev, - costo: v, + unitCost: v, + price: precioSugerido, + })); + }; + + const handleEditCostoChange = (v: string) => { + if (!editingProduct) return; + if (!v) { + setEditingProduct((prev) => ({ ...prev!, unitCost: 0, price: 0, suggestedPrice: 0 })); + return; + } + const costo = parseFloat(v.replace(",", ".")); + if (isNaN(costo)) { + setEditingProduct((prev) => ({ ...prev!, unitCost: 0 })); + return; + } + const precioSugerido = redondearPrecio(costo * (1 + SUGERENCIA_UTILIDAD)); + setEditingProduct((prev) => ({ + ...prev!, + unitCost: costo, price: precioSugerido, + suggestedPrice: precioSugerido, })); }; @@ -172,23 +263,21 @@ export default function InventarioScreen() { /> - {/* Mostrar productos agrupados */} - {Object.entries(groupedProducts).map(([tipo, productos]) => ( - - {/* Encabezado visual destacado */} + {Object.entries(groupedProducts).map(([productType, productos]) => ( + - {tipo === "Producto de cocina" ? "🍳 Productos de cocina" : "🌱 Materias primas"} + {productType === "Producto de cocina" ? "🍳 Productos de cocina" : "🌱 Materias primas"} {productos.length === 0 && ( @@ -207,20 +296,50 @@ export default function InventarioScreen() { {product.description} - - Precio venta: ${product.price.toFixed(2)} |{" "} - Costo: ${product.costo.toFixed(2)} |{" "} - Utilidad:{" "} - - ${ (product.price - product.costo).toFixed(2) } + + + Precio venta: ${product.price.toFixed(2)} - {" | "} - Stock:{" "} - - {product.stock} - {" "} - | Categoría: {product.category} - + + Costo: ${product.unitCost.toFixed(2)} + + + + Utilidad:{" "} + ${(product.price - product.unitCost).toFixed(2)} + + + + Stock:{" "} + + {product.stock} + + + + Categoría: {product.category} + + + Tipo: {product.productType} + + {product.suggestedPrice !== null && ( + + Sugerido: ${product.suggestedPrice?.toFixed(2)} + + )} + + En Menú: {product.inMenu ? "Sí" : "No"} + + {product.createdAt && ( + + Creado: {product.createdAt?.toDate().toLocaleDateString()} + + )} + {product.updatedAt && ( + + Actualizado: {product.updatedAt?.toDate().toLocaleDateString()} + + )} + Editar deleteProduct(product.id)} + onPress={() => {console.log(product.id);deleteProduct(product.id)}} className="bg-red-500 px-3 py-1 rounded" > Eliminar @@ -239,13 +358,10 @@ export default function InventarioScreen() { ))} - {/* Línea divisoria visual entre categorías */} ))} - - {/* Modal Agregar Producto */} @@ -274,12 +390,12 @@ export default function InventarioScreen() { multiline /> - Costo ($) + Costo por unidad ($) @@ -299,7 +415,7 @@ export default function InventarioScreen() { - El precio sugerido es un {SUGERENCIA_UTILIDAD * 100}% sobre el costo. + El precio sugerido es un {SUGERENCIA_UTILIDAD * 100}% sobre el costo por unidad. Stock Inicial - Tipo + Tipo de Producto { setNewProduct({ ...newProduct, - tipo: v, - category: categoriasPorTipo[v][0], // Cambia la categoría al primer valor disponible + productType: v, + category: categoriasPorTipo[v][0], }); }} style={{ height: 40 }} @@ -335,11 +451,24 @@ export default function InventarioScreen() { } style={{ height: 40 }} > - {categoriasPorTipo[newProduct.tipo].map((cat) => ( + {categoriasPorTipo[newProduct.productType].map((cat) => ( ))} + ¿En el Menú? + + + setNewProduct({ ...newProduct, inMenu: v === "true" }) + } + style={{ height: 40 }} + > + + + + - {/* Modal Editar Producto */} @@ -393,32 +521,36 @@ export default function InventarioScreen() { placeholder="Descripción" multiline /> - Precio venta ($) + Costo por unidad ($) - setEditingProduct({ - ...editingProduct, - price: parseFloat(v) || 0, - }) - } + value={String(editingProduct.unitCost)} + onChangeText={handleEditCostoChange} className="w-full p-3 border-2 border-gray-300 rounded-lg mb-4" - placeholder="Precio venta" + placeholder="Costo por unidad" keyboardType="numeric" /> - Costo ($) + Precio venta ($) setEditingProduct({ ...editingProduct, - costo: parseFloat(v) || 0, + price: parseFloat(v) || 0, }) } className="w-full p-3 border-2 border-gray-300 rounded-lg mb-4" - placeholder="Costo" + placeholder="Precio venta" keyboardType="numeric" /> + Precio venta sugerido ($) + + + {editingProduct.suggestedPrice !== null ? `$${editingProduct.suggestedPrice?.toFixed(2)}` : "--"} + + + + El precio sugerido es un {SUGERENCIA_UTILIDAD * 100}% sobre el costo por unidad. + Stock - Tipo + Tipo de Producto - setEditingProduct({ ...editingProduct, tipo: v }) - } + selectedValue={editingProduct.productType} + onValueChange={(v) => { + const newCategory = categoriasPorTipo[v][0]; + setEditingProduct({ + ...editingProduct, + productType: v, + category: newCategory + }); + }} style={{ height: 40 }} > @@ -454,11 +591,24 @@ export default function InventarioScreen() { } style={{ height: 40 }} > - {categoriasPorTipo[editingProduct.tipo].map((cat) => ( + {categoriasPorTipo[editingProduct.productType].map((cat) => ( ))} + ¿En el Menú? + + + setEditingProduct({ ...editingProduct, inMenu: v === "true" }) + } + style={{ height: 40 }} + > + + + + ); -} +}; \ No newline at end of file