Hola estoy haciendo un programa en electron y vite.
y estoy teniendo problemas.
El programa es para gestionar stock, o sea tener un stock de insumos, y productos.
los insumos son los que componen el producto.
ejemplo:
harina para el pan que es el producto.
Y quiero organizar mejor.

Ahora tengo un modal que se abre en inventario y en sidebar.
si lo habro en inventario esta todo perfecto funciona bien, pero en el sidebar no guarda nada, como si no funcionara.

El otro problema que tengo es el calcular las cosas.
si agrego un producto se abre una ventana que es para rellenar que insumos ocupa ese producto, quiero agregarle las unidades de medida.
por ejemplo si tienes leche 3litros.
pero ocupas 2 tazas de leche.
alguien tiene que rellenar ese campo. y al lado se tiene que mostrar los precios del insumo basado solo en 2 tazas de leche y no de los 3litros que hay en total en stock.

Primero se cargan los insumos y luego se cargan los productos que ocupan eso que esta en stock me entiendes.
al final del modal cuando se selecciona los insumos debe mostrar el precio del costo o sea de las tazas de leche mas la harina o lo que sea.
Me puedes ayudar en eso.
Si necesitas algun archivo mas me lo pides.

import React, { useState } from 'react';
import CloseIcon from '@mui/icons-material/Close';
import "./ModalAdd.css";

const unidadesDisponibles = ["unidad", "kg", "g", "litro", "ml", "cucharada", "taza", "pieza"];

const ModalAdd = ({ onClose, onItemAdded, onProductoCreado }) => {
const [tipo, setTipo] = useState('insumo');
const [nombre, setNombre] = useState('');
const [cantidad, setCantidad] = useState('');
const [unidad, setUnidad] = useState('');
const [precio, setPrecio] = useState('');
const [loading, setLoading] = useState(false);

const handleSubmit = async e => {
e.preventDefault();
if (loading) return;

if (!nombre.trim()) {
  alert('Completa el campo nombre');
  return;
}
if (!precio || isNaN(parseFloat(precio))) {
  alert('Completa el campo precio correctamente');
  return;
}
if (tipo === 'insumo' && !unidad.trim()) {
  alert('Completa el campo unidad');
  return;
}

setLoading(true);

try {
  if (tipo === 'producto') {
    const productoCreado = await window.api.addProducto({
      nombre: nombre.trim(),
      precio: parseFloat(precio),
    });
    if (onProductoCreado) {
      onProductoCreado(productoCreado.id);
    } else {
      onItemAdded && onItemAdded();
      onClose();
    }
  } else {
    await window.api.addIngrediente({
      nombre: nombre.trim(),
      cantidad: cantidad ? parseFloat(cantidad) : 0,
      unidad: unidad.trim(),
      precio: parseFloat(precio),
    });
    onItemAdded && onItemAdded();
    onClose();
  }
} catch (error) {
  console.error('Error agregando:', error);
  alert('Error al agregar, revisa la consola');
} finally {
  setLoading(false);
}


};

return (
<div className="modal___overlay">
<div className="modal__container">
<button onClick={onClose} className="modal_close" disabled={loading}>
<CloseIcon />
</button>

<h2>Agregar {tipo === 'producto' ? 'Producto' : 'Insumo'}</h2>

    <form className='modal__form' onSubmit={handleSubmit}>

      <label>
        Tipo:
        <select value={tipo} onChange={e => setTipo(e.target.value)} disabled={loading}>
          <option value="insumo">Insumo</option>
          <option value="producto">Producto</option>
        </select>
      </label>

      <input
        placeholder="Nombre"
        value={nombre}
        onChange={e => setNombre(e.target.value)}
        required
        disabled={loading}
      />

      <input
        placeholder="Precio"
        type="number"
        step="0.01"
        value={precio}
        onChange={e => setPrecio(e.target.value)}
        required
        disabled={loading}
      />

      {tipo === 'insumo' && (
        <>
          <input
            placeholder="Cantidad"
            type="number"
            value={cantidad}
            onChange={e => setCantidad(e.target.value)}
            disabled={loading}
          />
          <select
            value={unidad}
            onChange={e => setUnidad(e.target.value)}
            required
            disabled={loading}
            style={{ marginTop: '0.5rem' }}
          >
            <option value="">Selecciona una unidad</option>
            {unidadesDisponibles.map(u => (
              <option key={u} value={u}>{u}</option>
            ))}
          </select>
        </>
      )}

      <button className='modal__submit' type="submit" disabled={loading}>
        {loading ? 'Agregando...' : 'Agregar'}
      </button>
    </form>
  </div>
</div>
IGNORE_WHEN_COPYING_START
content_copy
download
Use code with caution.
IGNORE_WHEN_COPYING_END

);
};

export default ModalAdd;

import React, { useState } from 'react';
import { Outlet } from 'react-router-dom';
import Sidebar from './Sidebar/Sidebar';
import Header from './Header/Header';
import ModalAdd from '@/ui/ModalAdd/ModalAdd';
import './Layout.css';

const Layout = () => {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [modalOpen, setModalOpen] = useState(false);

const toggleSidebar = () => setSidebarCollapsed(prev => !prev);
const openModal = () => setModalOpen(true);
const closeModal = () => setModalOpen(false);

return (
<div className={app-layout${sidebarCollapsed ? ' collapsed' : ''}}>
<Sidebar
isCollapsed={sidebarCollapsed}
toggleSidebar={toggleSidebar}
onOpenModal={openModal}
/>
<div className="main-container">
<Header />
<main className="app-content">
<Outlet />
</main>
</div>

{modalOpen && <ModalAdd onClose={closeModal} />}
</div>
IGNORE_WHEN_COPYING_START
content_copy
download
Use code with caution.
IGNORE_WHEN_COPYING_END

);
};

export default Layout;

import React, { useState, useEffect } from 'react';
import { NavLink } from 'react-router-dom';
import styles from './Sidebar.module.css';
import HomeIcon from '@mui/icons-material/Home';
import InventoryIcon from '@mui/icons-material/Inventory';
import EditNoteIcon from '@mui/icons-material/EditNote';
import GroupIcon from '@mui/icons-material/Group';
import AddIcon from '@mui/icons-material/Add';
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
import ChevronRightIcon from '@mui/icons-material/ChevronRight';

const Sidebar = ({ isCollapsed, toggleSidebar, onOpenModal }) => {
const [isMobileView, setIsMobileView] = useState(window.innerWidth < 768);

useEffect(() => {
const handleResize = () => {
setIsMobileView(window.innerWidth < 768);
};

window.addEventListener('resize', handleResize);
handleResize();

return () => window.removeEventListener('resize', handleResize);
IGNORE_WHEN_COPYING_START
content_copy
download
Use code with caution.
IGNORE_WHEN_COPYING_END

}, []);

return (
<aside className={${styles['sidebar-container']} ${isCollapsed && !isMobileView ? styles.closed : ''}}>
<div className={styles['sidebar-content']}>
<div className={styles['sidebar-logo-container']}>
{isCollapsed && !isMobileView ? (
<NavLink to="/" className={styles['sidebar-logo-link']} title="Inicio">
<div style={{ fontWeight: 'bold', fontSize: '1.5rem', color: '#cbd5e1' }}>I</div>
</NavLink>
) : (
<NavLink to="/" className={styles['sidebar-logo-link']} title="Inicio">
<h2 style={{ color: '#f8fafc', margin: 0 }}>Inventary</h2>
</NavLink>
)}
</div>

<nav className={styles['sidebar-items']}>
      <NavLink
        to="/"
        className={({ isActive }) =>
          isActive
            ? `${styles['sidebar-item']} ${styles.active}`
            : styles['sidebar-item']
        }
        title="Inventario"
      >
        <HomeIcon className={styles['sidebar-icon']} />
        <span className={styles['sidebar-item-text']}>Inventario</span>
      </NavLink>

      <NavLink
        to="/productos"
        className={({ isActive }) =>
          isActive
            ? `${styles['sidebar-item']} ${styles.active}`
            : styles['sidebar-item']
        }
        title="Productos"
      >
        <InventoryIcon className={styles['sidebar-icon']} />
        <span className={styles['sidebar-item-text']}>Productos</span>
      </NavLink>

      <NavLink
        to="/recetas"
        className={({ isActive }) =>
          isActive
            ? `${styles['sidebar-item']} ${styles.active}`
            : styles['sidebar-item']
        }
        title="Recetas"
      >
        <EditNoteIcon className={styles['sidebar-icon']} />
        <span className={styles['sidebar-item-text']}>Recetas</span>
      </NavLink>

      <NavLink
        to="/reportes"
        className={({ isActive }) =>
          isActive
            ? `${styles['sidebar-item']} ${styles.active}`
            : styles['sidebar-item']
        }
        title="Reportes"
      >
        <GroupIcon className={styles['sidebar-icon']} />
        <span className={styles['sidebar-item-text']}>Reportes</span>
      </NavLink>

      <button
        className={styles['sidebar-item']}
        title="Agregar nuevo"
        onClick={() => onOpenModal()}
        type="button"
      >
        <AddIcon className={styles['sidebar-icon']} />
        <span className={styles['sidebar-item-text']}>Agregar</span>
      </button>
    </nav>

    <button
      onClick={toggleSidebar}
      className={styles['sidebar-toggle-btn']}
      title={isCollapsed ? "Expandir" : "Colapsar"}
      aria-label={isCollapsed ? "Expandir barra lateral" : "Colapsar barra lateral"}
    >
      {isCollapsed ? (
        <ChevronRightIcon className={styles['sidebar-icon']} />
      ) : (
        <ChevronLeftIcon className={styles['sidebar-icon']} />
      )}
    </button>
  </div>
</aside>
IGNORE_WHEN_COPYING_START
content_copy
download
Use code with caution.
IGNORE_WHEN_COPYING_END

);
};

export default Sidebar;

aca es donde se muestra el stock.
import React, { useState, useEffect } from 'react';
import ProductList from '@/ui/Inventory/ProductList';
import IngredientList from '@/ui/Inventory/IngredientList';
import ModalAdd from '@/ui/ModalAdd/ModalAdd';
import ModalRecipe from '@/ui/ModalRecipe/ModalRecipe';

const Inventory = () => {
const [vista, setVista] = useState('productos');
const [productos, setProductos] = useState([]);
const [ingredientes, setIngredientes] = useState([]);
const [modalAddOpen, setModalAddOpen] = useState(false);
const [modalRecetaOpen, setModalRecetaOpen] = useState(false);
const [nuevoProductoId, setNuevoProductoId] = useState(null);

const cargarDatos = async () => {
if (vista === 'productos') {
const prods = await window.api.getProductos();
setProductos(prods);
} else if (vista === 'ingredientes') {
const ingred = await window.api.getIngredientes();
setIngredientes(ingred);
}
};

useEffect(() => {
cargarDatos();
}, [vista]);

const abrirModalAdd = () => setModalAddOpen(true);
const cerrarModalAdd = () => setModalAddOpen(false);

const abrirModalReceta = () => setModalRecetaOpen(true);
const cerrarModalReceta = () => setModalRecetaOpen(false);

const onItemAdded = () => {
cargarDatos();
};

const onProductoCreado = (productoId) => {
setNuevoProductoId(productoId);
cerrarModalAdd();
abrirModalReceta();
};

return (
<div className="container">
<div className="sidebar-buttons">
<button onClick={() => setVista('productos')}>Productos</button>
<button onClick={() => setVista('ingredientes')}>Ingredientes</button>
<button onClick={abrirModalAdd}>Agregar</button>
</div>

<div className="main-section">
    {vista === 'productos' && (
      <ProductList productos={productos} />
    )}
    {vista === 'ingredientes' && (
      <IngredientList ingredientes={ingredientes} />
    )}

    {modalAddOpen && (
      <ModalAdd
        onClose={cerrarModalAdd}
        onItemAdded={onItemAdded}
        onProductoCreado={onProductoCreado}
      />
    )}

    {modalRecetaOpen && nuevoProductoId && (
      <ModalRecipe
        productoId={nuevoProductoId}
        onClose={() => {
          cerrarModalReceta();
          onItemAdded();
          setNuevoProductoId(null);
        }}
      />
    )}
  </div>
</div>
IGNORE_WHEN_COPYING_START
content_copy
download
Use code with caution.
IGNORE_WHEN_COPYING_END

);
};

export default Inventory;

si necesitas algun archivo mas me dices.
Por cierto esta base de datos es todo local no hay nada con internet es la idea.
const { app, BrowserWindow, ipcMain, Menu } = require('electron');
const path = require('path');
const db = require('./db/database');

function createMenu() {
const isMac = process.platform === 'darwin';

const template = [
...(isMac
? [{
label: app.name,
submenu: [{ role: 'quit', label: 'Salir' }],
}]
: [{
label: 'Archivo',
submenu: [{ role: 'quit', label: 'Salir' }],
}]
),
{
label: 'Exportar',
submenu: [
{
label: 'Exportar Insumos',
click: () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('menu-exportar-insumos');
}
},
{
label: 'Exportar Productos',
click: () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('menu-exportar-productos');
}
},
{
label: 'Exportar Ventas',
enabled: false,
click: () => {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow) focusedWindow.webContents.send('menu-exportar-ventas');
}
}
]
},
{
label: 'Vista',
submenu: [
{
label: 'Recargar',
accelerator: 'CmdOrCtrl+R',
click: (_, focusedWindow) => {
if (focusedWindow) focusedWindow.reload();
}
}
]
}
];

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}

function createWindow() {
const win = new BrowserWindow({
width: 1024,
height: 768,
minWidth: 800,
minHeight: 600,
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
}
});

if (app.isPackaged) {
win.loadFile(path.join(__dirname, '../../dist/index.html'));
} else {
win.loadURL('http://localhost:5173');
win.webContents.openDevTools();
}

createMenu();
}

app.whenReady().then(() => {
createWindow();
});

app.on('window-all-closed', () => {
if (process.platform !== 'darwin') app.quit();
});

app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

// IPC handlers
ipcMain.handle('ping', () => 'pong');

ipcMain.handle('get-ingredientes', () => {
return db.prepare('SELECT * FROM ingredientes').all();
});

ipcMain.handle('add-ingrediente', (_, ingrediente) => {
const stmt = db.prepare(
'INSERT INTO ingredientes (nombre, cantidad, unidad, precio) VALUES (?, ?, ?, ?)'
);
const info = stmt.run(ingrediente.nombre, ingrediente.cantidad, ingrediente.unidad, ingrediente.precio);
return { id: info.lastInsertRowid, ...ingrediente };
});

ipcMain.handle('get-productos', () => {
return db.prepare('SELECT * FROM productos').all();
});

ipcMain.handle('add-producto', (_, producto) => {
const stmt = db.prepare('INSERT INTO productos (nombre, precio) VALUES (?, ?)');
const info = stmt.run(producto.nombre, producto.precio);
return { id: info.lastInsertRowid, ...producto };
});

ipcMain.handle('add-producto-insumo', (_, data) => {
const { producto_id, ingrediente_id, cantidad } = data;
const stmt = db.prepare('INSERT INTO producto_insumo (producto_id, ingrediente_id, cantidad) VALUES (?, ?, ?)');
const info = stmt.run(producto_id, ingrediente_id, cantidad);
return { id: info.lastInsertRowid, producto_id, ingrediente_id, cantidad };
});

ipcMain.handle('get-receta-producto', (_, producto_id) => {
const stmt = db.prepare(SELECT i.*, pi.cantidad FROM producto_insumo pi JOIN ingredientes i ON i.id = pi.ingrediente_id WHERE pi.producto_id = ?);
return stmt.all(producto_id);
});

ipcMain.handle('eliminar-producto', (_, id) => {
const stmt = db.prepare('DELETE FROM productos WHERE id = ?');
const info = stmt.run(id);
return info.changes > 0;
});

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
ping: () => ipcRenderer.invoke('ping'),

getIngredientes: () => ipcRenderer.invoke('get-ingredientes'),
addIngrediente: (ingrediente) => ipcRenderer.invoke('add-ingrediente', ingrediente),

getProductos: () => ipcRenderer.invoke('get-productos'),
addProducto: (producto) => ipcRenderer.invoke('add-producto', producto),
eliminarProducto: (id) => ipcRenderer.invoke('eliminar-producto', id),

addProductoInsumo: (data) => ipcRenderer.invoke('add-producto-insumo', data),
getRecetaProducto: (productoId) => ipcRenderer.invoke('get-receta-producto', productoId),

onExportInsumos: (callback) => ipcRenderer.on('menu-exportar-insumos', callback),
onExportProductos: (callback) => ipcRenderer.on('menu-exportar-productos', callback),
onExportVentas: (callback) => ipcRenderer.on('menu-exportar-ventas', callback),
});

const path = require('path');
const { app } = require('electron');
const Database = require('better-sqlite3');

const dbPath = path.join(app.getPath('userData'), 'data.db');
const db = new Database(dbPath);

// Crear tabla ingredientes
db.prepare(CREATE TABLE IF NOT EXISTS ingredientes ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, cantidad REAL NOT NULL, unidad TEXT NOT NULL, precio REAL DEFAULT 0 )).run();

db.prepare(CREATE TABLE IF NOT EXISTS productos ( id INTEGER PRIMARY KEY AUTOINCREMENT, nombre TEXT NOT NULL, precio REAL DEFAULT 0 )).run();

db.prepare(CREATE TABLE IF NOT EXISTS producto_insumo ( id INTEGER PRIMARY KEY AUTOINCREMENT, producto_id INTEGER NOT NULL, ingrediente_id INTEGER NOT NULL, cantidad REAL NOT NULL, FOREIGN KEY(producto_id) REFERENCES productos(id), FOREIGN KEY(ingrediente_id) REFERENCES ingredientes(id) )).run();

//usuarios
db.prepare(CREATE TABLE IF NOT EXISTS usuarios ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL, role TEXT NOT NULL -- 'noble' o 'plebeyo' )).run();

module.exports = db;