diff --git a/backend/src/db/api/sales.js b/backend/src/db/api/sales.js
index 5128ece..95670d2 100644
--- a/backend/src/db/api/sales.js
+++ b/backend/src/db/api/sales.js
@@ -1,4 +1,3 @@
-
const db = require('../models');
const FileDBApi = require('./file');
const crypto = require('crypto');
@@ -68,11 +67,16 @@ module.exports = class SalesDBApi {
{ transaction },
);
-
-
-
-
-
+ if (data.sale_items && data.sale_items.length > 0) {
+ for (const item of data.sale_items) {
+ await db.sale_items.create({
+ ...item,
+ saleId: sales.id,
+ createdById: currentUser.id,
+ updatedById: currentUser.id,
+ }, { transaction });
+ }
+ }
return sales;
}
@@ -573,5 +577,4 @@ module.exports = class SalesDBApi {
}
-};
-
+};
\ No newline at end of file
diff --git a/frontend/src/config.ts b/frontend/src/config.ts
index a9783c8..3914980 100644
--- a/frontend/src/config.ts
+++ b/frontend/src/config.ts
@@ -8,8 +8,8 @@ export const localStorageStyleKey = 'style'
export const containerMaxW = 'xl:max-w-full xl:mx-auto 2xl:mx-20'
-export const appTitle = 'created by Flatlogic generator!'
+export const appTitle = 'El dolarcito POS'
export const getPageTitle = (currentPageTitle: string) => `${currentPageTitle} — ${appTitle}`
-export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''
+export const tinyKey = process.env.NEXT_PUBLIC_TINY_KEY || ''
\ No newline at end of file
diff --git a/frontend/src/menuAside.ts b/frontend/src/menuAside.ts
index 87d3d13..2593e66 100644
--- a/frontend/src/menuAside.ts
+++ b/frontend/src/menuAside.ts
@@ -7,6 +7,12 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiViewDashboardOutline,
label: 'Dashboard',
},
+ {
+ href: '/pos',
+ icon: icon.mdiCashRegister,
+ label: 'Punto de Venta',
+ permissions: 'CREATE_SALES'
+ },
{
href: '/users/users-list',
@@ -128,4 +134,4 @@ const menuAside: MenuAsideItem[] = [
},
]
-export default menuAside
+export default menuAside
\ No newline at end of file
diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx
index 23e5d29..d591352 100644
--- a/frontend/src/pages/dashboard.tsx
+++ b/frontend/src/pages/dashboard.tsx
@@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton
import BaseIcon from "../components/BaseIcon";
import { getPageTitle } from '../config'
import Link from "next/link";
+import CardBox from '../components/CardBox';
+import BaseButton from '../components/BaseButton';
import { hasPermission } from "../helpers/userPermissions";
import { fetchWidgets } from '../stores/roles/rolesSlice';
@@ -16,52 +18,36 @@ import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
+
const Dashboard = () => {
const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
- const loadingMessage = 'Loading...';
+ const loadingMessage = 'Cargando...';
-
const [users, setUsers] = React.useState(loadingMessage);
- const [roles, setRoles] = React.useState(loadingMessage);
- const [permissions, setPermissions] = React.useState(loadingMessage);
const [products, setProducts] = React.useState(loadingMessage);
- const [categories, setCategories] = React.useState(loadingMessage);
- const [suppliers, setSuppliers] = React.useState(loadingMessage);
- const [purchases, setPurchases] = React.useState(loadingMessage);
- const [purchase_items, setPurchase_items] = React.useState(loadingMessage);
const [sales, setSales] = React.useState(loadingMessage);
- const [sale_items, setSale_items] = React.useState(loadingMessage);
- const [cash_movements, setCash_movements] = React.useState(loadingMessage);
- const [inventory_movements, setInventory_movements] = React.useState(loadingMessage);
- const [settings, setSettings] = React.useState(loadingMessage);
+ const [lowStockProducts, setLowStockProducts] = React.useState([]);
+ const [loadingLowStock, setLoadingLowStock] = React.useState(true);
-
- const [widgetsRole, setWidgetsRole] = React.useState({
- role: { value: '', label: '' },
- });
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
-
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
-
async function loadData() {
- const entities = ['users','roles','permissions','products','categories','suppliers','purchases','purchase_items','sales','sale_items','cash_movements','inventory_movements','settings',];
- const fns = [setUsers,setRoles,setPermissions,setProducts,setCategories,setSuppliers,setPurchases,setPurchase_items,setSales,setSale_items,setCash_movements,setInventory_movements,setSettings,];
+ const entities = ['users','products','sales'];
+ const fns = [setUsers,setProducts,setSales];
const requests = entities.map((entity, index) => {
-
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({data: {count: null}});
}
-
});
Promise.allSettled(requests).then((results) => {
@@ -74,49 +60,155 @@ const Dashboard = () => {
});
});
}
-
+
+ async function loadLowStock() {
+ if (!hasPermission(currentUser, 'READ_PRODUCTS')) return;
+ try {
+ setLoadingLowStock(true);
+ const response = await axios.get('/products');
+ const productsData = response.data.rows || [];
+ const lowStock = productsData.filter(p => p.stock <= (p.min_stock || 0));
+ setLowStockProducts(lowStock);
+ } catch (error) {
+ console.error('Error loading low stock products:', error);
+ } finally {
+ setLoadingLowStock(false);
+ }
+ }
+
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
+
React.useEffect(() => {
if (!currentUser) return;
loadData().then();
- setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
+ loadLowStock().then();
}, [currentUser]);
React.useEffect(() => {
- if (!currentUser || !widgetsRole?.role?.value) return;
- getWidgets(widgetsRole?.role?.value || '').then();
- }, [widgetsRole?.role?.value]);
+ if (!currentUser || !currentUser?.app_role?.id) return;
+ getWidgets(currentUser.app_role.id).then();
+ }, [currentUser]);
return (
<>
-
- {getPageTitle('Overview')}
-
+ {getPageTitle('Panel de Control')}
- {''}
+
-
+
+
+ {hasPermission(currentUser, 'READ_SALES') && (
+
+
+
+
Ventas Totales
+
{sales}
+
+
+
+
+ )}
+ {hasPermission(currentUser, 'READ_PRODUCTS') && (
+
+
+
+
Productos
+
{products}
+
+
+
+
+ )}
+ {hasPermission(currentUser, 'READ_USERS') && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ Alertas de Stock Bajo
+
+ Ver todos
+
+
+ {loadingLowStock ? (
+
Cargando alertas...
+ ) : lowStockProducts.length > 0 ? (
+
+
+
+ | Producto |
+ Stock Actual |
+ Mínimo |
+
+
+
+ {lowStockProducts.slice(0, 5).map((p) => (
+
+ | {p.name} |
+ {p.stock} |
+ {p.min_stock} |
+
+ ))}
+
+
+ ) : (
+
No hay productos con stock bajo 🎉
+ )}
+
+
+
+
+
+
+
Acceso Rápido al POS
+
Comienza a vender ahora mismo con la interfaz optimizada de El dolarcito.
+
+
+
+
+
+
+
+
{hasPermission(currentUser, 'CREATE_ROLES') && { return; }}
+ widgetsRole={{ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }}
/>}
- {!!rolesWidgets.length &&
- hasPermission(currentUser, 'CREATE_ROLES') && (
-
- {`${widgetsRole?.role?.label || 'Users'}'s widgets`}
-
- )}
-
+
{(isFetchingQuery || loading) && (
@@ -127,7 +219,7 @@ const Dashboard = () => {
size={48}
path={icon.mdiLoading}
/>{' '}
- Loading widgets...
+ Cargando widgets...
)}
@@ -137,383 +229,11 @@ const Dashboard = () => {
key={widget.id}
userId={currentUser?.id}
widget={widget}
- roleId={widgetsRole?.role?.value || ''}
+ roleId={currentUser?.app_role?.id || ''}
admin={hasPermission(currentUser, 'CREATE_ROLES')}
/>
))}
-
- {!!rolesWidgets.length &&
}
-
-
-
-
- {hasPermission(currentUser, 'READ_USERS') &&
-
-
-
-
- Users
-
-
- {users}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_ROLES') &&
-
-
-
-
- Roles
-
-
- {roles}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PERMISSIONS') &&
-
-
-
-
- Permissions
-
-
- {permissions}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PRODUCTS') &&
-
-
-
-
- Products
-
-
- {products}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CATEGORIES') &&
-
-
-
-
- Categories
-
-
- {categories}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SUPPLIERS') &&
-
-
-
-
- Suppliers
-
-
- {suppliers}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PURCHASES') &&
-
-
-
-
- Purchases
-
-
- {purchases}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_PURCHASE_ITEMS') &&
-
-
-
-
- Purchase items
-
-
- {purchase_items}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SALES') &&
-
-
-
-
- Sales
-
-
- {sales}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SALE_ITEMS') &&
-
-
-
-
- Sale items
-
-
- {sale_items}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_CASH_MOVEMENTS') &&
-
-
-
-
- Cash movements
-
-
- {cash_movements}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_INVENTORY_MOVEMENTS') &&
-
-
-
-
- Inventory movements
-
-
- {inventory_movements}
-
-
-
-
-
-
-
- }
-
- {hasPermission(currentUser, 'READ_SETTINGS') &&
-
-
-
-
- Settings
-
-
- {settings}
-
-
-
-
-
-
-
- }
-
-
-
>
)
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx
index c4dab4d..2b7bb81 100644
--- a/frontend/src/pages/index.tsx
+++ b/frontend/src/pages/index.tsx
@@ -1,166 +1,89 @@
-import React, { useEffect, useState } from 'react';
+import React from 'react';
import type { ReactElement } from 'react';
import Head from 'next/head';
import Link from 'next/link';
import BaseButton from '../components/BaseButton';
-import CardBox from '../components/CardBox';
import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest';
-import BaseDivider from '../components/BaseDivider';
-import BaseButtons from '../components/BaseButtons';
import { getPageTitle } from '../config';
-import { useAppSelector } from '../stores/hooks';
-import CardBoxComponentTitle from "../components/CardBoxComponentTitle";
-import { getPexelsImage, getPexelsVideo } from '../helpers/pexels';
-
+import { mdiCashRegister, mdiChevronRight } from '@mdi/js';
+import BaseIcon from '../components/BaseIcon';
export default function Starter() {
- const [illustrationImage, setIllustrationImage] = useState({
- src: undefined,
- photographer: undefined,
- photographer_url: undefined,
- })
- const [illustrationVideo, setIllustrationVideo] = useState({video_files: []})
- const [contentType, setContentType] = useState('image');
- const [contentPosition, setContentPosition] = useState('right');
- const textColor = useAppSelector((state) => state.style.linkColor);
-
const title = 'El dolarcito'
- // Fetch Pexels image/video
- useEffect(() => {
- async function fetchData() {
- const image = await getPexelsImage();
- const video = await getPexelsVideo();
- setIllustrationImage(image);
- setIllustrationVideo(video);
- }
- fetchData();
- }, []);
+ return (
+
+
+
{getPageTitle('Bienvenido')}
+
- const imageBlock = (image) => (
-
-
+
+
+
+
+
+ Sistema POS Profesional
+
+
+
+ Control total de tu negocio con El dolarcito
+
+
+
+ Gestiona ventas, inventario, compras y caja en una sola plataforma rápida, segura y responsive.
+
+
+
+
+
+ Ver demostración
+
+
+
+
+
+
+ {/* Decorative elements */}
+
+
+
+
+
+
+
);
-
- const videoBlock = (video) => {
- if (video?.video_files?.length > 0) {
- return (
-
-
-
-
)
- }
- };
-
- return (
-
-
-
{getPageTitle('Starter Page')}
-
-
-
-
- {contentType === 'image' && contentPosition !== 'background'
- ? imageBlock(illustrationImage)
- : null}
- {contentType === 'video' && contentPosition !== 'background'
- ? videoBlock(illustrationVideo)
- : null}
-
-
-
-
-
© 2026 {title}. All rights reserved
-
- Privacy Policy
-
-
-
-
- );
}
Starter.getLayout = function getLayout(page: ReactElement) {
- return
{page};
+ return
{page};
};
-
diff --git a/frontend/src/pages/pos.tsx b/frontend/src/pages/pos.tsx
new file mode 100644
index 0000000..eb3cd51
--- /dev/null
+++ b/frontend/src/pages/pos.tsx
@@ -0,0 +1,247 @@
+import * as icon from '@mdi/js';
+import Head from 'next/head';
+import React, { useState, useEffect } from 'react';
+import axios from 'axios';
+import type { ReactElement } from 'react';
+import LayoutAuthenticated from '../layouts/Authenticated';
+import SectionMain from '../components/SectionMain';
+import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton';
+import BaseIcon from "../components/BaseIcon";
+import CardBox from '../components/CardBox';
+import BaseButton from '../components/BaseButton';
+import FormField from '../components/FormField';
+import { getPageTitle } from '../config';
+import { useAppSelector } from '../stores/hooks';
+import { toast } from 'react-toastify';
+
+const POSPage = () => {
+ const [products, setProducts] = useState([]);
+ const [search, setSearch] = useState('');
+ const [cart, setCart] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [submitting, setSubmitting] = useState(false);
+ const { currentUser } = useAppSelector((state) => state.auth);
+
+ useEffect(() => {
+ fetchProducts();
+ }, []);
+
+ const fetchProducts = async () => {
+ try {
+ setLoading(true);
+ const response = await axios.get('/products');
+ setProducts(response.data.rows || []);
+ } catch (error) {
+ console.error('Error fetching products:', error);
+ toast.error('Error al cargar productos');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const addToCart = (product) => {
+ const existing = cart.find(item => item.id === product.id);
+ if (existing) {
+ setCart(cart.map(item =>
+ item.id === product.id
+ ? { ...item, quantity: item.quantity + 1, subtotal: (item.quantity + 1) * item.sale_price }
+ : item
+ ));
+ } else {
+ setCart([...cart, {
+ ...product,
+ quantity: 1,
+ unit_price: product.sale_price,
+ subtotal: product.sale_price,
+ productId: product.id
+ }]);
+ }
+ };
+
+ const removeFromCart = (productId) => {
+ setCart(cart.filter(item => item.id !== productId));
+ };
+
+ const updateQuantity = (productId, delta) => {
+ setCart(cart.map(item => {
+ if (item.id === productId) {
+ const newQty = Math.max(1, item.quantity + delta);
+ return { ...item, quantity: newQty, subtotal: newQty * item.sale_price };
+ }
+ return item;
+ }));
+ };
+
+ const total = cart.reduce((acc, item) => acc + Number(item.subtotal), 0);
+
+ const handleCheckout = async () => {
+ if (cart.length === 0) return;
+ try {
+ setSubmitting(true);
+ const saleData = {
+ document_number: `POS-${Date.now()}`,
+ sale_date: new Date().toISOString(),
+ total: total,
+ status: 'PAGADA',
+ customer_name: 'Cliente General',
+ sale_items: cart.map(item => ({
+ productId: item.productId,
+ product_name: item.name,
+ quantity: item.quantity,
+ unit_price: item.unit_price,
+ subtotal: item.subtotal
+ }))
+ };
+
+ await axios.post('/sales', { data: saleData });
+ toast.success('Venta completada con éxito');
+ setCart([]);
+ } catch (error) {
+ console.error('Error in checkout:', error);
+ toast.error('Error al procesar la venta');
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ const filteredProducts = products.filter(p =>
+ p.name.toLowerCase().includes(search.toLowerCase()) ||
+ (p.sku && p.sku.toLowerCase().includes(search.toLowerCase()))
+ );
+
+ return (
+ <>
+
+
{getPageTitle('Punto de Venta')}
+
+
+
+
+
+
+
+ {/* Product Selection */}
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+
+
+
+ {loading ? (
+
Cargando productos...
+ ) : filteredProducts.length > 0 ? (
+ filteredProducts.map((p) => (
+
+ ))
+ ) : (
+
No se encontraron productos
+ )}
+
+
+
+ {/* Cart Summary */}
+
+
+
+
+
+ Carrito
+
+
+ {cart.length} items
+
+
+
+
+ {cart.length > 0 ? (
+ cart.map((item) => (
+
+
+ {item.name}
+
+
+
+
+
+ {item.quantity}
+
+
+
${Number(item.subtotal).toFixed(2)}
+
+
+ ))
+ ) : (
+
+
+
El carrito está vacío
+
+ )}
+
+
+
+
+ Subtotal:
+ ${total.toFixed(2)}
+
+
+ Total:
+ ${total.toFixed(2)}
+
+
+
+
+
+
+
+ >
+ );
+};
+
+POSPage.getLayout = function getLayout(page: ReactElement) {
+ return
{page};
+};
+
+export default POSPage;