From 3e28e0d52ea730c275301bc8dd697751b010860c Mon Sep 17 00:00:00 2001 From: Flatlogic Bot Date: Mon, 26 Jan 2026 21:17:39 +0000 Subject: [PATCH] SoftDolarcito_v1 --- backend/src/db/api/sales.js | 19 +- frontend/src/config.ts | 4 +- frontend/src/menuAside.ts | 8 +- frontend/src/pages/dashboard.tsx | 552 ++++++++----------------------- frontend/src/pages/index.tsx | 217 ++++-------- frontend/src/pages/pos.tsx | 247 ++++++++++++++ 6 files changed, 473 insertions(+), 574 deletions(-) create mode 100644 frontend/src/pages/pos.tsx 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') && ( + +
+
+

Usuarios

+

{users}

+
+ +
+
+ )} +
+ +
+ +
+

+ + Alertas de Stock Bajo +

+ Ver todos +
+
+ {loadingLowStock ? ( +

Cargando alertas...

+ ) : lowStockProducts.length > 0 ? ( + + + + + + + + + + {lowStockProducts.slice(0, 5).map((p) => ( + + + + + + ))} + +
ProductoStock ActualMínimo
{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) => ( -
-
- - Photo by {image?.photographer} on Pexels - -
+ +
+
+
+ + 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 */} +
+
+
+
+
+ +
+
+
+
+ + {title} +
+
+ Política de Privacidad + Términos de Servicio + Contacto +
+
+
+ © {new Date().getFullYear()} {title} POS. Todos los derechos reservados. +
+
+
); - - const videoBlock = (video) => { - if (video?.video_files?.length > 0) { - return ( -
- -
- - Video by {video.user.name} on Pexels - -
-
) - } - }; - - return ( -
- - {getPageTitle('Starter Page')} - - - -
- {contentType === 'image' && contentPosition !== 'background' - ? imageBlock(illustrationImage) - : null} - {contentType === 'video' && contentPosition !== 'background' - ? videoBlock(illustrationVideo) - : null} -
- - - -
-

This is a React.js/Node.js app generated by the Flatlogic Web App Generator

-

For guides and documentation please check - your local README.md and the Flatlogic documentation

-
- - - - - -
-
-
-
-
-

© 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;