diff --git a/backend/src/index.js b/backend/src/index.js index 2e35bd9..20a8ebd 100644 --- a/backend/src/index.js +++ b/backend/src/index.js @@ -12,6 +12,7 @@ const swaggerUI = require('swagger-ui-express'); const swaggerJsDoc = require('swagger-jsdoc'); const authRoutes = require('./routes/auth'); +const publicRoutes = require('./routes/public'); const fileRoutes = require('./routes/file'); const searchRoutes = require('./routes/search'); const sqlRoutes = require('./routes/sql'); @@ -123,6 +124,7 @@ require('./auth/auth'); app.use(bodyParser.json()); +app.use('/api/public', publicRoutes); app.use('/api/auth', authRoutes); app.use('/api/file', fileRoutes); app.use('/api/pexels', pexelsRoutes); diff --git a/backend/src/routes/public.js b/backend/src/routes/public.js new file mode 100644 index 0000000..f73a572 --- /dev/null +++ b/backend/src/routes/public.js @@ -0,0 +1,67 @@ + +const express = require('express'); +const router = express.Router(); +const db = require('../db/models'); +const wrapAsync = require('../helpers').wrapAsync; +const { LocalAIApi } = require("../ai/LocalAIApi"); + +// Get Top 3 Products +router.get('/top-products', wrapAsync(async (req, res) => { + const products = await db.products.findAll({ + where: { status: 'active' }, + order: [['total_sales_count', 'DESC']], + limit: 3, + include: [{ + model: db.file, + as: 'images', + }] + }); + res.status(200).send(products); +})); + +// Get FAQs for Chatbot +router.get('/faq', wrapAsync(async (req, res) => { + const faqs = await db.faq_articles.findAll({ + order: [['sort_order', 'ASC']], + }); + res.status(200).send(faqs); +})); + +// Public Chatbot endpoint +router.post('/chatbot', wrapAsync(async (req, res) => { + const { message } = req.body; + + // Fetch FAQs to provide context + const faqs = await db.faq_articles.findAll(); + const faqContext = faqs.map(f => `Q: ${f.question}\nA: ${f.answer}`).join('\n\n'); + + const prompt = ` + You are a helpful customer support assistant for our Shopify store. + Use the following FAQ information to answer the user's question. + If the answer is not in the FAQ, be polite and ask them to contact support. + + FAQ Context: + ${faqContext} + + User Question: ${message} + `; + + const resp = await LocalAIApi.createResponse( + { + input: [ + { role: "system", content: "You are a helpful customer support assistant." }, + { role: "user", content: prompt }, + ], + }, + { poll_interval: 2, poll_timeout: 60 } + ); + + if (resp.success) { + const text = LocalAIApi.extractText(resp); + res.status(200).send({ text }); + } else { + res.status(500).send({ error: "AI failed to respond" }); + } +})); + +module.exports = router; diff --git a/frontend/src/components/ChatbotWidget.tsx b/frontend/src/components/ChatbotWidget.tsx new file mode 100644 index 0000000..28774e1 --- /dev/null +++ b/frontend/src/components/ChatbotWidget.tsx @@ -0,0 +1,106 @@ + +import React, { useState, useEffect } from 'react'; +import { mdiChatProcessing, mdiClose, mdiSend } from '@mdi/js'; +import BaseIcon from './BaseIcon'; +import axios from 'axios'; + +const ChatbotWidget = () => { + const [isOpen, setIsOpen] = useState(false); + const [messages, setMessages] = useState([ + { role: 'bot', text: 'Hello! How can I help you today?' }, + ]); + const [input, setInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const toggleChat = () => setIsOpen(!isOpen); + + const sendMessage = async () => { + if (!input.trim()) return; + + const userMessage = { role: 'user', text: input }; + setMessages((prev) => [...prev, userMessage]); + setInput(''); + setIsLoading(true); + + try { + const response = await axios.post('/public/chatbot', { message: input }); + const botMessage = { role: 'bot', text: response.data.text || "I'm sorry, I couldn't understand that." }; + setMessages((prev) => [...prev, botMessage]); + } catch (error) { + console.error('Chatbot error:', error); + setMessages((prev) => [...prev, { role: 'bot', text: 'Sorry, I am having trouble connecting right now.' }]); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ {!isOpen && ( + + )} + + {isOpen && ( +
+
+

Store Assistant

+ +
+ +
+ {messages.map((msg, idx) => ( +
+
+ {msg.text} +
+
+ ))} + {isLoading && ( +
+
+ Typing... +
+
+ )} +
+ +
+ setInput(e.target.value)} + onKeyPress={(e) => e.key === 'Enter' && sendMessage()} + placeholder="Type your question..." + className="flex-1 border rounded-full px-4 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" + /> + +
+
+ )} +
+ ); +}; + +export default ChatbotWidget; diff --git a/frontend/src/pages/dashboard.tsx b/frontend/src/pages/dashboard.tsx index 3ad97b4..5320194 100644 --- a/frontend/src/pages/dashboard.tsx +++ b/frontend/src/pages/dashboard.tsx @@ -1,6 +1,6 @@ import * as icon from '@mdi/js'; import Head from 'next/head' -import React from 'react' +import React, { useState, useEffect } from 'react' import axios from 'axios'; import type { ReactElement } from 'react' import LayoutAuthenticated from '../layouts/Authenticated' @@ -9,6 +9,8 @@ import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton import BaseIcon from "../components/BaseIcon"; import { getPageTitle } from '../config' import Link from "next/link"; +import BaseButton from '../components/BaseButton'; +import CardBox from '../components/CardBox'; import { hasPermission } from "../helpers/userPermissions"; import { fetchWidgets } from '../stores/roles/rolesSlice'; @@ -16,6 +18,7 @@ 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); @@ -24,34 +27,34 @@ const Dashboard = () => { const loadingMessage = 'Loading...'; - - const [users, setUsers] = React.useState(loadingMessage); - const [roles, setRoles] = React.useState(loadingMessage); - const [permissions, setPermissions] = React.useState(loadingMessage); - const [organizations, setOrganizations] = React.useState(loadingMessage); - const [stores, setStores] = React.useState(loadingMessage); - const [pages, setPages] = React.useState(loadingMessage); - const [collections, setCollections] = React.useState(loadingMessage); - const [products, setProducts] = React.useState(loadingMessage); - const [product_variants, setProduct_variants] = React.useState(loadingMessage); - const [customers, setCustomers] = React.useState(loadingMessage); - const [addresses, setAddresses] = React.useState(loadingMessage); - const [orders, setOrders] = React.useState(loadingMessage); - const [order_items, setOrder_items] = React.useState(loadingMessage); - const [fulfillments, setFulfillments] = React.useState(loadingMessage); - const [payments, setPayments] = React.useState(loadingMessage); - const [discount_codes, setDiscount_codes] = React.useState(loadingMessage); - const [refunds, setRefunds] = React.useState(loadingMessage); - const [inventory_adjustments, setInventory_adjustments] = React.useState(loadingMessage); - const [faq_articles, setFaq_articles] = React.useState(loadingMessage); - const [chatbot_configs, setChatbot_configs] = React.useState(loadingMessage); - const [chat_sessions, setChat_sessions] = React.useState(loadingMessage); - const [chat_messages, setChat_messages] = React.useState(loadingMessage); - const [support_tickets, setSupport_tickets] = React.useState(loadingMessage); - const [shopify_sync_jobs, setShopify_sync_jobs] = React.useState(loadingMessage); + const [users, setUsers] = useState(loadingMessage); + const [roles, setRoles] = useState(loadingMessage); + const [permissions, setPermissions] = useState(loadingMessage); + const [organizations, setOrganizations] = useState(loadingMessage); + const [stores, setStores] = useState(loadingMessage); + const [pages, setPages] = useState(loadingMessage); + const [collections, setCollections] = useState(loadingMessage); + const [products, setProducts] = useState(loadingMessage); + const [product_variants, setProduct_variants] = useState(loadingMessage); + const [customers, setCustomers] = useState(loadingMessage); + const [addresses, setAddresses] = useState(loadingMessage); + const [orders, setOrders] = useState(loadingMessage); + const [order_items, setOrder_items] = useState(loadingMessage); + const [fulfillments, setFulfillments] = useState(loadingMessage); + const [payments, setPayments] = useState(loadingMessage); + const [discount_codes, setDiscount_codes] = useState(loadingMessage); + const [refunds, setRefunds] = useState(loadingMessage); + const [inventory_adjustments, setInventory_adjustments] = useState(loadingMessage); + const [faq_articles, setFaq_articles] = useState(loadingMessage); + const [chatbot_configs, setChatbot_configs] = useState(loadingMessage); + const [chat_sessions, setChat_sessions] = useState(loadingMessage); + const [chat_messages, setChat_messages] = useState(loadingMessage); + const [support_tickets, setSupport_tickets] = useState(loadingMessage); + const [shopify_sync_jobs, setShopify_sync_jobs] = useState(loadingMessage); + const [topProducts, setTopProducts] = useState([]); - const [widgetsRole, setWidgetsRole] = React.useState({ + const [widgetsRole, setWidgetsRole] = useState({ role: { value: '', label: '' }, }); const { currentUser } = useAppSelector((state) => state.auth); @@ -59,22 +62,17 @@ const Dashboard = () => { const { rolesWidgets, loading } = useAppSelector((state) => state.roles); - - const organizationId = currentUser?.organizations?.id; - async function loadData() { const entities = ['users','roles','permissions','organizations','stores','pages','collections','products','product_variants','customers','addresses','orders','order_items','fulfillments','payments','discount_codes','refunds','inventory_adjustments','faq_articles','chatbot_configs','chat_sessions','chat_messages','support_tickets','shopify_sync_jobs',]; const fns = [setUsers,setRoles,setPermissions,setOrganizations,setStores,setPages,setCollections,setProducts,setProduct_variants,setCustomers,setAddresses,setOrders,setOrder_items,setFulfillments,setPayments,setDiscount_codes,setRefunds,setInventory_adjustments,setFaq_articles,setChatbot_configs,setChat_sessions,setChat_messages,setSupport_tickets,setShopify_sync_jobs,]; 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) => { @@ -86,18 +84,26 @@ const Dashboard = () => { } }); }); + + try { + const res = await axios.get('/public/top-products'); + setTopProducts(res.data); + } catch (err) { + console.error('Failed to fetch top products:', err); + } } async function getWidgets(roleId) { await dispatch(fetchWidgets(roleId)); } - React.useEffect(() => { + + useEffect(() => { if (!currentUser) return; loadData().then(); setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } }); }, [currentUser]); - React.useEffect(() => { + useEffect(() => { if (!currentUser || !widgetsRole?.role?.value) return; getWidgets(widgetsRole?.role?.value || '').then(); }, [widgetsRole?.role?.value]); @@ -117,15 +123,52 @@ const Dashboard = () => { {''} +
+ + +
+ {topProducts.map((product) => ( +
+
+ +
+
{product.product_title}
+
Sales: {product.total_sales_count || 0}
+
Rank #{product.current_sales_rank || 'N/A'}
+
+ ))} + {topProducts.length === 0 &&
No sales data available yet.
} +
+
+ + + +
+
+ AI Agent + + + Online + +
+
+ "Our AI bot is currently helping customers using information from your FAQ articles." +
+ +
+
+
+ {hasPermission(currentUser, 'CREATE_ROLES') && } + {!!rolesWidgets.length && hasPermission(currentUser, 'CREATE_ROLES') && ( -

+

{`${widgetsRole?.role?.label || 'Users'}'s widgets`}

)} @@ -158,682 +201,54 @@ const Dashboard = () => { {!!rolesWidgets.length &&
} -
- - - {hasPermission(currentUser, 'READ_USERS') && -
-
-
-
- Users -
-
- {users} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ROLES') && -
-
-
-
- Roles -
-
- {roles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PERMISSIONS') && -
-
-
-
- Permissions -
-
- {permissions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ORGANIZATIONS') && -
-
-
-
- Organizations -
-
- {organizations} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_STORES') && -
-
-
-
- Stores -
-
- {stores} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_PAGES') && -
-
-
-
- Pages -
-
- {pages} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_COLLECTIONS') && -
-
-
-
- Collections -
-
- {collections} -
-
-
- -
-
-
- } - +
{hasPermission(currentUser, 'READ_PRODUCTS') && -
+
-
- Products -
-
- {products} -
-
-
- +
Products
+
{products}
+
} - - {hasPermission(currentUser, 'READ_PRODUCT_VARIANTS') && -
-
-
-
- Product variants -
-
- {product_variants} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CUSTOMERS') && -
-
-
-
- Customers -
-
- {customers} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_ADDRESSES') && -
-
-
-
- Addresses -
-
- {addresses} -
-
-
- -
-
-
- } - + {hasPermission(currentUser, 'READ_ORDERS') && -
+
-
- Orders -
-
- {orders} -
-
-
- +
Orders
+
{orders}
+
} - - {hasPermission(currentUser, 'READ_ORDER_ITEMS') && -
+ + {hasPermission(currentUser, 'READ_CUSTOMERS') && +
-
- Order items -
-
- {order_items} -
-
-
- +
Customers
+
{customers}
+
} - - {hasPermission(currentUser, 'READ_FULFILLMENTS') && -
-
-
-
- Fulfillments -
-
- {fulfillments} -
-
-
- -
-
-
- } - + {hasPermission(currentUser, 'READ_PAYMENTS') && -
+
-
- Payments -
-
- {payments} -
-
-
- +
Payments
+
{payments}
+
} - - {hasPermission(currentUser, 'READ_DISCOUNT_CODES') && -
-
-
-
- Discount codes -
-
- {discount_codes} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_REFUNDS') && -
-
-
-
- Refunds -
-
- {refunds} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_INVENTORY_ADJUSTMENTS') && -
-
-
-
- Inventory adjustments -
-
- {inventory_adjustments} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_FAQ_ARTICLES') && -
-
-
-
- Faq articles -
-
- {faq_articles} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CHATBOT_CONFIGS') && -
-
-
-
- Chatbot configs -
-
- {chatbot_configs} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CHAT_SESSIONS') && -
-
-
-
- Chat sessions -
-
- {chat_sessions} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_CHAT_MESSAGES') && -
-
-
-
- Chat messages -
-
- {chat_messages} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_SUPPORT_TICKETS') && -
-
-
-
- Support tickets -
-
- {support_tickets} -
-
-
- -
-
-
- } - - {hasPermission(currentUser, 'READ_SHOPIFY_SYNC_JOBS') && -
-
-
-
- Shopify sync jobs -
-
- {shopify_sync_jobs} -
-
-
- -
-
-
- } - -
diff --git a/frontend/src/pages/index.tsx b/frontend/src/pages/index.tsx index 608ae44..6460665 100644 --- a/frontend/src/pages/index.tsx +++ b/frontend/src/pages/index.tsx @@ -1,4 +1,3 @@ - import React, { useEffect, useState } from 'react'; import type { ReactElement } from 'react'; import Head from 'next/head'; @@ -13,7 +12,12 @@ import { getPageTitle } from '../config'; import { useAppSelector } from '../stores/hooks'; import CardBoxComponentTitle from "../components/CardBoxComponentTitle"; import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'; - +import BaseIcon from '../components/BaseIcon'; +import axios from 'axios'; +import ChatbotWidget from '../components/ChatbotWidget'; +import SectionMain from '../components/SectionMain'; +import SectionTitleLineWithButton from '../components/SectionTitleLineWithButton'; +import { mdiShopping, mdiStar } from '@mdi/js'; export default function Starter() { const [illustrationImage, setIllustrationImage] = useState({ @@ -24,17 +28,25 @@ export default function Starter() { const [illustrationVideo, setIllustrationVideo] = useState({video_files: []}) const [contentType, setContentType] = useState('video'); const [contentPosition, setContentPosition] = useState('left'); + const [topProducts, setTopProducts] = useState([]); const textColor = useAppSelector((state) => state.style.linkColor); const title = 'Shopify Ops Dashboard' - // Fetch Pexels image/video + // Fetch Pexels image/video and top products useEffect(() => { async function fetchData() { const image = await getPexelsImage(); const video = await getPexelsVideo(); setIllustrationImage(image); setIllustrationVideo(video); + + try { + const res = await axios.get('/public/top-products'); + setTopProducts(res.data); + } catch (err) { + console.error('Failed to fetch top products:', err); + } } fetchData(); }, []); @@ -94,24 +106,9 @@ export default function Starter() { }; return ( -
+
- {getPageTitle('Starter Page')} + {getPageTitle('Store Home')} @@ -126,41 +123,89 @@ export default function Starter() { {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

+
+
+

+ Modern Operations for your Shopify Store +

+

+ The all-in-one dashboard to manage your products, orders, and customer support with AI. +

- - - - - + + + +
+

Access your products, customers, and order lifecycle in one integrated workspace.

+
+ + + + +
-
-

© 2026 {title}. All rights reserved

- + + + + {''} + + +
+ {topProducts.length > 0 ? topProducts.map((product) => ( +
+
+ {product.images?.[0] ? ( + {product.product_title} + ) : ( +
+ +
+ )} +
+
+

{product.product_title}

+

{product.description}

+
+ + Bestseller + + #{product.current_sales_rank || 'N/A'} +
+
+
+ )) : ( +
+ Loading featured products... +
+ )} +
+
+ +
+

© 2026 {title}. All rights reserved

+ Privacy Policy
+
); } Starter.getLayout = function getLayout(page: ReactElement) { return {page}; -}; - +}; \ No newline at end of file