<!DOCTYPE html>
Build POS System
Weekly Performance Report
7 Days
14 Days
30 Days
<p id="reportSubject">Loading...</p>
<div class="kpi-grid">
<div class="kpi-card">
<div class="kpi-label">Total Sales</div>
<div class="kpi-value" id="totalSales">$0.00</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Transactions</div>
<div class="kpi-value" id="transactions">0</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Avg Ticket</div>
<div class="kpi-value" id="avgTicket">$0.00</div>
</div>
<div class="kpi-card">
<div class="kpi-label">Total Variance</div>
<div class="kpi-value" id="totalVariance">$0.00</div>
</div>
</div>
<div class="chart-section">
<h3>Sales Trend</h3>
<div class="chart-wrap">
<canvas id="salesChart"></canvas>
</div>
</div>
<div class="chart-section">
<h3>Cashier Summary</h3>
<div class="table-wrap">
<table class="table">
<thead>
<tr>
<th>Cashier</th>
<th>Sales</th>
<th>Txns</th>
<th>Over/Short</th>
<th>Flags</th>
</tr>
</thead>
<tbody id="cashierTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<aside class="agent-panel">
<div class="card agent-card">
<div class="agent-header">
<h2>Hermes Agent AI</h2>
<span class="agent-status">Online</span>
</div>
<div class="agent-summary">
<p>Ask Hermes to inspect stats, flag issues, or draft actions.</p>
</div>
<div class="agent-actions">
<button class="action-btn" onclick="runAgentAction('Summarize weekly performance')">Summarize</button>
<button class="action-btn" onclick="runAgentAction('Find risk flags')">Find Risks</button>
<button class="action-btn" onclick="runAgentAction('Draft digest email')">Draft Digest</button>
</div>
<div class="chat-box" id="chatBox"></div>
<div class="chat-input-row">
<input id="agentInput" type="text" placeholder="Ask Hermes..." />
<button id="sendAgentBtn" onclick="sendAgentMessage()">Send</button>
</div>
<div class="agent-note">Ready for backend integration with <code>/api/agent/*</code>
</div>
</div>
</aside>
styles.css
Applied
body {
margin: 0;
padding: 24px;
font-family: system-ui, sans-serif;
background: #f9fafb;
color: #1e293b;
}
.title {
color: #5c6ac4;
font-size: 2rem;
font-weight: 700;
margin-bottom: 12px;
text-align: center;
}
currentTime {
margin-bottom: 20px;
color: #334155;
font-size: 1rem;
text-align: center;
}
.page-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) 360px;
gap: 20px;
max-width: 1500px;
margin: 0 auto;
}
.container {
min-width: 0;
}
.card {
background: #fff;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
text-align: left;
}
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.toolbar select,
.chat-input-row input {
padding: 10px 12px;
border: 1px solid #cbd5e1;
border-radius: 8px;
background: white;
outline: none;
}
.kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin: 24px 0;
}
.kpi-card {
background: #f8fafc;
padding: 18px;
border-radius: 10px;
border-left: 4px solid #3b82f6;
}
.kpi-label {
font-size: 12px;
color: #64748b;
text-transform: uppercase;
font-weight: 600;
margin-bottom: 8px;
}
.kpi-value {
font-size: 28px;
font-weight: 700;
color: #0f172a;
}
.negative {
color: #dc2626;
}
.chart-section {
margin: 32px 0;
}
.chart-section h3 {
margin-bottom: 14px;
color: #1e293b;
}
.chart-wrap {
position: relative;
width: 100%;
height: 320px;
}
.table-wrap {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
font-size: 14px;
min-width: 640px;
}
.table th {
background: #f1f5f9;
padding: 12px;
text-align: left;
font-weight: 600;
border-bottom: 2px solid #e2e8f0;
}
.table td {
padding: 12px;
border-bottom: 1px solid #e2e8f0;
}
.badge {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.badge-danger {
background: #fee2e2;
color: #991b1b;
}
.badge-warning {
background: #fef3c7;
color: #92400e;
}
.agent-panel {
min-width: 0;
}
.agent-card {
position: sticky;
top: 16px;
}
.agent-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.agent-status {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.agent-summary {
background: #eff6ff;
border: 1px solid #dbeafe;
padding: 12px;
border-radius: 10px;
margin-bottom: 14px;
}
.agent-actions {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
margin-bottom: 14px;
}
.action-btn,
sendAgentBtn {
border: none;
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
font-weight: 600;
background: #5c6ac4;
color: white;
}
.action-btn:hover,
sendAgentBtn:hover {
opacity: 0.92;
}
.chat-box {
height: 320px;
overflow-y: auto;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 12px;
background: #f8fafc;
display: flex;
flex-direction: column;
gap: 10px;
}
.msg {
max-width: 90%;
padding: 10px 12px;
border-radius: 12px;
line-height: 1.4;
font-size: 14px;
}
.msg.user {
align-self: flex-end;
background: #dbeafe;
color: #1e3a8a;
}
.msg.bot {
align-self: flex-start;
background: white;
border: 1px solid #e2e8f0;
}
.chat-input-row {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px;
margin-top: 12px;
}
.agent-note {
margin-top: 12px;
font-size: 12px;
color: #64748b;
}
@media (max-width: 1100px) {
.page-grid {
grid-template-columns: 1fr;
}
.agent-card {
position: static;
}
}
@media (max-width: 900px) {
.kpi-grid {
grid-template-columns: repeat(2, 1fr);
}
.agent-actions {
grid-template-columns: 1fr;
}
}
@media (max-width: 600px) {
body {
padding: 14px;
}
.kpi-grid {
grid-template-columns: 1fr;
}
.chart-wrap {
height: 260px;
}
.chat-box {
height: 260px;
}
}
script.js
Applied
var salesChart;
function showTime() {
var timeEl = document.getElementById('currentTime');
if (timeEl) timeEl.textContent = new Date().toUTCString();
}
function formatCurrency(value) {
return '$' + Number(value || 0).toFixed(2);
}
function getFallbackData(days) {
var dailySales = [];
var labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
for (var i = 0; i < days; i++) {
dailySales.push({
date: labels[i % labels.length],
sales: Math.round((Math.random() * 1000 + 200) * 100) / 100,
transactions: Math.floor(Math.random() * 50) + 10
});
}
var totalSales = dailySales.reduce(function (sum, d) { return sum + d.sales; }, 0);
var totalTransactions = dailySales.reduce(function (sum, d) { return sum + d.transactions; }, 0);
return {
subject: 'Weekly Performance Summary',
summary: {
totalSales: totalSales,
totalTransactions: totalTransactions,
avgTicket: totalTransactions ? totalSales / totalTransactions : 0,
totalOverShort: 18.4
},
dailySales: dailySales,
cashierPerformance: [
{ id: 1, name: 'Ava', sales: 2450.2, transactions: 48, overShort: 2.1, escalationCount: 0 },
{ id: 2, name: 'Ben', sales: 1980.4, transactions: 39, overShort: 11.6, escalationCount: 1 },
{ id: 3, name: 'Chloe', sales: 2675, transactions: 53, overShort: 0, escalationCount: 0 },
{ id: 4, name: 'Daniel', sales: 1316.15, transactions: 24, overShort: 4.7, escalationCount: 2 }
]
};
}
function renderChart(dailySales) {
var canvas = document.getElementById('salesChart');
if (!canvas || typeof Chart === 'undefined') return;
var ctx = canvas.getContext('2d');
if (salesChart) salesChart.destroy();
salesChart = new Chart(ctx, {
type: 'line',
data: {
labels: dailySales.map(function (d) { return d.date; }),
datasets: [
{
label: 'Sales $',
data: dailySales.map(function (d) { return d.sales; }),
borderColor: '#3b82f6',
backgroundColor: 'rgba(59,130,246,0.12)',
tension: 0.35,
fill: true
},
{
label: 'Transactions',
data: dailySales.map(function (d) { return d.transactions; }),
borderColor: '#10b981',
backgroundColor: 'rgba(16,185,129,0.12)',
tension: 0.35,
fill: false,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
scales: {
y: {
beginAtZero: true,
ticks: {
callback: function (value) {
return '$' + value;
}
}
},
y1: {
beginAtZero: true,
position: 'right',
grid: { drawOnChartArea: false }
}
}
}
});
}
function renderReport(data) {
var reportSubjectEl = document.getElementById('reportSubject');
var totalSalesEl = document.getElementById('totalSales');
var transactionsEl = document.getElementById('transactions');
var avgTicketEl = document.getElementById('avgTicket');
var totalVarianceEl = document.getElementById('totalVariance');
var tbody = document.getElementById('cashierTableBody');
if (reportSubjectEl) reportSubjectEl.textContent = data.subject;
if (totalSalesEl) totalSalesEl.textContent = formatCurrency(data.summary.totalSales);
if (transactionsEl) transactionsEl.textContent = data.summary.totalTransactions;
if (avgTicketEl) avgTicketEl.textContent = formatCurrency(data.summary.avgTicket);
if (totalVarianceEl) {
totalVarianceEl.textContent = formatCurrency(data.summary.totalOverShort);
totalVarianceEl.classList.toggle('negative', Number(data.summary.totalOverShort) > 50);
}
renderChart(data.dailySales || []);
if (tbody) {
tbody.innerHTML = '';
(data.cashierPerformance || []).forEach(function (c) {
var tr = document.createElement('tr');
var flagHtml = '';
if (c.escalationCount >= 3) {
flagHtml = '<span class="badge badge-danger">Escalated</span>';
} else if (c.escalationCount > 0) {
flagHtml = '<span class="badge badge-warning">Watch</span>';
}
tr.innerHTML =
'<td><strong>' + c.name + '</strong></td>' +
'<td>' + formatCurrency(c.sales) + '</td>' +
'<td>' + c.transactions + '</td>' +
'<td style="color:' + (c.overShort > 10 ? '#dc2626' : '#16a34a') + '">' + formatCurrency(c.overShort) + '</td>' +
'<td>' + flagHtml + '</td>';
tbody.appendChild(tr);
});
}
}
function addMessage(text, type) {
var chatBox = document.getElementById('chatBox');
if (!chatBox) return;
var msg = document.createElement('div');
msg.className = 'msg ' + type;
msg.textContent = text;
chatBox.appendChild(msg);
chatBox.scrollTop = chatBox.scrollHeight;
}
function sendAgentMessage() {
var input = document.getElementById('agentInput');
if (!input || !input.value.trim()) return;
var text = input.value.trim();
addMessage(text, 'user');
input.value = '';
setTimeout(function () {
var lower = text.toLowerCase();
var reply = 'Hermes is analyzing the dashboard.';
if (lower.includes('summar')) {
reply = 'Sales are steady, transactions are healthy, and variance is within normal limits.';
} else if (lower.includes('risk') || lower.includes('flag')) {
reply = 'Ben and Daniel have watch flags. Review over/short activity and escalation counts.';
} else if (lower.includes('digest')) {
reply = 'Digest draft ready: summary, key metrics, cashier flags, and top actions can be sent.';
} else if (lower.includes('resolve')) {
reply = 'I can prepare a resolution flow, but backend agent routes are needed for actual issue changes.';
}
addMessage(reply, 'bot');
}, 500);
}
function runAgentAction(prompt) {
var input = document.getElementById('agentInput');
if (input) input.value = prompt;
sendAgentMessage();
}
showTime();
renderReport(getFallbackData(7));
setInterval(showTime, 1000);
var daysRangeEl = document.getElementById('daysRange');
if (daysRangeEl) {
daysRangeEl.addEventListener('change', function () {
renderReport(getFallbackData(Number(daysRangeEl.value || 7)));
addMessage('Updated report range to ' + daysRangeEl.value + ' days.', 'bot');
});
}
var agentInput = document.getElementById('agentInput');
if (agentInput) {
agentInput.addEventListener('keydown', function (e) {
if (e.key === 'Enter') sendAgentMessage();
});
}
addMessage('Hello, I am Hermes. Ask me to summarize sales, flag risks, or draft a digest.', 'bot');
.envfiles
Applied
JWT_SECRET=secret-key
STORE_NAME=My POS Store
ALERT_EMAILS=admin@example.com
SENDGRID_FROM_EMAIL=alerts@yourstore.com
SENDGRID_API_KEY=SG.xxx
ALERT_WEBHOOK_URL=https://hooks.slack.com/services/T000/B000/XXXXX
OPS_EMAIL=ops@yourstore.com
NEXT_PUBLIC_APP_URL=http://localhost:3000
HERMES_AGENT_KEY=hk_live_xxxxx
NEXT_PUBLIC_HERMES_AGENT_KEY=hk_live_xxxxx
DATABASE_URL=postgresql://user:pass@localhost:5432/pos
.gitignore
Applied
node_modules/
.env
.env.local
.env.*.local
dist/
.next/
out/
*.log
.DS_Store
Thumbs.db
.vscode/
.idea/
package.json
Applied
{
"name": ##"pos-system"##,
"version": "1.2.7",
"private": true,
"description": ##"POS System with analytics##, digests, and monitoring",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"build": "node server.js",
"lint": "eslint ."
},
"dependencies": {
"cors": "^2.8.5",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.2"
},
"devDependencies": {
"eslint": "^8.57.0",
"nodemon": "^3.1.0"
}
}
. Install dependencies:
npm install
CMD.cd
Applied
npm install
npm run dev
- Start the server:
npm run dev
server.js
Applied
import express from 'express';
import cors from 'cors';
import jwt from 'jsonwebtoken';
const app = express();
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET || 'secret-key';
const ADMIN_TOKEN = process.env.ADMIN_TOKEN || 'admin-token';
function authMiddleware(req, res, next) {
const auth = req.headers.authorization || '';
const token = auth.startsWith('Bearer ') ? auth.slice(7) : null;
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const payload = jwt.verify(token, JWT_SECRET);
req.user = payload;
next();
} catch (err) {
return res.status(401).json({ error: 'Invalid token' });
}
}
function adminOnly(req, res, next) {
if (req.user?.role === 'ADMIN') return next();
return res.status(403).json({ error: 'Forbidden: Admin only' });
}
app.post('/api/auth/login', (req, res) => {
const { username, password } = req.body || {};
if (username === 'admin' && password === 'admin123') {
const token = jwt.sign(
{ id: '1', name: 'Admin User', role: 'ADMIN', storeId: '12' },
JWT_SECRET,
{ expiresIn: '8h' }
);
return res.json({
success: true,
token,
user: { id: '1', name: 'Admin User', role: 'ADMIN', storeId: '12' }
});
}
res.status(401).json({ error: 'Invalid credentials' });
});
app.get('/api/admin/weekly-stats', authMiddleware, adminOnly, (req, res) => {
const days = parseInt(req.query.days) || 7;
const now = new Date();
const dailySales = [];
for (let i = days - 1; i >= 0; i--) {
const date = new Date(now);
date.setDate(now.getDate() - i);
dailySales.push({
date: date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }),
sales: parseFloat((Math.random() * 1000 + 200).toFixed(2)),
transactions: Math.floor(Math.random() * 50) + 10
});
}
const totalSales = dailySales.reduce((sum, d) => sum + d.sales, 0);
const totalTransactions = dailySales.reduce((sum, d) => sum + d.transactions, 0);
const hourlyDistribution = Array.from({ length: 24 }, (_, h) => ({
hour: ${h}:00,
sales: parseFloat(((Math.sin((h / 24) * 2 * Math.PI) + 1) * 50 + 100).toFixed(2))
}));
res.json({
summary: {
totalSales,
totalTransactions,
avgTicket: totalTransactions ? totalSales / totalTransactions : 0,
totalOverShort: 18.4
},
dailySales,
hourlyDistribution,
cashierPerformance: [
{ id: 1, name: 'Ava', sales: 2450.2, transactions: 48, overShort: 2.1, escalationCount: 0 },
{ id: 2, name: 'Ben', sales: 1980.4, transactions: 39, overShort: 11.6, escalationCount: 1 },
{ id: 3, name: 'Chloe', sales: 2675, transactions: 53, overShort: 0, escalationCount: 0 },
{ id: 4, name: 'Daniel', sales: 1316.15, transactions: 24, overShort: 4.7, escalationCount: 2 }
],
topProducts: [
{ name: 'Latte', revenue: 1220.5 },
{ name: 'Sandwich', revenue: 980.0 },
{ name: 'Muffin', revenue: 640.75 },
{ name: 'Espresso', revenue: 510.25 }
]
});
});
app.post('/api/admin/send-digest', authMiddleware, adminOnly, (req, res) => {
res.json({ success: true, message: 'Digest email triggered (mock)' });
});
app.get('/', (req, res) => {
res.send('POS System Backend Running');
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log(Server running on port ${PORT}));
README.cd
README.md
Applied
POS System
Setup
- Copy
.envand fill in your values. - Install dependencies:
npm install
- Start the server:
npm run dev
Features
- JWT login
- CORS-enabled backend
- Weekly stats API
- Hermes agent-ready frontend
- Responsive dashboard UI
Default Login
- Username:
admin - Password:
admin123
API
POST /api/auth/loginGET /api/admin/weekly-stats?days=7POST /api/admin/send-digest
Notes
This project is structured for OneCompiler-style editing with separate HTML, CSS, JS, and Node backend files.
Install
Applied
npm install
npm i express cors jsonwebtoken
npm i -D nodemon eslint
Top comments (0)