Bienvenido a Crea y Corte, donde cada producto es una obra de arte personalizada. Descubre regalos únicos que reflejan tu estilo y creatividad.

ERP

import React, { useState, useEffect, useCallback, useMemo } from ‘react’;
import { initializeApp } from ‘firebase/app’;
import { getAuth, signInWithCustomToken, signInAnonymously, onAuthStateChanged } from ‘firebase/auth’;
import { getFirestore, collection, onSnapshot, doc, addDoc, updateDoc, setDoc, query, where, getDocs, writeBatch, deleteDoc, Timestamp } from ‘firebase/firestore’;
// Importamos Recharts para los gráficos del nuevo módulo
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from ‘recharts’;

// CONFIGURACIÓN DE FIREBASE (Variables globales inyectadas por el entorno)
const firebaseConfig = typeof __firebase_config !== ‘undefined’ ? JSON.parse(__firebase_config) : {};
const appId = typeof __app_id !== ‘undefined’ ? __app_id : ‘default-app-id’;

// INICIALIZACIÓN DE SERVICIOS DE FIREBASE
const app = initializeApp(firebaseConfig);
const db = getFirestore(app);
const auth = getAuth(app);

// — ICONOS EXISTENTES (Manteniendo estilo manual) —
const HomeIcon = (props) => (

);
const ShoppingCartIcon = (props) => (

);
const DollarSignIcon = (props) => (

);
const PackageIcon = (props) => (

);
const FileTextIcon = (props) => (

);
const CalculatorIcon = (props) => (

);
const EditIcon = (props) => (

);
const Trash2Icon = (props) => (

);
const PlusCircleIcon = (props) => (

);
const LoaderIcon = (props) => (

);
const RotateCcwIcon = (props) => (

);
const DownloadIcon = (props) => (

);
const AlertTriangleIcon = (props) => (

);

// — NUEVOS ICONOS PARA FINANZAS —
const BarChart3Icon = (props) => (

);
const ArrowUpRightIcon = (props) => (

);
const ArrowDownRightIcon = (props) => (

);

// MODAL DE CONFIRMACIÓN
const ConfirmModal = ({ isOpen, onClose, onConfirm, title, message }) => {
if (!isOpen) return null;
return (

{title}

{message}


);
};

// HOOK PERSONALIZADO para gestionar la conexión a Firestore
function useFirestoreCollection(collectionName) {
const [data, setData] = useState([]);
const [userId, setUserId] = useState(null);
const [isAuthReady, setIsAuthReady] = useState(false);

useEffect(() => {
const unsubscribeAuth = onAuthStateChanged(auth, async (user) => {
if (user) {
setUserId(user.uid);
} else {
if (typeof __initial_auth_token !== ‘undefined’) {
await signInWithCustomToken(auth, __initial_auth_token);
} else {
await signInAnonymously(auth);
}
}
setIsAuthReady(true);
});
return () => unsubscribeAuth();
}, []);

useEffect(() => {
if (!isAuthReady || !userId) return;

const q = collection(db, ‘artifacts’, appId, ‘users’, userId, collectionName);

const unsubscribe = onSnapshot(q, (querySnapshot) => {
const items = [];
querySnapshot.forEach((doc) => {
items.push({ id: doc.id, …doc.data() });
});

// Ordenar por fecha, más reciente primero, si la fecha existe y es Timestamp
if (items.length > 0 && items[0].date && items[0].date instanceof Timestamp) {
items.sort((a, b) => b.date.toMillis() – a.date.toMillis());
} else if (items.length > 0 && items[0].createdAt && items[0].createdAt instanceof Timestamp) {
// Ordenar por createdAt si date no existe (ej: productCosts)
items.sort((a, b) => b.createdAt.toMillis() – a.createdAt.toMillis());
}

setData(items);
}, (error) => {
console.error(`Error fetching ${collectionName}: `, error);
});

return () => unsubscribe();
}, [isAuthReady, userId, collectionName]);

return { data, userId, isAuthReady };
}

// FUNCIONES DE UTILIDAD
const formatCurrency = (value) => new Intl.NumberFormat(‘es-CL’, { style: ‘currency’, currency: ‘CLP’ }).format(Math.round(value || 0));
const formatNumber = (value) => Math.round(value || 0); // Para valores no monetarios en CSV

const formatDate = (date) => {
if (!date) return ”;
const d = date instanceof Timestamp ? date.toDate() : new Date(date);
if (isNaN(d.getTime())) return ”; // Check for invalid date
return d.toISOString().split(‘T’)[0];
};

const formatDateTime = (date) => {
if (!date) return ”;
const d = date instanceof Timestamp ? date.toDate() : new Date(date);
if (isNaN(d.getTime())) return ”; // Check for invalid date
return d.toLocaleString(‘es-CL’);
}

const getToday = () => new Date().toISOString().split(‘T’)[0];

const getYearMonth = (date) => ({
year: date.getFullYear(),
month: date.getMonth() + 1
});

// COMPONENTES DE UI
const Card = ({ title, value, icon, color }) => (

{icon}

{title}

{value}

);

const NavItem = ({ icon, label, active, onClick }) => (

);

// — NUEVO COMPONENTE: DASHBOARD FINANCIERO (Finanzas) —
const FinanceDashboard = ({ transactions }) => {
const currentYear = new Date().getFullYear();
const [selectedYear, setSelectedYear] = useState(currentYear);

// Calcular años disponibles basados en las transacciones
const availableYears = useMemo(() => {
const years = new Set([currentYear]); // Siempre incluir el año actual
transactions.forEach(t => {
if (t.date) {
const d = t.date instanceof Timestamp ? t.date.toDate() : new Date(t.date);
if (!isNaN(d.getTime())) {
years.add(d.getFullYear());
}
}
});
return Array.from(years).sort((a, b) => b – a); // Orden descendente
}, [transactions, currentYear]);

const metrics = useMemo(() => {
// 1. Filtrar solo transacciones válidas
const validTransactions = transactions.filter(t => t.status !== ‘anulada’);

// 2. Filtrar por el año seleccionado
const yearTransactions = validTransactions.filter(t => {
const d = t.date instanceof Timestamp ? t.date.toDate() : new Date(t.date);
return d.getFullYear() === selectedYear;
});

// 3. Totales Generales (PARA EL AÑO SELECCIONADO)
// Ingresos = Todas las ventas
const totalIncome = yearTransactions
.filter(t => t.type === ‘sale’)
.reduce((sum, t) => sum + (t.totalAmount || 0), 0);

// Egresos = Todas las compras
const totalExpenses = yearTransactions
.filter(t => t.type === ‘purchase’)
.reduce((sum, t) => sum + (t.totalAmount || 0), 0);

const netFlow = totalIncome – totalExpenses;

// 4. Datos Mensuales para Gráfico
const months = [“Ene”, “Feb”, “Mar”, “Abr”, “May”, “Jun”, “Jul”, “Ago”, “Sep”, “Oct”, “Nov”, “Dic”];

const monthlyData = months.map((monthName, index) => {
const monthIncome = yearTransactions
.filter(t => {
const d = t.date instanceof Timestamp ? t.date.toDate() : new Date(t.date);
return t.type === ‘sale’ && d.getMonth() === index;
})
.reduce((sum, t) => sum + (t.totalAmount || 0), 0);

const monthExpense = yearTransactions
.filter(t => {
const d = t.date instanceof Timestamp ? t.date.toDate() : new Date(t.date);
return t.type === ‘purchase’ && d.getMonth() === index;
})
.reduce((sum, t) => sum + (t.totalAmount || 0), 0);

return {
name: monthName,
Ingresos: monthIncome,
Egresos: monthExpense
};
});

// 5. Transacciones Recientes (del año seleccionado)
const recentTransactions = […yearTransactions]
.sort((a, b) => {
const dateA = a.date instanceof Timestamp ? a.date.toMillis() : new Date(a.date).getTime();
const dateB = b.date instanceof Timestamp ? b.date.toMillis() : new Date(b.date).getTime();
return dateB – dateA;
})
.slice(0, 15);

return { totalIncome, totalExpenses, netFlow, monthlyData, recentTransactions };
}, [transactions, selectedYear]);

return (

Tablero Financiero (Flujo de Caja)

{/* Selector de Año */}


{/* KPIs */}

}
color=”bg-emerald-500″
/>
}
color=”bg-rose-500″
/>

= 0 ? ‘border-l-emerald-500’ : ‘border-l-rose-500’}`}>

= 0 ? ‘bg-emerald-100 text-emerald-600’ : ‘bg-rose-100 text-rose-600’}`}>

Utilidad Neta {selectedYear}

= 0 ? ‘text-emerald-700’ : ‘text-rose-700’}`}>
{formatCurrency(metrics.netFlow)}

{/* Gráfico Comparativo Mensual */}

Comparativa Mensual ({selectedYear})





`$${val/1000}k`} />
formatCurrency(value)}
/>




{/* Historial Detallado Unificado */}

Movimientos Financieros ({selectedYear})

{metrics.recentTransactions.length > 0 ? metrics.recentTransactions.map(t => (

)) : (

)}

Fecha Tipo Detalle Documento Monto
{formatDateTime(t.date)}
{t.type === ‘sale’ ? ‘Ingreso’ : ‘Egreso’}
{t.vendor || t.customer || ‘Sin nombre’} {t.documentType} {t.documentNumber ? `#${t.documentNumber}` : ”} {t.type === ‘sale’ ? ‘+’ : ‘-‘} {formatCurrency(t.totalAmount)}
No hay movimientos registrados para este año.

);
};

// PESTAÑAS PRINCIPALES EXISTENTES (SIN CAMBIOS EN LÓGICA)
const Dashboard = ({ userId, transactions, settings, onSettingsChange, reportMonth }) => {
const [modalState, setModalState] = useState({ isOpen: false, onConfirm: null, title: ”, message: ” });
const [isResetting, setIsResetting] = useState(false);

const closeModal = () => setModalState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const currentMonthData = useMemo(() => {
const now = new Date();
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
// Asegurarse que date existe y es un objeto Timestamp antes de llamar a toDate()
return transactions.filter(t => t.date && t.date.toDate && t.date.toDate() >= startOfMonth && t.status !== ‘anulada’);
}, [transactions]);

const sales = useMemo(() => currentMonthData.filter(t => t.type === ‘sale’), [currentMonthData]);
const purchases = useMemo(() => currentMonthData.filter(t => t.type === ‘purchase’), [currentMonthData]);

// Calcular totales netos y de IVA excluyendo items marcados como eliminados
const netSales = sales.reduce((sum, t) => sum + t.netAmount, 0);

const netPurchases = purchases.reduce((sum, purchase) => {
const validItemsNet = purchase.items.reduce((itemSum, item) => {
return item.inventoryDeleted !== true ? itemSum + item.netAmount : itemSum;
}, 0);
return sum + validItemsNet;
}, 0);

const ivaCredit = purchases.reduce((sum, purchase) => {
const validItemsIVA = purchase.items.reduce((itemSum, item) => {
return item.inventoryDeleted !== true ? itemSum + item.iva : itemSum;
}, 0);
return sum + validItemsIVA;
}, 0);

const last10Transactions = useMemo(() =>
[…transactions]
.slice(0, 10),
[transactions]);

const handleResetMonthClick = () => {
const { year, month } = reportMonth;
setModalState({
isOpen: true,
onConfirm: () => handleResetMonthConfirm(year, month),
title: ‘Reiniciar Mes Seleccionado’,
message: `¿Está seguro de que desea reiniciar TODOS los datos para ${month}/${year}? Esto eliminará permanentemente las transacciones y el resumen guardado para este mes.`
});
};

const handleResetMonthConfirm = async (year, month) => {
closeModal();
if (!userId) return;
setIsResetting(true);

const startDate = Timestamp.fromDate(new Date(year, month – 1, 1));
const endDate = Timestamp.fromDate(new Date(year, month, 1)); // El mes siguiente al inicio
const batch = writeBatch(db);

try {
// 1. Encontrar y eliminar transacciones del mes
const transactionsRef = collection(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’);
const q = query(transactionsRef, where(‘date’, ‘>=’, startDate), where(‘date’, ‘<', endDate)); const querySnapshot = await getDocs(q); querySnapshot.forEach(docSnap => {
batch.delete(docSnap.ref);
});

// 2. Eliminar el resumen mensual guardado
const summaryId = `${year}-${String(month).padStart(2, ‘0’)}`;
const summaryRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘monthly_summaries’, summaryId);
batch.delete(summaryRef); // No falla si no existe

// 3. Ejecutar batch
await batch.commit();
console.log(`Mes ${month}/${year} reiniciado exitosamente.`); // Log en lugar de alert
} catch (error) {
console.error(`Error reiniciando el mes ${month}/${year}: `, error);
} finally {
setIsResetting(false);
}
};

return (

Resumen del Mes (Válido)

} color=”bg-green-500″ />
} color=”bg-red-500″ />
} color=”bg-blue-500″ />

Configuración General


onSettingsChange(‘ppmRate’, e.target.value)}
onBlur={e => onSettingsChange(‘ppmRate’, e.target.value, true)}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
placeholder=”Ej: 1.5″
step=”0.1″
/>

Pago Provisional Mensual. Afecta el cálculo del F29.


onSettingsChange(‘profitFactor’, e.target.value)}
onBlur={e => onSettingsChange(‘profitFactor’, e.target.value, true)}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
placeholder=”Ej: 1.8″
step=”0.1″
/>

Multiplicador para costos. Usado en Costeo de Productos.


onSettingsChange(‘ivaRate’, e.target.value)}
onBlur={e => onSettingsChange(‘ivaRate’, e.target.value, true)}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
placeholder=”Ej: 19″
step=”0.1″
/>

Impuesto al Valor Agregado para compras y ventas.

Costos de Energía (valor por minuto)


onSettingsChange(‘energyCostPC’, e.target.value)} onBlur={e => onSettingsChange(‘energyCostPC’, e.target.value, true)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” />

onSettingsChange(‘energyCostLaser’, e.target.value)} onBlur={e => onSettingsChange(‘energyCostLaser’, e.target.value, true)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” />

onSettingsChange(‘energyCostPlotter’, e.target.value)} onBlur={e => onSettingsChange(‘energyCostPlotter’, e.target.value, true)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” />

onSettingsChange(‘energyCostIron’, e.target.value)} onBlur={e => onSettingsChange(‘energyCostIron’, e.target.value, true)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” />

onSettingsChange(‘energyCostSewing’, e.target.value)} onBlur={e => onSettingsChange(‘energyCostSewing’, e.target.value, true)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” />

{/* Sección de Acciones del Mes */}

Acciones del Mes Seleccionado

Mes actualmente seleccionado en Reporte F29: {reportMonth.month}/{reportMonth.year}

¡Cuidado! Esta acción eliminará todas las transacciones y el resumen guardado para el mes seleccionado.

Últimas 10 Transacciones (Todas)

{last10Transactions.map(t => (

))}

Fecha Tipo Estado Proveedor/Cliente Total
{formatDateTime(t.date)}
{t.type === ‘sale’ ? ‘Venta’ : ‘Compra’}

{t.status === ‘anulada’ ? ‘Anulada’ : ‘Válida’}
{t.vendor || t.customer || ‘N/A’} {formatCurrency(t.totalAmount)}

);
};

const PurchaseRegistry = ({ userId, inventory, ivaRate, transactions }) => {
const [form, setForm] = useState({
date: getToday(),
vendor: ”,
documentType: ‘Factura’,
documentNumber: ”,
items: [{ name: ”, grossAmount: 0, netAmount: 0, quantity: 1, unit: ‘unidades’, classification: ‘Materia Prima’ }], // Added netAmount to state
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [modalState, setModalState] = useState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const closeModal = () => setModalState({ isOpen: false, onConfirm: null, title: ”, message: ” });
const vatMultiplier = 1 + (ivaRate / 100);

// Sugerencias de proveedores
const providerSuggestions = useMemo(() => {
if (!transactions) return [];
const providers = transactions
.filter(t => t.type === ‘purchase’ && t.vendor)
.map(t => t.vendor);
return […new Set(providers)];
}, [transactions]);

const handleItemChange = (index, field, value) => {
const newItems = […form.items];
newItems[index][field] = value;

const isBoleta = form.documentType === ‘Boleta’;
// Multiplier based on IVA rate
const multiplier = 1 + (ivaRate / 100);

// — LÓGICA DE CÁLCULO BIDIRECCIONAL —
if (field === ‘grossAmount’) {
const gross = Number(value);
// Si es Boleta, Neto = Bruto. Si es Factura, Neto = Bruto / 1.19
newItems[index].netAmount = isBoleta ? gross : (gross / multiplier);
}

if (field === ‘netAmount’) {
const net = Number(value);
// Si es Boleta, Bruto = Neto. Si es Factura, Bruto = Neto * 1.19
newItems[index].grossAmount = isBoleta ? net : (net * multiplier);
}
// —————————————-

setForm({ …form, items: newItems });
};

// Recalcular montos al cambiar tipo de documento
const handleDocumentTypeChange = (e) => {
const newDocType = e.target.value;
const isBoleta = newDocType === ‘Boleta’;
const multiplier = 1 + (ivaRate / 100);

// Recalcular items basados en el nuevo tipo de documento
// Mantener Monto Bruto como referencia (lo que se pagó)
const newItems = form.items.map(item => {
const gross = Number(item.grossAmount);
// Si cambia a Boleta, Neto = Bruto. Si cambia a Factura, Neto = Bruto / 1.19
const net = isBoleta ? gross : (gross / multiplier);
return { …item, netAmount: net };
});

setForm({ …form, documentType: newDocType, items: newItems });
};

const addItem = () => {
setForm({
…form,
items: […form.items, { name: ”, grossAmount: 0, netAmount: 0, quantity: 1, unit: ‘unidades’, classification: ‘Materia Prima’ }]
});
};

const removeItem = (index) => {
setForm({ …form, items: form.items.filter((_, i) => i !== index) });
};

const handleSubmit = async (e) => {
e.preventDefault();
if (!userId || form.items.some(i => !i.name || i.grossAmount <= 0)) { console.error("Por favor complete todos los campos de los ítems."); return; } setIsSubmitting(true); const batch = writeBatch(db); // Corrección de Timezone: Tratar YYYY-MM-DD como fecha local, no UTC. const dateParts = form.date.split('-').map(Number); // new Date(YYYY, MM-1, DD) crea una fecha local a medianoche. const selectedDate = new Date(dateParts[0], dateParts[1] - 1, dateParts[2]); const purchaseDate = Timestamp.fromDate(selectedDate); // ---- INICIO DE CAMBIOS: Lógica de Boleta vs Factura ---- // Ya no es necesario recalcular aquí porque se calcula en el cambio de input, // pero por seguridad usamos los valores del formulario const isBoleta = form.documentType === 'Boleta'; // ---- FIN DE CAMBIOS ---- // Prepare transaction items with potential inventory IDs const processedItems = []; for (const item of form.items) { // Usamos los valores calculados en el estado const gross = Number(item.grossAmount); const net = Number(item.netAmount); const iva = gross - net; // Costo unitario siempre basado en el Neto const unitCostForItem = net / item.quantity; const processedItem = { ...item, netAmount: net, iva, grossAmount: gross }; if (item.classification === 'Materia Prima') { const existingItem = inventory.find(inv => inv.name.toLowerCase() === item.name.toLowerCase());
let inventoryId;

const inventoryUpdateData = {
lastPurchaseDate: purchaseDate,
lastVendor: form.vendor,
lastDocumentType: form.documentType
};

if (existingItem) {
inventoryId = existingItem.id;
const itemRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, inventoryId);
// Update stock and last purchase details in batch
batch.update(itemRef, {
stock: (existingItem.stock || 0) + Number(item.quantity),
unitCost: unitCostForItem, // <-- CAMBIO ...inventoryUpdateData // Add last purchase details }); } else { // Create new inventory item in batch const newItemRef = doc(collection(db, 'artifacts', appId, 'users', userId, 'inventory')); inventoryId = newItemRef.id; // Get the auto-generated ID batch.set(newItemRef, { name: item.name, stock: Number(item.quantity), unit: item.unit, unitCost: unitCostForItem, // <-- CAMBIO salePrice: (unitCostForItem) * 1.3, // <-- CAMBIO: Basar precio en nuevo costo ...inventoryUpdateData // Add last purchase details }); } processedItem.inventoryId = inventoryId; // Store inventory ID in transaction item } else if (item.classification === 'Activo Fijo') { const assetRef = doc(collection(db, 'artifacts', appId, 'users', userId, 'fixed_assets')); batch.set(assetRef, { name: item.name, purchaseValue: net, purchaseDate: purchaseDate, vendor: form.vendor }); processedItem.fixedAssetId = assetRef.id; // Guardar ID del activo creado } processedItems.push(processedItem); } const transactionData = { date: purchaseDate, vendor: form.vendor, documentType: form.documentType, documentNumber: form.documentNumber, // Añadido type: 'purchase', status: 'válida', items: processedItems, // Use items with inventoryId netAmount: processedItems.reduce((sum, item) => sum + item.netAmount, 0),
iva: processedItems.reduce((sum, item) => sum + item.iva, 0),
totalAmount: processedItems.reduce((sum, item) => sum + item.grossAmount, 0),
};

const transRef = doc(collection(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’));
batch.set(transRef, transactionData);

try {
await batch.commit();
setForm({
date: getToday(), vendor: ”, documentType: ‘Factura’, documentNumber: ”,
items: [{ name: ”, grossAmount: 0, netAmount: 0, quantity: 1, unit: ‘unidades’, classification: ‘Materia Prima’ }],
});
} catch (error) {
console.error(“Error al registrar compra: “, error);
} finally {
setIsSubmitting(false);
}
};

const handleAnnulPurchase = async (purchase) => {
if (!purchase || purchase.status === ‘anulada’) return;

const onConfirm = async () => {
const batch = writeBatch(db);

// 1. Marcar compra como anulada
const purchaseRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’, purchase.id);
batch.update(purchaseRef, { status: ‘anulada’ });

// 2. Revertir stock y activos fijos
try {
for (const item of purchase.items) {
// Revertir materia prima
if (item.classification === ‘Materia Prima’ && item.inventoryId) {
const invItem = inventory.find(i => i.id === item.inventoryId);
if (invItem) {
const quantityToReturn = Number(item.quantity);
const invRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, invItem.id);
batch.update(invRef, { stock: (invItem.stock || 0) – quantityToReturn });
}
}
// Eliminar activo fijo creado
if (item.classification === ‘Activo Fijo’ && item.fixedAssetId) {
const assetRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘fixed_assets’, item.fixedAssetId);
batch.delete(assetRef);
}
}

await batch.commit();
} catch (error) {
console.error(“Error al anular la compra: “, error);
}
closeModal();
};

setModalState({
isOpen: true,
onConfirm: onConfirm,
title: ‘Anular Compra’,
message: `¿Está seguro de que desea anular la compra N° ${purchase.documentNumber || purchase.id.substring(0,6)}? Esta acción devolverá los ítems al inventario (si aplica) y eliminará los activos fijos creados.`
});
};

const totals = useMemo(() => {
const totalGross = form.items.reduce((sum, item) => sum + Number(item.grossAmount), 0);
const totalNet = form.items.reduce((sum, item) => sum + Number(item.netAmount), 0);
const totalIva = totalGross – totalNet;
return { totalGross, totalNet, totalIva };
}, [form.items]);

// Historial de compras
const purchaseTransactions = useMemo(() =>
transactions.filter(t => t.type === ‘purchase’)
, [transactions]);

return (

Registro de Compras


setForm({…form, date: e.target.value})} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″ required />

setForm({…form, vendor: e.target.value})}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
required
list=”provider-suggestions” // Link to datalist
/>

{providerSuggestions.map((p, i) =>


setForm({…form, documentNumber: e.target.value})}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
placeholder=”Ej: 123456″
required
/>

Ítems de la Compra

{form.items.map((item, index) => {
return (


handleItemChange(index, ‘name’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” placeholder=”Ej: Harina” required/>

handleItemChange(index, ‘grossAmount’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” required step=”any” placeholder=”Total con IVA”/>

{/* Ahora el Monto Neto es editable y de tipo number */}
handleItemChange(index, ‘netAmount’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” required step=”any” placeholder=”Sin IVA”/>

handleItemChange(index, ‘quantity’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” min=”0.01″ step=”0.01″ required/>


)})}

Total Neto:
{formatCurrency(totals.totalNet)}
IVA ({ivaRate}%):
{formatCurrency(totals.totalIva)}
Total Bruto:
{formatCurrency(totals.totalGross)}

{/* Historial de Compras */}

Historial de Compras

{purchaseTransactions.map(purchase => (

))}

Fecha Proveedor Tipo Doc. N° Doc. Total Estado Acción
{formatDateTime(purchase.date)} {purchase.vendor} {purchase.documentType} {purchase.documentNumber} {formatCurrency(purchase.totalAmount)}
{purchase.status === ‘anulada’ ? ‘Anulada’ : ‘Válida’}
{purchase.status !== ‘anulada’ && (

)}

);
};

const SaleRegistry = ({ userId, productCosts, inventory, ivaRate, transactions }) => {
// Componente SaleRegistry
// … (código existente) …
const [form, setForm] = useState({
date: getToday(),
customer: ”,
documentType: ‘Boleta’,
documentNumber: ”, // Añadido
items: [{ productId: ”, quantity: 1, unitPrice: 0, totalGross: 0 }],
});
const [isSubmitting, setIsSubmitting] = useState(false);
const [errorMessage, setErrorMessage] = useState(”);
const [modalState, setModalState] = useState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const vatMultiplier = 1 + (ivaRate / 100);
const saleTransactions = useMemo(() => transactions.filter(t => t.type === ‘sale’).slice(0, 20), [transactions]);

const handleItemChange = (index, field, value) => {
const newItems = […form.items];
newItems[index][field] = value;

if (field === ‘productId’) {
const selectedProduct = productCosts.find(p => p.id === value);
if(selectedProduct) {
newItems[index].unitPrice = selectedProduct.baseNetPrice || 0;
newItems[index].totalGross = (selectedProduct.roundedPrice || 0) * newItems[index].quantity;
}
}

if (field === ‘quantity’) {
const product = productCosts.find(p => p.id === newItems[index].productId);
if (product) {
newItems[index].totalGross = (product.roundedPrice || 0) * (Number(newItems[index].quantity) || 0);
}
}

if (field === ‘totalGross’) {
// Recalculates unitPrice if gross is manually changed
const netAmount = Number(value) / vatMultiplier;
const quantity = Number(newItems[index].quantity);
if (quantity > 0) {
newItems[index].unitPrice = netAmount / quantity;
}
}

setForm({ …form, items: newItems });
};

const addItem = () => {
setForm({ …form, items: […form.items, { productId: ”, quantity: 1, unitPrice: 0, totalGross: 0 }] });
};

const removeItem = (index) => {
setForm({ …form, items: form.items.filter((_, i) => i !== index) });
};

const handleSubmit = async (e) => {
e.preventDefault();
setErrorMessage(”);

if (!userId || form.items.some(i => !i.productId || i.quantity <= 0)) { setErrorMessage("Por favor complete todos los campos de los ítems."); return; } setIsSubmitting(true); const batch = writeBatch(db); const requiredMaterials = new Map(); // --- 1. Calcular materiales requeridos y Validar Stock --- for (const item of form.items) { const product = productCosts.find(p => p.id === item.productId);
if (!product || !product.inventoryItemsUsed) continue;

for (const ingredient of product.inventoryItemsUsed) {
const required = (requiredMaterials.get(ingredient.inventoryId) || 0) + (Number(ingredient.quantity) * Number(item.quantity));
requiredMaterials.set(ingredient.inventoryId, required);
}
}

for (const [inventoryId, requiredQuantity] of requiredMaterials.entries()) {
const invItem = inventory.find(i => i.id === inventoryId);
if (!invItem || (invItem.stock || 0) < requiredQuantity) { setErrorMessage(`Venta bloqueada: Stock insuficiente de "${invItem?.name || 'ID: '+inventoryId}". Necesitas ${requiredQuantity}, solo tienes ${invItem?.stock || 0}.`); setIsSubmitting(false); return; } } // --- 2. Descontar Stock --- for (const [inventoryId, requiredQuantity] of requiredMaterials.entries()) { const invItem = inventory.find(i => i.id === inventoryId);
const invRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, inventoryId);
batch.update(invRef, { stock: (invItem.stock || 0) – requiredQuantity });
}

// — 3. Registrar Transacción de Venta —
// Corrección de Timezone: Tratar YYYY-MM-DD como fecha local, no UTC.
const dateParts = form.date.split(‘-‘).map(Number);
const selectedDate = new Date(dateParts[0], dateParts[1] – 1, dateParts[2]);

const transactionData = {
date: Timestamp.fromDate(selectedDate), // <-- CAMBIO: Usar la fecha del formulario customer: form.customer, documentType: form.documentType, documentNumber: form.documentNumber, // Añadido type: 'sale', status: 'válida', items: form.items.map(item => {
const product = productCosts.find(p => p.id === item.productId);
const totalGross = Number(item.totalGross);
const totalNet = totalGross / vatMultiplier;
return {
productId: item.productId,
name: product?.name || ‘N/A’,
quantity: Number(item.quantity),
unitPrice: item.unitPrice,
netAmount: totalNet,
iva: totalGross – totalNet
};
}),
};

transactionData.netAmount = transactionData.items.reduce((sum, item) => sum + item.netAmount, 0);
transactionData.iva = transactionData.items.reduce((sum, item) => sum + item.iva, 0);
transactionData.totalAmount = transactionData.netAmount + transactionData.iva;

const transRef = doc(collection(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’));
batch.set(transRef, transactionData);

try {
await batch.commit();
setForm({
date: getToday(), customer: ”, documentType: ‘Boleta’, documentNumber: ”,
items: [{ productId: ”, quantity: 1, unitPrice: 0, totalGross: 0 }],
});
} catch (error) {
console.error(“Error al registrar venta: “, error);
setErrorMessage(“Hubo un error al registrar la venta.”);
} finally {
setIsSubmitting(false);
}
};

const handleAnnulSale = async (sale) => {
if (!sale || sale.status === ‘anulada’) return;

const onConfirm = async () => {
const batch = writeBatch(db);

// 1. Marcar venta como anulada
const saleRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’, sale.id);
batch.update(saleRef, { status: ‘anulada’ });

// 2. Revertir stock
try {
for (const item of sale.items) {
const product = productCosts.find(p => p.id === item.productId);
if (!product || !product.inventoryItemsUsed) continue;

for (const ingredient of product.inventoryItemsUsed) {
const invItem = inventory.find(i => i.id === ingredient.inventoryId);
if (invItem) {
const quantityToReturn = Number(ingredient.quantity) * Number(item.quantity);
const invRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, invItem.id);
batch.update(invRef, { stock: (invItem.stock || 0) + quantityToReturn });
}
}
}

await batch.commit();
} catch (error) {
console.error(“Error al anular la venta: “, error);
setErrorMessage(“Error al revertir el stock. Contacte a soporte.”);
}
closeModal();
};

setModalState({
isOpen: true,
onConfirm: onConfirm,
title: ‘Anular Venta’,
message: `¿Está seguro de que desea anular la venta #${sale.id.substring(0,6)}? Esta acción devolverá los ${sale.items.length} ítem(s) al inventario.`
});
};

const closeModal = () => setModalState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const totals = useMemo(() => {
const totalGross = form.items.reduce((sum, item) => sum + Number(item.totalGross), 0);
const totalNet = totalGross / vatMultiplier;
const totalIva = totalGross – totalNet;
return { totalGross, totalNet, totalIva };
}, [form.items, vatMultiplier]);

return (

Registro de Ventas

{errorMessage &&

{errorMessage}

}


setForm({…form, date: e.target.value})} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm” required />

setForm({…form, customer: e.target.value})} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm” />


setForm({…form, documentNumber: e.target.value})}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm”
placeholder=”Ej: 123456″
/>

Ítems de la Venta

{form.items.map((item, index) => {
const product = productCosts.find(p => p.id === item.productId);
const netAmount = Number(item.totalGross) / vatMultiplier;
return (



handleItemChange(index, ‘quantity’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” min=”1″ step=”1″ required/>


handleItemChange(index, ‘totalGross’, e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm text-sm” required step=”any”/>

)})}

Total Neto:
{formatCurrency(totals.totalNet)}
IVA Débito ({ivaRate}%):
{formatCurrency(totals.totalIva)}
Total Bruto:
{formatCurrency(totals.totalGross)}

Historial de Ventas

{saleTransactions.map(sale => (

))}

Fecha N° Doc. Monto Estado Acción
{formatDateTime(sale.date)}
ID: {sale.id.substring(0,6)}…
{sale.documentNumber || ‘N/A’} {formatCurrency(sale.totalAmount)}
{sale.status === ‘anulada’ ? ‘Anulada’ : ‘Válida’}
{sale.status !== ‘anulada’ && (

)}

);
};

const InventoryAndAssets = ({ userId, inventory, fixedAssets }) => {
const [editingItem, setEditingItem] = useState(null);
const [itemData, setItemData] = useState({ name: ”, stock: 0, salePrice: 0, unitCost: 0, unit: ‘unidades’ });

const [editingAsset, setEditingAsset] = useState(null);
const [assetData, setAssetData] = useState({ name: ”, purchaseValue: 0, purchaseDate: ”, vendor: ”});

const [modalState, setModalState] = useState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const closeModal = () => setModalState({ isOpen: false, onConfirm: null, title: ”, message: ” });

// — Lógica de Inventario —
const handleEditClick = (item) => {
setEditingItem(item.id);
setItemData({ name: item.name, stock: item.stock, salePrice: item.salePrice, unitCost: item.unitCost, unit: item.unit || ‘unidades’ });
};

const handleSave = async () => {
if (!userId || !editingItem) return;
const docRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, editingItem);
try {
// Solo guardar los campos editables
await updateDoc(docRef, {
name: itemData.name,
stock: Number(itemData.stock),
salePrice: Number(itemData.salePrice),
unitCost: Number(itemData.unitCost),
unit: itemData.unit
});
setEditingItem(null);
} catch (error) {
console.error(“Error updating item: “, error);
}
};

const handleDeleteInventoryClick = (itemId, itemName) => {
setModalState({
isOpen: true,
onConfirm: () => handleDeleteInventory(itemId),
title: ‘Eliminar Ítem de Inventario’,
message: `¿Está seguro de que desea eliminar “${itemName}”? Esto marcará las compras asociadas y afectará el cálculo del IVA Crédito en reportes pasados.`
});
};

const handleDeleteInventory = async (itemId) => {
if (!userId) return;
const batch = writeBatch(db);
try {
// 1. Marcar items en transacciones de compra
const transactionsRef = collection(db, ‘artifacts’, appId, ‘users’, userId, ‘transactions’);
const q = query(transactionsRef, where(‘type’, ‘==’, ‘purchase’));
const querySnapshot = await getDocs(q);

querySnapshot.forEach(docSnap => {
const transaction = docSnap.data();
let needsUpdate = false;
const updatedItems = transaction.items.map(item => {
if (item.inventoryId === itemId) {
needsUpdate = true;
return { …item, inventoryDeleted: true }; // Marcar el item
}
return item;
});

if (needsUpdate) {
batch.update(docSnap.ref, { items: updatedItems });
}
});

// 2. Eliminar el item del inventario
const invRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘inventory’, itemId);
batch.delete(invRef);

// 3. Ejecutar batch
await batch.commit();

} catch (error) {
console.error(“Error deleting item and updating transactions: “, error);
} finally {
closeModal();
}
};

// — Lógica de Activos Fijos —
const handleEditAssetClick = (asset) => {
setEditingAsset(asset.id);
setAssetData({
name: asset.name,
purchaseValue: asset.purchaseValue,
purchaseDate: formatDate(asset.purchaseDate),
vendor: asset.vendor
});
};

const handleSaveAsset = async () => {
if (!userId || !editingAsset) return;
const docRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘fixed_assets’, editingAsset);
try {
// Validate date before creating Timestamp
const dateToSave = assetData.purchaseDate ? Timestamp.fromDate(new Date(assetData.purchaseDate)) : null;
if (!dateToSave) {
console.error(“Invalid purchase date provided for asset.”);
return; // Prevent saving with invalid date
}

await updateDoc(docRef, {
name: assetData.name,
purchaseValue: Number(assetData.purchaseValue),
purchaseDate: dateToSave,
vendor: assetData.vendor
});
setEditingAsset(null);
} catch (error) {
console.error(“Error updating asset: “, error);
}
};

const handleDeleteAssetClick = (assetId, assetName) => {
setModalState({
isOpen: true,
onConfirm: () => handleDeleteAsset(assetId),
title: ‘Eliminar Activo Fijo’,
message: `¿Está seguro de que desea eliminar “${assetName}”? Esta acción no se puede deshacer.`
});
};

const handleDeleteAsset = async (assetId) => {
if (!userId) return;
try {
await deleteDoc(doc(db, ‘artifacts’, appId, ‘users’, userId, ‘fixed_assets’, assetId));
} catch (error) {
console.error(“Error deleting asset: “, error);
}
closeModal();
};

return (

Inventario y Activos Fijos

Inventario (Materia Prima)

{inventory.map(item => (

{editingItem === item.id ? (
<>
{/* Campos Editables */}

{/* Campos de solo lectura */}

{/* Acciones */}


) : (
<>
{/* Datos normales */}

{/* Acciones */}


)}

))}

Producto Stock Unidad Costo Unit. Precio Venta Últ. Compra Proveedor Documento Acciones
setItemData({…itemData, name: e.target.value})} className=”border rounded px-2 py-1 w-full text-sm” /> setItemData({…itemData, stock: e.target.value})} className=”border rounded px-2 py-1 w-20 text-sm” step=”any”/> setItemData({…itemData, unitCost: e.target.value})} className=”border rounded px-2 py-1 w-24 text-sm” step=”any”/> setItemData({…itemData, salePrice: e.target.value})} className=”border rounded px-2 py-1 w-24 text-sm” step=”any”/> {formatDate(item.lastPurchaseDate)} {item.lastVendor} {item.lastDocumentType}
{item.name} {item.stock} {item.unit} {formatCurrency(item.unitCost)} {formatCurrency(item.salePrice)} {formatDate(item.lastPurchaseDate)} {item.lastVendor} {item.lastDocumentType}

Activos Fijos

{fixedAssets.map(asset => (

{editingAsset === asset.id ? (
<>


) : (
<>


)}

))}

Activo Valor Compra Fecha Compra Proveedor Acciones
setAssetData({…assetData, name: e.target.value})} className=”border rounded px-2 py-1 w-full” /> setAssetData({…assetData, purchaseValue: e.target.value})} className=”border rounded px-2 py-1 w-28″ step=”any”/> setAssetData({…assetData, purchaseDate: e.target.value})} className=”border rounded px-2 py-1 w-36″ /> setAssetData({…assetData, vendor: e.target.value})} className=”border rounded px-2 py-1 w-full” />
{asset.name} {formatCurrency(asset.purchaseValue)} {formatDate(asset.purchaseDate)} {asset.vendor}

);
};

const ProductCosting = ({ userId, settings, inventory, productCosts }) => {
const [productName, setProductName] = useState(”);
const [sku, setSku] = useState(”); // Estado para SKU
const [skuSuggestions, setSkuSuggestions] = useState([]); // Estado para sugerencias
const [manualCostItems, setManualCostItems] = useState([]);
const [inventoryItemsUsed, setInventoryItemsUsed] = useState([]);
const [energyItemsUsed, setEnergyItemsUsed] = useState([]);
const [isSubmitting, setIsSubmitting] = useState(false);
const [editingProductId, setEditingProductId] = useState(null);
const [searchTerm, setSearchTerm] = useState(”);
const [modalState, setModalState] = useState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const { profitFactor, ivaRate = 19 } = settings;

// Efecto para cargar sugerencias de SKU
useEffect(() => {
const uniqueSkus = […new Set(productCosts.map(p => p.sku).filter(Boolean))];
setSkuSuggestions(uniqueSkus);
}, [productCosts]);

const closeModal = () => setModalState({ isOpen: false, onConfirm: null, title: ”, message: ” });

const resetForm = () => {
setProductName(”);
setSku(”); // Limpiar SKU
setManualCostItems([]);
setInventoryItemsUsed([]);
setEnergyItemsUsed([]);
setEditingProductId(null);
};

const handleEditClick = (product) => {
setEditingProductId(product.id);
setProductName(product.name);
setSku(product.sku || ”); // Cargar SKU
setManualCostItems(product.manualCostItems || []);
setInventoryItemsUsed(product.inventoryItemsUsed || []);
setEnergyItemsUsed(product.energyItemsUsed || []);
window.scrollTo(0, 0); // Scroll to top to see the form
};

// Handlers for items (sin cambios)
const handleManualItemChange = (index, field, value) => {
const newItems = […manualCostItems];
newItems[index][field] = value;
setManualCostItems(newItems);
};
const addManualItem = () => setManualCostItems([…manualCostItems, { name: ”, value: 0 }]);
const removeManualItem = (index) => setManualCostItems(manualCostItems.filter((_, i) => i !== index));

const handleInventoryItemChange = (index, field, value) => {
const newItems = […inventoryItemsUsed];
newItems[index][field] = value;
setInventoryItemsUsed(newItems);
};
const addInventoryItem = () => setInventoryItemsUsed([…inventoryItemsUsed, { inventoryId: ”, quantity: 0 }]);
const removeInventoryItem = (index) => setInventoryItemsUsed(inventoryItemsUsed.filter((_, i) => i !== index));

const handleEnergyItemChange = (index, field, value) => {
const newItems = […energyItemsUsed];
newItems[index][field] = value;
setEnergyItemsUsed(newItems);
};
const addEnergyItem = () => setEnergyItemsUsed([…energyItemsUsed, { machine: ‘PC’, minutes: 0 }]);
const removeEnergyItem = (index) => setEnergyItemsUsed(energyItemsUsed.filter((_, i) => i !== index));

const handleSaveProduct = async (e) => {
e.preventDefault();
if (!userId || !productName) {
console.error(“Complete el nombre del producto.”);
return;
}

setIsSubmitting(true);
const calculations = calculatedNewProduct;

const productData = {
name: productName,
sku: sku, // Guardar SKU
manualCostItems: manualCostItems.map(item => ({…item, value: Number(item.value)})),
inventoryItemsUsed,
energyItemsUsed,
totalCost: calculations.totalCost,
baseNetPrice: calculations.baseNetPrice,
finalPrice: calculations.finalPrice,
roundedPrice: calculations.roundedPrice,
unitProfit: calculations.unitProfit,
createdAt: Timestamp.now(), // Usar Timestamp
};

try {
if(editingProductId) {
const docRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘product_costs’, editingProductId);
// Al actualizar, asegurarse de no sobrescribir createdAt si no se necesita
delete productData.createdAt;
await updateDoc(docRef, productData);
} else {
await addDoc(collection(db, ‘artifacts’, appId, ‘users’, userId, ‘product_costs’), productData);
}
resetForm();
} catch (error) {
console.error(“Error guardando producto costeado: “, error);
} finally {
setIsSubmitting(false);
}
};

const handleDeleteProductClick = (productId, productName) => {
setModalState({
isOpen: true,
onConfirm: () => handleDeleteProduct(productId),
title: `Eliminar Producto Costeado`,
message: `¿Está seguro de que desea eliminar “${productName}”? Esta acción no se puede deshacer.`
});
};

const handleDeleteProduct = async (productId) => {
if (!userId) return;
try {
await deleteDoc(doc(db, ‘artifacts’, appId, ‘users’, userId, ‘product_costs’, productId));
} catch (error) {
console.error(“Error eliminando producto: “, error);
}
closeModal();
};

const calculatedNewProduct = useMemo(() => {
const manualTotal = manualCostItems.reduce((sum, item) => sum + Number(item.value), 0);

const inventoryTotal = inventoryItemsUsed.reduce((sum, item) => {
const inventoryItem = inventory.find(i => i.id === item.inventoryId);
const cost = (inventoryItem?.unitCost || 0) * (Number(item.quantity) || 0);
return sum + cost;
}, 0);

const energyTotal = energyItemsUsed.reduce((sum, item) => {
const machineCosts = {
PC: settings.energyCostPC || 0,
Laser: settings.energyCostLaser || 0,
Plotter: settings.energyCostPlotter || 0,
Iron: settings.energyCostIron || 0,
Sewing: settings.energyCostSewing || 0,
};
const costPerMinute = machineCosts[item.machine] || 0;
return sum + (costPerMinute * (Number(item.minutes) || 0));
}, 0);

const totalCost = manualTotal + inventoryTotal + energyTotal;
if (totalCost === 0) return { totalCost: 0, baseNetPrice: 0, unitIva: 0, unitProfit: 0, finalPrice: 0, roundedPrice: 0 };

const baseNetPrice = totalCost * Number(profitFactor);
const unitIva = baseNetPrice * (ivaRate / 100);
const finalPrice = baseNetPrice + unitIva;

return {
totalCost,
baseNetPrice,
unitIva,
unitProfit: baseNetPrice – totalCost,
finalPrice,
roundedPrice: Math.round(finalPrice / 10) * 10
};
}, [manualCostItems, inventoryItemsUsed, energyItemsUsed, profitFactor, inventory, settings, ivaRate]);

// Filtrar productos guardados según el término de búsqueda
const filteredProductCosts = useMemo(() => {
if (!searchTerm) {
return productCosts;
}
return productCosts.filter(p =>
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
(p.sku && p.sku.toLowerCase().includes(searchTerm.toLowerCase())) // Incluir SKU en búsqueda
);
}, [productCosts, searchTerm]);

const energyMachineOptions = [
{ key: ‘PC’, label: ‘PC’ },
{ key: ‘Laser’, label: ‘Máquina Láser’ },
{ key: ‘Plotter’, label: ‘Plotter’ },
{ key: ‘Iron’, label: ‘Plancha Eléctrica’ },
{ key: ‘Sewing’, label: ‘Máquina de Coser’ },
];

return (

Costeo de Productos

{/* Formulario para nuevo producto */}

{editingProductId ? ‘Editando Producto’ : ‘Costear Nuevo Producto’}

{/* Nombre y SKU */}


setProductName(e.target.value)} className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm” placeholder=”Ej: Torta de Chocolate” required />

setSku(e.target.value)}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm”
placeholder=”Ej: TCHOC001″
list=”sku-suggestions” // Link to datalist
/>

{skuSuggestions.map((s, index) =>

{/* Materia Prima de Inventario */}

Materia Prima (desde Inventario)

{inventoryItemsUsed.map((item, index) => {
const selectedInvItem = inventory.find(i => i.id === item.inventoryId);
const itemCost = (selectedInvItem?.unitCost || 0) * (Number(item.quantity) || 0);
return (


handleInventoryItemChange(index, ‘quantity’, e.target.value)} className=”col-span-2 block w-full rounded-md border-gray-300 shadow-sm text-sm” required step=”any” />
{formatCurrency(itemCost)} ({selectedInvItem?.unit})

);
})}

{/* Costos Manuales */}

Otros Costos (Manual)

{manualCostItems.map((item, index) => (

handleManualItemChange(index, ‘name’, e.target.value)} className=”block w-2/3 rounded-md border-gray-300 shadow-sm text-sm” required />
handleManualItemChange(index, ‘value’, e.target.value)} className=”block w-1/3 rounded-md border-gray-300 shadow-sm text-sm” required step=”any” />

))}

{/* Costos de Energia */}

Costos de Energía

{energyItemsUsed.map((item, index) => {
const costPerMinute = settings[`energyCost${item.machine}`] || 0;
const itemCost = costPerMinute * (Number(item.minutes) || 0);
return (


handleEnergyItemChange(index, ‘minutes’, e.target.value)} className=”col-span-2 block w-full rounded-md border-gray-300 shadow-sm text-sm” required step=”any” />
{formatCurrency(itemCost)}

);
})}

{calculatedNewProduct && (

Cálculo de Precio (Factor Ganancia: {profitFactor}x)

Costo Total Producción: {formatCurrency(calculatedNewProduct.totalCost)}
Precio Neto Base: {formatCurrency(calculatedNewProduct.baseNetPrice)}
IVA por unidad ({ivaRate}%): {formatCurrency(calculatedNewProduct.unitIva)}
Ganancia por unidad: {formatCurrency(calculatedNewProduct.unitProfit)}

Precio Venta Final: {formatCurrency(calculatedNewProduct.finalPrice)}
Precio Final Redondeado: {formatCurrency(calculatedNewProduct.roundedPrice)}

)}

{editingProductId && (

)}

{/* Lista de productos guardados */}

Productos Guardados

setSearchTerm(e.target.value)}
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
/>
{filteredProductCosts.map(p => {
return (


{p.name}

{p.sku &&

SKU: {p.sku}

} {/* Mostrar SKU */}

    {p.inventoryItemsUsed?.map((item, i) => {
    const invItem = inventory.find(inv => inv.id === item.inventoryId);
    return

  • {item.quantity} {invItem?.unit || ”} de {invItem?.name || ‘N/A’}
  • })}
    {p.manualCostItems?.map((item, i) =>

  • {item.name}: {formatCurrency(item.value)}
  • )}
    {p.energyItemsUsed?.map((item, i) =>

  • {item.minutes} min. de {item.machine}
  • )}

Costo Total: {formatCurrency(p.totalCost)}
Ganancia: {formatCurrency(p.unitProfit)}
Precio Venta Sugerido: {formatCurrency(p.roundedPrice)}

)
})}

);
};

const F29Report = ({ userId, transactions, monthlySummaries, ppmRate, reportMonth, setReportMonth }) => {
// Componente F29Report
// … (código existente) …
// Usa el estado global reportMonth
const selectedDate = reportMonth;

const reportData = useMemo(() => {
const { year, month } = selectedDate;
const startDate = new Date(year, month – 1, 1);
const endDate = new Date(year, month, 0, 23, 59, 59);

// Filtrar transacciones válidas para el período
const periodTransactions = transactions.filter(t => {
if (!t.date || !(t.date instanceof Timestamp)) return false; // Asegurarse que t.date existe y es Timestamp
const tDate = t.date.toDate();
return tDate >= startDate && tDate <= endDate && t.status !== 'anulada'; }); const sales = periodTransactions.filter(t => t.type === ‘sale’);
const purchases = periodTransactions.filter(t => t.type === ‘purchase’);

// Sumarizar ventas
const salesByDoc = sales.reduce((acc, t) => {
acc[t.documentType] = (acc[t.documentType] || 0) + t.totalAmount;
return acc;
}, {});
const salesByDocCount = sales.reduce((acc, t) => {
acc[t.documentType] = (acc[t.documentType] || 0) + 1;
return acc;
}, {});

const totalNetSales = sales.reduce((sum, t) => sum + t.netAmount, 0);
const totalIvaDebit = sales.reduce((sum, t) => sum + t.iva, 0);

// Sumarizar compras, ignorando items marcados como eliminados del inventario
let totalNetPurchases = 0;
let totalIvaCredit = 0;
const purchasesByDoc = {};
const purchasesByDocCount = {};

purchases.forEach(p => {
let purchaseNet = 0;
let purchaseIva = 0;
let purchaseTotal = 0;
let validItemsCount = 0;

p.items.forEach(item => {
// Solo sumar si el item de inventario asociado no fue eliminado
if (item.inventoryDeleted !== true) {
purchaseNet += item.netAmount;
purchaseIva += item.iva;
validItemsCount++;
}
// Sumar al total bruto siempre para mantener consistencia con el documento original, pero solo contar la transacción si tuvo items válidos
purchaseTotal += item.grossAmount;
});

if (validItemsCount > 0) { // Solo contar la transacción si tuvo items válidos para IVA Crédito
totalNetPurchases += purchaseNet;
totalIvaCredit += purchaseIva;
purchasesByDoc[p.documentType] = (purchasesByDoc[p.documentType] || 0) + purchaseTotal; // Usar total bruto original del doc
purchasesByDocCount[p.documentType] = (purchasesByDocCount[p.documentType] || 0) + 1;
}
});

// Buscar remanente del mes anterior
const prevMonthDate = new Date(year, month – 2, 1);
const prevMonthSummary = monthlySummaries.find(s => s.year === prevMonthDate.getFullYear() && s.month === prevMonthDate.getMonth() + 1);
const ivaCreditCarryover = prevMonthSummary ? prevMonthSummary.ivaCreditCarryover || 0 : 0;

// Calcular IVA a pagar
const ivaToPay = totalIvaDebit – totalIvaCredit – ivaCreditCarryover;

// Activos Fijos del mes (basado en items válidos)
const monthlyFixedAssets = purchases
.map(p => ({
…p,
items: p.items.filter(i => i.classification === ‘Activo Fijo’ && i.inventoryDeleted !== true) // Filtrar por clasificación Y estado no eliminado
}))
.filter(p => p.items.length > 0); // Solo incluir compras que SÍ tienen activos fijos válidos

const totalFixedAssetsValue = monthlyFixedAssets.reduce((sum, p) => sum + p.items.reduce((itemSum, i) => itemSum + i.netAmount, 0), 0);

// Calcular PPM
const ppmAmount = totalNetSales * (Number(ppmRate) / 100);

return {
salesByDoc, salesByDocCount, purchasesByDoc, purchasesByDocCount,
totalNetSales, totalIvaDebit, totalNetPurchases, totalIvaCredit,
ivaCreditCarryover, ivaToPay,
monthlyFixedAssets, totalFixedAssetsValue, ppmAmount
};
}, [selectedDate, transactions, monthlySummaries, ppmRate]);

const handleSaveMonth = async () => {
if (!userId) return;

const { year, month } = selectedDate;
const id = `${year}-${String(month).padStart(2, ‘0’)}`;

const dataToSave = {
year,
month,
totalNetSales: reportData.totalNetSales,
totalIvaDebit: reportData.totalIvaDebit,
totalNetPurchases: reportData.totalNetPurchases,
totalIvaCredit: reportData.totalIvaCredit,
ivaCreditCarryover: reportData.ivaToPay < 0 ? Math.abs(reportData.ivaToPay) : 0, finalIvaPaid: reportData.ivaToPay > 0 ? reportData.ivaToPay : 0,
ppmPaid: reportData.ppmAmount
};

try {
await setDoc(doc(db, ‘artifacts’, appId, ‘users’, userId, ‘monthly_summaries’, id), dataToSave);
// No usar alert
} catch(error) {
console.error(“Error saving monthly summary: “, error);
// No usar alert
}
};

// Función para exportar CSV
const handleExportCSV = () => {
const { year, month } = selectedDate;
const data = reportData;

let csvContent = “Concepto,Valor\n”; // Encabezado

// Añadir datos de Ventas
csvContent += “Resumen Ventas,\n”;
Object.entries(data.salesByDoc).forEach(([docType, total]) => {
csvContent += `Total ${docType} (${data.salesByDocCount[docType] || 0} trans.),${formatNumber(total)}\n`;
});
csvContent += `Total Neto Ventas,${formatNumber(data.totalNetSales)}\n`;
csvContent += `IVA Debito Total,${formatNumber(data.totalIvaDebit)}\n`;
csvContent += “\n”; // Separador

// Añadir datos de Compras
csvContent += “Resumen Compras (Validas para IVA),\n”;
Object.entries(data.purchasesByDoc).forEach(([docType, total]) => {
csvContent += `Total ${docType} (${data.purchasesByDocCount[docType] || 0} trans.),${formatNumber(total)}\n`;
});
csvContent += `Total Neto Compras,${formatNumber(data.totalNetPurchases)}\n`;
csvContent += `IVA Credito Total,${formatNumber(data.totalIvaCredit)}\n`;
csvContent += “\n”; // Separador

// Añadir Activos Fijos si existen
if(data.monthlyFixedAssets.length > 0) {
csvContent += “Activos Fijos Comprados,\n”;
csvContent += “Fecha,Activo,Proveedor,Valor Neto\n”;
data.monthlyFixedAssets.forEach(p => p.items.forEach(item => {
csvContent += `${formatDate(p.date)},”${item.name}”,”${p.vendor}”,${formatNumber(item.netAmount)}\n`;
}));
csvContent += `Total Activos Fijos,${formatNumber(data.totalFixedAssetsValue)}\n`;
csvContent += “\n”; // Separador
}

// Añadir Resumen Final
csvContent += “Calculo Final,\n”;
csvContent += `IVA Debito (Ventas),${formatNumber(data.totalIvaDebit)}\n`;
csvContent += `(-) IVA Credito (Compras),${formatNumber(data.totalIvaCredit)}\n`;
csvContent += `(-) Remanente Mes Anterior,${formatNumber(data.ivaCreditCarryover)}\n`;
csvContent += `${data.ivaToPay >= 0 ? ‘IVA A PAGAR’ : ‘REMANENTE CREDITO’},${formatNumber(Math.abs(data.ivaToPay))}\n`;
csvContent += `(+) PPM (Tasa: ${ppmRate}%),${formatNumber(data.ppmAmount)}\n`;
csvContent += `TOTAL A PAGAR SII,${formatNumber((data.ivaToPay > 0 ? data.ivaToPay : 0) + data.ppmAmount)}\n`;

// Crear y descargar el archivo
const blob = new Blob([csvContent], { type: ‘text/csv;charset=utf-8;’ });
const link = document.createElement(“a”);
if (link.download !== undefined) { // Feature detection
const url = URL.createObjectURL(blob);
link.setAttribute(“href”, url);
link.setAttribute(“download”, `reporte_f29_${year}_${String(month).padStart(2, ‘0’)}.csv`);
link.style.visibility = ‘hidden’;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url); // Liberar memoria
}
};

return (

Reporte Mensual F29


{
const [year, month] = e.target.value.split(‘-‘).map(Number);
// Usa la función del estado global
setReportMonth({ year, month });
}}
className=”border-gray-300 rounded-md shadow-sm”
/>
{/* Sales Section */}

Resumen de Ventas

{Object.entries(reportData.salesByDoc).map(([docType, total]) => (

Total {docType} ({reportData.salesByDocCount[docType] || 0} trans.): {formatCurrency(total)}

))}


Total Neto Ventas: {formatCurrency(reportData.totalNetSales)}
IVA Débito Total: {formatCurrency(reportData.totalIvaDebit)}

{/* Purchases Section */}

Resumen de Compras (Válidas para IVA)

{Object.entries(reportData.purchasesByDoc).map(([docType, total]) => (

Total {docType} ({reportData.purchasesByDocCount[docType] || 0} trans.): {formatCurrency(total)}

))}


Total Neto Compras: {formatCurrency(reportData.totalNetPurchases)}
IVA Crédito Total: {formatCurrency(reportData.totalIvaCredit)}

{/* Fixed Assets Section */}
{reportData.monthlyFixedAssets.length > 0 && (

Activos Fijos Comprados en el Mes

{reportData.monthlyFixedAssets.map(p => p.items.map((item, index) => (

)))}

Fecha Activo Proveedor Valor Neto
{formatDate(p.date)} {item.name} {p.vendor} {formatCurrency(item.netAmount)}
Total Activos Fijos del Mes: {formatCurrency(reportData.totalFixedAssetsValue)}

)}

{/* Final Summary */}

Cálculo Final del Período

IVA Débito (Ventas):
{formatCurrency(reportData.totalIvaDebit)}
(-) IVA Crédito (Compras):
{formatCurrency(reportData.totalIvaCredit)}
(-) Remanente Mes Anterior:
{formatCurrency(reportData.ivaCreditCarryover)}

= 0 ? ‘bg-red-100 text-red-800’ : ‘bg-blue-100 text-blue-800’}`}>
{reportData.ivaToPay >= 0 ? ‘IVA A PAGAR:’ : ‘REMANENTE CRÉDITO:’}
{formatCurrency(Math.abs(reportData.ivaToPay))}
(+) PPM (Tasa: {ppmRate}%):
{formatCurrency(reportData.ppmAmount)}
TOTAL A PAGAR SII:
{formatCurrency((reportData.ivaToPay > 0 ? reportData.ivaToPay : 0) + reportData.ppmAmount)}


Guarda este resumen para el cálculo del remanente del próximo mes.

);
};

// COMPONENTE PRINCIPAL DE LA APLICACIÓN
export default function App() {
const [activeTab, setActiveTab] = useState(‘Finanzas’); // Cambiado a ‘Finanzas’ para resaltar la nueva funcionalidad

// Estado para el mes seleccionado en F29, inicializado al mes actual
const [reportMonth, setReportMonth] = useState(getYearMonth(new Date()));

const { data: transactions, userId, isAuthReady } = useFirestoreCollection(‘transactions’);
const { data: inventory } = useFirestoreCollection(‘inventory’);
const { data: fixedAssets } = useFirestoreCollection(‘fixed_assets’);
const { data: monthlySummaries } = useFirestoreCollection(‘monthly_summaries’);
const { data: productCosts } = useFirestoreCollection(‘product_costs’);

const [settings, setSettings] = useState({
ppmRate: 1.0,
profitFactor: 1.5,
ivaRate: 19,
energyCostPC: 0,
energyCostLaser: 0,
energyCostPlotter: 0,
energyCostIron: 0,
energyCostSewing: 0,
});

useEffect(() => {
if (!isAuthReady || !userId) return;

const settingsRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘settings’, ‘app_settings’);
const unsubscribe = onSnapshot(settingsRef, (doc) => {
if (doc.exists()) {
setSettings(prev => ({…prev, …doc.data()}));
}
});

return () => unsubscribe();
}, [isAuthReady, userId]);

const handleSettingsChange = async (key, value, shouldSave = false) => {
const numericValue = Number(value);
if (isNaN(numericValue) && value !== ”) return;

setSettings(prev => ({ …prev, [key]: value === ” ? ” : numericValue }));

if (shouldSave && isAuthReady && userId) {
const settingsRef = doc(db, ‘artifacts’, appId, ‘users’, userId, ‘settings’, ‘app_settings’);
try {
await setDoc(settingsRef, { [key]: numericValue }, { merge: true });
} catch (error) {
console.error(“Error saving settings: “, error);
}
}
};

const renderContent = () => {
if (!isAuthReady) {
return

Cargando…

;
}

switch (activeTab) {
case ‘Finanzas’:
return ;
case ‘Inicio’:
return ;
case ‘Registro de Compras’:
return ;
case ‘Registro de Ventas’:
return ;
case ‘Inventario’:
return ;
case ‘Costeo de Productos’:
return ;
case ‘Reporte F29’:
return ;
default:
return

Seleccione una opción

;
}
};

const navItems = [
{ label: ‘Finanzas’, icon: },
{ label: ‘Inicio’, icon: },
{ label: ‘Registro de Compras’, icon: },
{ label: ‘Registro de Ventas’, icon: },
{ label: ‘Inventario’, icon: },
{ label: ‘Costeo de Productos’, icon: },
{ label: ‘Reporte F29’, icon: },
];

return (

{/* Sidebar */}

{/* Main Content */}


{renderContent()}

);
}