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 }) => (
{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″
/>
Utilidad Neta {selectedYear}
= 0 ? ‘text-emerald-700’ : ‘text-rose-700’}`}>
{formatCurrency(metrics.netFlow)}
{/* Gráfico Comparativo Mensual */}
Comparativa Mensual ({selectedYear})
/>
{/* Historial Detallado Unificado */}
Movimientos Financieros ({selectedYear})
| 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)
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)
| 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
{/* Historial de Compras */}
Historial de Compras
| 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
Historial de Ventas
| 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)
| 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
| 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
{editingProductId ? ‘Editando Producto’ : ‘Costear Nuevo Producto’}
{/* Lista de productos guardados */}
Productos Guardados
className=”mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500″
/>
return (
{p.name}
{p.sku &&
SKU: {p.sku}
} {/* Mostrar SKU */}
-
{p.inventoryItemsUsed?.map((item, i) => {
- {item.quantity} {invItem?.unit || ”} de {invItem?.name || ‘N/A’}
- {item.name}: {formatCurrency(item.value)}
- {item.minutes} min. de {item.machine}
const invItem = inventory.find(inv => inv.id === item.inventoryId);
return
})}
{p.manualCostItems?.map((item, i) =>
)}
{p.energyItemsUsed?.map((item, i) =>
)}
)
})}
);
};
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”
/>
Resumen de Ventas
))}
{/* Purchases Section */}
Resumen de Compras (Válidas para IVA)
))}
{/* Fixed Assets Section */}
{reportData.monthlyFixedAssets.length > 0 && (
Activos Fijos Comprados en el Mes
| 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
{formatCurrency(reportData.totalIvaDebit)}
{formatCurrency(reportData.totalIvaCredit)}
{formatCurrency(reportData.ivaCreditCarryover)}
{reportData.ivaToPay >= 0 ? ‘IVA A PAGAR:’ : ‘REMANENTE CRÉDITO:’}
{formatCurrency(Math.abs(reportData.ivaToPay))}
{formatCurrency(reportData.ppmAmount)}
{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
;
}
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
;
}
};
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 (
{/* Main Content */}
{renderContent()}
);
}
