touchbistro: Add 15 React MCP apps

This commit is contained in:
Jake Shore 2026-02-13 00:02:29 -05:00
parent 14f651082c
commit 6741068aef
16 changed files with 2047 additions and 0 deletions

View File

@ -0,0 +1,177 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface InventoryItem {
id: string;
name: string;
category: string;
currentStock: number;
parLevel: number;
unit: string;
costPerUnit: number;
supplier: string;
lastRestocked: string;
}
export default function InventoryTracker() {
const [items, setItems] = useState<InventoryItem[]>([]);
const [filter, setFilter] = useState<'all' | 'low' | 'critical'>('all');
const [categoryFilter, setCategoryFilter] = useState('all');
useEffect(() => {
// Mock data
setItems([
{ id: '1', name: 'Ground Beef', category: 'Meats', currentStock: 45, parLevel: 100, unit: 'lbs', costPerUnit: 5.99, supplier: 'Premium Meats Co', lastRestocked: '2024-02-14' },
{ id: '2', name: 'Chicken Breast', category: 'Meats', currentStock: 12, parLevel: 80, unit: 'lbs', costPerUnit: 4.50, supplier: 'Premium Meats Co', lastRestocked: '2024-02-13' },
{ id: '3', name: 'Romaine Lettuce', category: 'Produce', currentStock: 8, parLevel: 30, unit: 'heads', costPerUnit: 1.25, supplier: 'Fresh Farms', lastRestocked: '2024-02-15' },
{ id: '4', name: 'Tomatoes', category: 'Produce', currentStock: 18, parLevel: 40, unit: 'lbs', costPerUnit: 2.75, supplier: 'Fresh Farms', lastRestocked: '2024-02-15' },
{ id: '5', name: 'Cheddar Cheese', category: 'Dairy', currentStock: 25, parLevel: 50, unit: 'lbs', costPerUnit: 6.25, supplier: 'Dairy Direct', lastRestocked: '2024-02-14' },
{ id: '6', name: 'Milk', category: 'Dairy', currentStock: 5, parLevel: 20, unit: 'gallons', costPerUnit: 3.99, supplier: 'Dairy Direct', lastRestocked: '2024-02-12' },
{ id: '7', name: 'Flour', category: 'Dry Goods', currentStock: 85, parLevel: 100, unit: 'lbs', costPerUnit: 0.89, supplier: 'Bulk Foods Inc', lastRestocked: '2024-02-10' },
{ id: '8', name: 'Olive Oil', category: 'Dry Goods', currentStock: 6, parLevel: 15, unit: 'bottles', costPerUnit: 12.99, supplier: 'Bulk Foods Inc', lastRestocked: '2024-02-11' },
{ id: '9', name: 'Bacon', category: 'Meats', currentStock: 22, parLevel: 40, unit: 'lbs', costPerUnit: 7.50, supplier: 'Premium Meats Co', lastRestocked: '2024-02-14' },
{ id: '10', name: 'Onions', category: 'Produce', currentStock: 15, parLevel: 35, unit: 'lbs', costPerUnit: 1.50, supplier: 'Fresh Farms', lastRestocked: '2024-02-15' },
]);
}, []);
const getStockStatus = (item: InventoryItem) => {
const percentage = (item.currentStock / item.parLevel) * 100;
if (percentage < 25) return 'critical';
if (percentage < 50) return 'low';
return 'good';
};
const filteredItems = items.filter((item) => {
const status = getStockStatus(item);
const matchesFilter = filter === 'all' || status === filter;
const matchesCategory = categoryFilter === 'all' || item.category === categoryFilter;
return matchesFilter && matchesCategory;
});
const categories = Array.from(new Set(items.map(i => i.category)));
const lowStockCount = items.filter(i => getStockStatus(i) === 'low').length;
const criticalCount = items.filter(i => getStockStatus(i) === 'critical').length;
const totalValue = items.reduce((sum, i) => sum + (i.currentStock * i.costPerUnit), 0);
return (
<div className="app">
<header className="app-header">
<h1>📦 Inventory Tracker</h1>
<p>Stock levels and alerts</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Items</div>
<div className="stat-value">{items.length}</div>
</div>
<div className="stat-card critical-card">
<div className="stat-label">Critical Stock</div>
<div className="stat-value critical">{criticalCount}</div>
</div>
<div className="stat-card low-card">
<div className="stat-label">Low Stock</div>
<div className="stat-value low">{lowStockCount}</div>
</div>
<div className="stat-card">
<div className="stat-label">Inventory Value</div>
<div className="stat-value value">${totalValue.toFixed(2)}</div>
</div>
</div>
<div className="filters">
<div className="filter-group">
{['all', 'low', 'critical'].map((status) => (
<button
key={status}
className={`filter-btn ${filter === status ? 'active' : ''}`}
onClick={() => setFilter(status as any)}
>
{status.charAt(0).toUpperCase() + status.slice(1)} Stock
</button>
))}
</div>
<select
className="category-select"
value={categoryFilter}
onChange={(e) => setCategoryFilter(e.target.value)}
>
<option value="all">All Categories</option>
{categories.map((cat) => (
<option key={cat} value={cat}>{cat}</option>
))}
</select>
<button className="btn-primary">+ Add Item</button>
</div>
<div className="inventory-grid">
{filteredItems.map((item) => {
const status = getStockStatus(item);
const percentage = Math.min((item.currentStock / item.parLevel) * 100, 100);
return (
<div key={item.id} className={`inventory-card ${status}`}>
<div className="item-header">
<div>
<h3>{item.name}</h3>
<span className="category-badge">{item.category}</span>
</div>
<span className={`status-badge ${status}`}>
{status === 'critical' && '🚨'}
{status === 'low' && '⚠️'}
{status === 'good' && '✓'}
</span>
</div>
<div className="stock-bar">
<div
className={`stock-fill ${status}`}
style={{ width: `${percentage}%` }}
></div>
</div>
<div className="stock-info">
<span className="current-stock">
{item.currentStock} {item.unit}
</span>
<span className="par-level">
Par: {item.parLevel} {item.unit}
</span>
</div>
<div className="item-details">
<div className="detail-row">
<span>Cost/Unit:</span>
<span className="value">${item.costPerUnit.toFixed(2)}</span>
</div>
<div className="detail-row">
<span>Total Value:</span>
<span className="value">${(item.currentStock * item.costPerUnit).toFixed(2)}</span>
</div>
<div className="detail-row">
<span>Supplier:</span>
<span className="value">{item.supplier}</span>
</div>
<div className="detail-row">
<span>Last Restocked:</span>
<span className="value">
{new Date(item.lastRestocked).toLocaleDateString()}
</span>
</div>
</div>
<div className="item-actions">
<button className="action-btn">Restock</button>
<button className="action-btn">Adjust</button>
</div>
</div>
);
})}
</div>
{filteredItems.length === 0 && (
<div className="no-results">No items found</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Inventory Tracker - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,301 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
}
.stat-card.critical-card {
border-color: #ef4444;
}
.stat-card.low-card {
border-color: #fbbf24;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.critical {
color: #ef4444;
}
.stat-value.low {
color: #fbbf24;
}
.stat-value.value {
color: #10b981;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
align-items: center;
}
.filter-group {
display: flex;
gap: 1rem;
}
.filter-btn {
padding: 0.75rem 1.5rem;
background: #1e293b;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
}
.filter-btn:hover {
background: #334155;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.category-select {
padding: 0.75rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
}
.category-select:focus {
outline: none;
border-color: #3b82f6;
}
.btn-primary {
padding: 0.875rem 1.5rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
margin-left: auto;
}
.btn-primary:hover {
background: #2563eb;
}
.inventory-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.inventory-card {
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s;
}
.inventory-card.critical {
border-color: #ef4444;
}
.inventory-card.low {
border-color: #fbbf24;
}
.inventory-card.good {
border-color: #10b981;
}
.inventory-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.item-header h3 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.category-badge {
display: inline-block;
padding: 0.25rem 0.625rem;
background: #334155;
color: #cbd5e1;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
}
.status-badge {
font-size: 1.5rem;
}
.stock-bar {
height: 8px;
background: #334155;
border-radius: 1rem;
overflow: hidden;
margin-bottom: 1rem;
}
.stock-fill {
height: 100%;
transition: width 0.3s;
}
.stock-fill.good {
background: #10b981;
}
.stock-fill.low {
background: #fbbf24;
}
.stock-fill.critical {
background: #ef4444;
}
.stock-info {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #334155;
}
.current-stock {
color: #f8fafc;
font-weight: 700;
font-size: 1.125rem;
}
.par-level {
color: #94a3b8;
font-size: 0.875rem;
}
.item-details {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.detail-row {
display: flex;
justify-content: space-between;
color: #94a3b8;
font-size: 0.875rem;
}
.detail-row .value {
color: #cbd5e1;
font-weight: 600;
}
.item-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
flex: 1;
padding: 0.75rem;
background: #334155;
border: none;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s;
}
.action-btn:hover {
background: #475569;
}
.no-results {
text-align: center;
padding: 3rem;
color: #64748b;
font-size: 1.125rem;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
}

View File

@ -0,0 +1,225 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface OrderItem {
id: string;
name: string;
quantity: number;
modifiers: string[];
instructions?: string;
status: 'new' | 'preparing' | 'ready';
}
interface KitchenOrder {
id: string;
orderNumber: string;
type: string;
table?: string;
server: string;
items: OrderItem[];
createdAt: string;
elapsedMinutes: number;
priority: 'normal' | 'high' | 'urgent';
}
export default function KitchenDisplay() {
const [orders, setOrders] = useState<KitchenOrder[]>([]);
const [filter, setFilter] = useState<'all' | 'new' | 'preparing'>('all');
useEffect(() => {
// Mock data
setOrders([
{
id: '1',
orderNumber: 'ORD-142',
type: 'Dine In',
table: 'Table 12',
server: 'Mike',
elapsedMinutes: 2,
priority: 'normal',
createdAt: new Date().toISOString(),
items: [
{ id: '1', name: 'Classic Burger', quantity: 2, modifiers: ['No Onions', 'Extra Cheese'], instructions: 'Medium rare', status: 'preparing' },
{ id: '2', name: 'Caesar Salad', quantity: 1, modifiers: ['Add Chicken'], status: 'preparing' },
],
},
{
id: '2',
orderNumber: 'ORD-143',
type: 'Takeout',
server: 'Lisa',
elapsedMinutes: 5,
priority: 'normal',
createdAt: new Date().toISOString(),
items: [
{ id: '3', name: 'Grilled Salmon', quantity: 1, modifiers: [], status: 'preparing' },
{ id: '4', name: 'French Fries', quantity: 2, modifiers: [], status: 'ready' },
],
},
{
id: '3',
orderNumber: 'ORD-144',
type: 'Delivery',
server: 'Tom',
elapsedMinutes: 12,
priority: 'high',
createdAt: new Date().toISOString(),
items: [
{ id: '5', name: 'Margherita Pizza', quantity: 2, modifiers: ['Extra Basil'], status: 'preparing' },
{ id: '6', name: 'Chicken Wings', quantity: 1, modifiers: ['BBQ Sauce'], status: 'preparing' },
{ id: '7', name: 'Garlic Bread', quantity: 1, modifiers: [], status: 'ready' },
],
},
{
id: '4',
orderNumber: 'ORD-145',
type: 'Dine In',
table: 'Table 5',
server: 'Mike',
elapsedMinutes: 18,
priority: 'urgent',
createdAt: new Date().toISOString(),
items: [
{ id: '8', name: 'Ribeye Steak', quantity: 1, modifiers: ['Medium Well'], instructions: 'Rush order', status: 'new' },
{ id: '9', name: 'Mashed Potatoes', quantity: 1, modifiers: ['Extra Butter'], status: 'new' },
],
},
{
id: '5',
orderNumber: 'ORD-146',
type: 'Dine In',
table: 'Table 8',
server: 'Lisa',
elapsedMinutes: 1,
priority: 'normal',
createdAt: new Date().toISOString(),
items: [
{ id: '10', name: 'Fish & Chips', quantity: 2, modifiers: [], status: 'new' },
{ id: '11', name: 'Coleslaw', quantity: 2, modifiers: [], status: 'new' },
],
},
]);
}, []);
const filteredOrders = orders.filter((order) => {
if (filter === 'all') return true;
return order.items.some(item => item.status === filter);
});
const getPriorityColor = (priority: string) => {
const colors = {
normal: '#3b82f6',
high: '#fbbf24',
urgent: '#ef4444',
};
return colors[priority as keyof typeof colors];
};
const stats = {
total: orders.length,
new: orders.filter(o => o.items.some(i => i.status === 'new')).length,
preparing: orders.filter(o => o.items.some(i => i.status === 'preparing')).length,
avgTime: Math.round(orders.reduce((sum, o) => sum + o.elapsedMinutes, 0) / orders.length),
};
return (
<div className="app kds">
<header className="kds-header">
<h1>🍳 Kitchen Display</h1>
<div className="header-stats">
<div className="header-stat">
<span className="stat-value">{stats.total}</span>
<span className="stat-label">Active Orders</span>
</div>
<div className="header-stat">
<span className="stat-value new">{stats.new}</span>
<span className="stat-label">New</span>
</div>
<div className="header-stat">
<span className="stat-value preparing">{stats.preparing}</span>
<span className="stat-label">Preparing</span>
</div>
<div className="header-stat">
<span className="stat-value time">{stats.avgTime}m</span>
<span className="stat-label">Avg Time</span>
</div>
</div>
</header>
<div className="filters">
{['all', 'new', 'preparing'].map((status) => (
<button
key={status}
className={`filter-btn ${filter === status ? 'active' : ''}`}
onClick={() => setFilter(status as any)}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
))}
</div>
<div className="orders-grid">
{filteredOrders.map((order) => (
<div
key={order.id}
className={`order-ticket ${order.priority}`}
style={{ borderTopColor: getPriorityColor(order.priority) }}
>
<div className="ticket-header">
<div className="ticket-info">
<span className="order-number">{order.orderNumber}</span>
<span className="order-type">{order.type}</span>
{order.table && <span className="order-table">🪑 {order.table}</span>}
</div>
<div className="ticket-time">
<span className={`elapsed ${order.priority}`}>
{order.elapsedMinutes}m
</span>
<span className="server">Server: {order.server}</span>
</div>
</div>
<div className="ticket-items">
{order.items.map((item) => (
<div key={item.id} className={`ticket-item ${item.status}`}>
<div className="item-header">
<span className="item-quantity">{item.quantity}×</span>
<span className="item-name">{item.name}</span>
<span className={`item-status ${item.status}`}>
{item.status === 'new' && '🔴'}
{item.status === 'preparing' && '🟡'}
{item.status === 'ready' && '✓'}
</span>
</div>
{item.modifiers.length > 0 && (
<div className="item-modifiers">
{item.modifiers.map((mod, idx) => (
<span key={idx} className="modifier"> {mod}</span>
))}
</div>
)}
{item.instructions && (
<div className="item-instructions">📝 {item.instructions}</div>
)}
</div>
))}
</div>
<div className="ticket-actions">
<button className="btn-action start">Start</button>
<button className="btn-action ready">Ready</button>
<button className="btn-action complete">Complete</button>
</div>
</div>
))}
</div>
{filteredOrders.length === 0 && (
<div className="no-orders">
<div className="no-orders-icon">🎉</div>
<div className="no-orders-text">All caught up!</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kitchen Display - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,355 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app.kds {
max-width: 100%;
margin: 0;
padding: 1.5rem;
}
.kds-header {
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.kds-header h1 {
font-size: 2rem;
color: #f8fafc;
}
.header-stats {
display: flex;
gap: 2rem;
}
.header-stat {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
}
.header-stat .stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.header-stat .stat-value.new {
color: #ef4444;
}
.header-stat .stat-value.preparing {
color: #fbbf24;
}
.header-stat .stat-value.time {
color: #3b82f6;
}
.header-stat .stat-label {
color: #94a3b8;
font-size: 0.75rem;
text-transform: uppercase;
font-weight: 600;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
}
.filter-btn {
padding: 0.875rem 1.75rem;
background: #1e293b;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1.125rem;
font-weight: 600;
transition: all 0.2s;
text-transform: capitalize;
}
.filter-btn:hover {
background: #334155;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.orders-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
gap: 1.5rem;
}
.order-ticket {
background: #1e293b;
border: 2px solid #334155;
border-top: 6px solid;
border-radius: 0.75rem;
padding: 1.5rem;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.order-ticket.urgent {
border-top-color: #ef4444;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.4);
}
50% {
box-shadow: 0 0 0 8px rgba(239, 68, 68, 0);
}
}
.order-ticket.high {
border-top-color: #fbbf24;
}
.order-ticket.normal {
border-top-color: #3b82f6;
}
.ticket-header {
display: flex;
justify-content: space-between;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 2px solid #334155;
}
.ticket-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.order-number {
color: #f8fafc;
font-weight: 700;
font-size: 1.5rem;
font-family: 'Courier New', monospace;
}
.order-type {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
}
.order-table {
color: #3b82f6;
font-size: 0.875rem;
font-weight: 600;
}
.ticket-time {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.5rem;
}
.elapsed {
font-size: 2rem;
font-weight: 700;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
background: #334155;
}
.elapsed.normal {
color: #10b981;
}
.elapsed.high {
color: #fbbf24;
background: #78350f;
}
.elapsed.urgent {
color: #fff;
background: #ef4444;
}
.server {
color: #64748b;
font-size: 0.75rem;
}
.ticket-items {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.25rem;
}
.ticket-item {
background: #0f172a;
padding: 1rem;
border-radius: 0.5rem;
border-left: 4px solid #334155;
}
.ticket-item.new {
border-left-color: #ef4444;
}
.ticket-item.preparing {
border-left-color: #fbbf24;
}
.ticket-item.ready {
border-left-color: #10b981;
opacity: 0.6;
}
.item-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.item-quantity {
color: #f8fafc;
font-weight: 700;
font-size: 1.25rem;
min-width: 2rem;
}
.item-name {
color: #f8fafc;
font-weight: 600;
font-size: 1.125rem;
flex: 1;
}
.item-status {
font-size: 1.25rem;
}
.item-modifiers {
display: flex;
flex-direction: column;
gap: 0.25rem;
margin-left: 2.75rem;
margin-bottom: 0.5rem;
}
.modifier {
color: #fbbf24;
font-size: 0.875rem;
font-weight: 600;
}
.item-instructions {
margin-left: 2.75rem;
padding: 0.625rem 0.875rem;
background: #1e293b;
border-left: 3px solid #ef4444;
border-radius: 0.25rem;
color: #fca5a5;
font-size: 0.875rem;
font-weight: 600;
}
.ticket-actions {
display: flex;
gap: 0.75rem;
}
.btn-action {
flex: 1;
padding: 1rem;
border: none;
border-radius: 0.5rem;
font-weight: 700;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
text-transform: uppercase;
}
.btn-action.start {
background: #3b82f6;
color: #fff;
}
.btn-action.start:hover {
background: #2563eb;
}
.btn-action.ready {
background: #fbbf24;
color: #78350f;
}
.btn-action.ready:hover {
background: #f59e0b;
}
.btn-action.complete {
background: #10b981;
color: #fff;
}
.btn-action.complete:hover {
background: #059669;
}
.no-orders {
text-align: center;
padding: 6rem 2rem;
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
}
.no-orders-icon {
font-size: 5rem;
margin-bottom: 1rem;
}
.no-orders-text {
color: #94a3b8;
font-size: 1.5rem;
font-weight: 600;
}

View File

@ -0,0 +1,177 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface PaymentMethodBreakdown {
method: string;
count: number;
amount: number;
percentage: number;
}
interface Payment {
id: string;
orderNumber: string;
method: string;
amount: number;
tip: number;
total: number;
time: string;
status: 'completed' | 'refunded';
}
export default function PaymentSummary() {
const [breakdown, setBreakdown] = useState<PaymentMethodBreakdown[]>([]);
const [recentPayments, setRecentPayments] = useState<Payment[]>([]);
useEffect(() => {
// Mock data
setBreakdown([
{ method: 'Credit Card', count: 82, amount: 5847.25, percentage: 68.4 },
{ method: 'Cash', count: 23, amount: 1234.50, percentage: 14.5 },
{ method: 'Debit Card', count: 18, amount: 987.40, percentage: 11.6 },
{ method: 'Mobile Payment', count: 12, amount: 478.17, percentage: 5.5 },
]);
setRecentPayments([
{ id: '1', orderNumber: 'ORD-142', method: 'Credit Card', amount: 68.50, tip: 12.00, total: 80.50, time: '14:32', status: 'completed' },
{ id: '2', orderNumber: 'ORD-141', method: 'Cash', amount: 45.00, tip: 8.00, total: 53.00, time: '14:28', status: 'completed' },
{ id: '3', orderNumber: 'ORD-140', method: 'Mobile Payment', amount: 92.75, tip: 18.00, total: 110.75, time: '14:25', status: 'completed' },
{ id: '4', orderNumber: 'ORD-139', method: 'Credit Card', amount: 54.25, tip: 10.00, total: 64.25, time: '14:20', status: 'completed' },
{ id: '5', orderNumber: 'ORD-138', method: 'Debit Card', amount: 78.90, tip: 15.00, total: 93.90, time: '14:15', status: 'completed' },
{ id: '6', orderNumber: 'ORD-137', method: 'Credit Card', amount: 34.20, tip: 0.00, total: 34.20, time: '14:10', status: 'refunded' },
]);
}, []);
const totalAmount = breakdown.reduce((sum, b) => sum + b.amount, 0);
const totalTips = recentPayments.reduce((sum, p) => sum + p.tip, 0);
const totalRefunds = recentPayments.filter(p => p.status === 'refunded').reduce((sum, p) => sum + p.total, 0);
const totalTransactions = breakdown.reduce((sum, b) => sum + b.count, 0);
const getMethodColor = (method: string) => {
const colors: Record<string, string> = {
'Credit Card': '#3b82f6',
'Cash': '#10b981',
'Debit Card': '#8b5cf6',
'Mobile Payment': '#fbbf24',
};
return colors[method] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<h1>💳 Payment Summary</h1>
<p>Payment breakdown and transactions</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Payments</div>
<div className="stat-value">${totalAmount.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Tips</div>
<div className="stat-value tips">${totalTips.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Refunds</div>
<div className="stat-value refunds">${totalRefunds.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Transactions</div>
<div className="stat-value">{totalTransactions}</div>
</div>
</div>
<div className="content-grid">
<div className="breakdown-panel">
<h2>Payment Methods</h2>
<div className="breakdown-list">
{breakdown.map((item) => (
<div key={item.method} className="breakdown-item">
<div className="breakdown-header">
<div>
<span
className="method-dot"
style={{ backgroundColor: getMethodColor(item.method) }}
></span>
<span className="method-name">{item.method}</span>
</div>
<span className="method-count">{item.count} transactions</span>
</div>
<div className="breakdown-bar">
<div
className="breakdown-fill"
style={{
width: `${item.percentage}%`,
backgroundColor: getMethodColor(item.method),
}}
></div>
</div>
<div className="breakdown-footer">
<span className="breakdown-amount">${item.amount.toFixed(2)}</span>
<span className="breakdown-percentage">{item.percentage}%</span>
</div>
</div>
))}
</div>
<div className="chart-container">
<div className="pie-chart">
{breakdown.map((item, index) => (
<div
key={item.method}
className="pie-slice"
style={{
backgroundColor: getMethodColor(item.method),
transform: `rotate(${breakdown.slice(0, index).reduce((sum, b) => sum + (b.percentage * 3.6), 0)}deg)`,
}}
></div>
))}
</div>
</div>
</div>
<div className="transactions-panel">
<h2>Recent Transactions</h2>
<div className="transactions-list">
{recentPayments.map((payment) => (
<div key={payment.id} className={`transaction-card ${payment.status}`}>
<div className="transaction-header">
<span className="order-number">{payment.orderNumber}</span>
<span className="transaction-time">{payment.time}</span>
</div>
<div className="transaction-body">
<div className="transaction-details">
<span
className="payment-method"
style={{ color: getMethodColor(payment.method) }}
>
{payment.method}
</span>
<div className="amounts">
<span className="amount">${payment.amount.toFixed(2)}</span>
{payment.tip > 0 && (
<span className="tip">+${payment.tip.toFixed(2)} tip</span>
)}
</div>
</div>
<div className="transaction-total">
<span className="total-label">Total</span>
<span className={`total-amount ${payment.status}`}>
{payment.status === 'refunded' && ''}
${payment.total.toFixed(2)}
</span>
</div>
</div>
{payment.status === 'refunded' && (
<div className="refund-badge">REFUNDED</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Payment Summary - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,293 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.tips {
color: #10b981;
}
.stat-value.refunds {
color: #ef4444;
}
.content-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
.breakdown-panel,
.transactions-panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
h2 {
color: #f8fafc;
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.breakdown-list {
display: flex;
flex-direction: column;
gap: 1.5rem;
margin-bottom: 2rem;
}
.breakdown-item {
background: #0f172a;
padding: 1.25rem;
border-radius: 0.5rem;
}
.breakdown-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.breakdown-header > div {
display: flex;
align-items: center;
gap: 0.75rem;
}
.method-dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.method-name {
color: #f8fafc;
font-weight: 600;
font-size: 1rem;
}
.method-count {
color: #94a3b8;
font-size: 0.875rem;
}
.breakdown-bar {
height: 8px;
background: #334155;
border-radius: 1rem;
overflow: hidden;
margin-bottom: 0.75rem;
}
.breakdown-fill {
height: 100%;
transition: width 0.3s;
}
.breakdown-footer {
display: flex;
justify-content: space-between;
}
.breakdown-amount {
color: #10b981;
font-weight: 700;
font-size: 1.125rem;
}
.breakdown-percentage {
color: #64748b;
font-size: 0.875rem;
}
.chart-container {
display: flex;
justify-content: center;
margin-top: 2rem;
}
.pie-chart {
width: 200px;
height: 200px;
border-radius: 50%;
position: relative;
overflow: hidden;
background: conic-gradient(
#3b82f6 0deg 68.4deg,
#10b981 68.4deg 137.4deg,
#8b5cf6 137.4deg 185.4deg,
#fbbf24 185.4deg 360deg
);
}
.transactions-list {
display: flex;
flex-direction: column;
gap: 1rem;
max-height: 600px;
overflow-y: auto;
}
.transaction-card {
background: #0f172a;
border-radius: 0.5rem;
padding: 1rem;
border: 1px solid #334155;
position: relative;
}
.transaction-card.refunded {
border-color: #ef4444;
opacity: 0.7;
}
.transaction-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.75rem;
}
.order-number {
color: #f8fafc;
font-weight: 600;
font-family: 'Courier New', monospace;
}
.transaction-time {
color: #64748b;
font-size: 0.875rem;
}
.transaction-body {
display: flex;
justify-content: space-between;
align-items: center;
}
.transaction-details {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.payment-method {
font-weight: 600;
font-size: 0.9375rem;
}
.amounts {
display: flex;
gap: 0.5rem;
align-items: center;
}
.amount {
color: #cbd5e1;
font-size: 0.875rem;
}
.tip {
color: #10b981;
font-size: 0.75rem;
}
.transaction-total {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.total-label {
color: #64748b;
font-size: 0.75rem;
text-transform: uppercase;
}
.total-amount {
font-size: 1.25rem;
font-weight: 700;
color: #10b981;
}
.total-amount.refunded {
color: #ef4444;
}
.refund-badge {
position: absolute;
top: 0.5rem;
right: 0.5rem;
padding: 0.25rem 0.625rem;
background: #ef4444;
color: #fff;
border-radius: 0.375rem;
font-size: 0.7rem;
font-weight: 700;
}

View File

@ -0,0 +1,155 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface HourlySales {
hour: string;
revenue: number;
orders: number;
}
interface TopItem {
name: string;
quantity: number;
revenue: number;
}
export default function SalesDashboard() {
const [hourlySales, setHourlySales] = useState<HourlySales[]>([]);
const [topItems, setTopItems] = useState<TopItem[]>([]);
const [stats, setStats] = useState({
totalRevenue: 0,
totalOrders: 0,
avgTicket: 0,
salesVsGoal: 0,
});
useEffect(() => {
// Mock data
setStats({
totalRevenue: 8547.32,
totalOrders: 142,
avgTicket: 60.19,
salesVsGoal: 85.5,
});
setHourlySales([
{ hour: '11am', revenue: 847.50, orders: 15 },
{ hour: '12pm', revenue: 1568.75, orders: 28 },
{ hour: '1pm', revenue: 1892.40, orders: 32 },
{ hour: '2pm', revenue: 1034.50, orders: 18 },
{ hour: '3pm', revenue: 678.20, orders: 12 },
{ hour: '4pm', revenue: 445.80, orders: 8 },
{ hour: '5pm', revenue: 1245.60, orders: 22 },
{ hour: '6pm', revenue: 834.57, orders: 35 },
]);
setTopItems([
{ name: 'Classic Burger', quantity: 45, revenue: 674.55 },
{ name: 'Caesar Salad', quantity: 38, revenue: 379.62 },
{ name: 'Grilled Salmon', quantity: 32, revenue: 799.68 },
{ name: 'Chicken Wings', quantity: 28, revenue: 363.72 },
{ name: 'French Fries', quantity: 52, revenue: 259.48 },
{ name: 'Margherita Pizza', quantity: 24, revenue: 407.76 },
{ name: 'Chocolate Cake', quantity: 22, revenue: 175.78 },
{ name: 'Craft Beer', quantity: 56, revenue: 391.44 },
]);
}, []);
const maxRevenue = Math.max(...hourlySales.map(h => h.revenue));
const maxQuantity = Math.max(...topItems.map(i => i.quantity));
return (
<div className="app">
<header className="app-header">
<h1>📈 Sales Dashboard</h1>
<p>Revenue analytics and top performers</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Revenue</div>
<div className="stat-value revenue">${stats.totalRevenue.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Orders</div>
<div className="stat-value">{stats.totalOrders}</div>
</div>
<div className="stat-card">
<div className="stat-label">Average Ticket</div>
<div className="stat-value">${stats.avgTicket.toFixed(2)}</div>
</div>
<div className="stat-card goal-card">
<div className="stat-label">Sales vs Goal</div>
<div className="stat-value goal">{stats.salesVsGoal}%</div>
<div className="goal-bar">
<div
className="goal-fill"
style={{ width: `${Math.min(stats.salesVsGoal, 100)}%` }}
></div>
</div>
</div>
</div>
<div className="content-grid">
<div className="chart-panel">
<h2>Revenue by Hour</h2>
<div className="hourly-chart">
{hourlySales.map((item) => (
<div key={item.hour} className="hour-bar">
<div
className="bar-fill"
style={{ height: `${(item.revenue / maxRevenue) * 100}%` }}
>
<div className="bar-tooltip">
<div className="tooltip-revenue">${item.revenue.toFixed(0)}</div>
<div className="tooltip-orders">{item.orders} orders</div>
</div>
</div>
<div className="bar-label">{item.hour}</div>
</div>
))}
</div>
<div className="summary-cards">
<div className="summary-card">
<div className="summary-label">Peak Hour</div>
<div className="summary-value">
{hourlySales.reduce((max, h) => h.revenue > max.revenue ? h : max).hour}
</div>
</div>
<div className="summary-card">
<div className="summary-label">Peak Revenue</div>
<div className="summary-value">
${Math.max(...hourlySales.map(h => h.revenue)).toFixed(2)}
</div>
</div>
</div>
</div>
<div className="top-items-panel">
<h2>Top Selling Items</h2>
<div className="items-list">
{topItems.map((item, index) => (
<div key={item.name} className="item-row">
<div className="item-rank">#{index + 1}</div>
<div className="item-info">
<div className="item-name">{item.name}</div>
<div className="item-bar">
<div
className="item-bar-fill"
style={{ width: `${(item.quantity / maxQuantity) * 100}%` }}
></div>
</div>
<div className="item-stats">
<span className="item-quantity">{item.quantity} sold</span>
<span className="item-revenue">${item.revenue.toFixed(2)}</span>
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sales Dashboard - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,280 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
text-align: center;
}
.stat-card.goal-card {
border-color: #10b981;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.revenue {
color: #10b981;
}
.stat-value.goal {
color: #10b981;
}
.goal-bar {
height: 6px;
background: #334155;
border-radius: 1rem;
overflow: hidden;
margin-top: 0.75rem;
}
.goal-fill {
height: 100%;
background: linear-gradient(to right, #10b981, #34d399);
transition: width 0.5s;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
@media (max-width: 1200px) {
.content-grid {
grid-template-columns: 1fr;
}
}
.chart-panel,
.top-items-panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
h2 {
color: #f8fafc;
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.hourly-chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 0.5rem;
height: 300px;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
margin-bottom: 1.5rem;
}
.hour-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
}
.bar-fill {
width: 100%;
background: linear-gradient(to top, #3b82f6, #60a5fa);
border-radius: 0.25rem;
position: relative;
min-height: 20px;
cursor: pointer;
transition: all 0.2s;
}
.bar-fill:hover {
background: linear-gradient(to top, #2563eb, #3b82f6);
}
.bar-tooltip {
position: absolute;
top: -3.5rem;
left: 50%;
transform: translateX(-50%);
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.375rem;
padding: 0.5rem;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
white-space: nowrap;
}
.bar-fill:hover .bar-tooltip {
opacity: 1;
}
.tooltip-revenue {
color: #10b981;
font-weight: 700;
font-size: 0.875rem;
}
.tooltip-orders {
color: #94a3b8;
font-size: 0.75rem;
}
.bar-label {
color: #94a3b8;
font-size: 0.75rem;
margin-top: 0.5rem;
font-weight: 600;
}
.summary-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.summary-card {
background: #0f172a;
padding: 1.25rem;
border-radius: 0.5rem;
text-align: center;
}
.summary-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.summary-value {
color: #f8fafc;
font-size: 1.5rem;
font-weight: 700;
}
.items-list {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.item-row {
display: flex;
gap: 1rem;
background: #0f172a;
padding: 1rem;
border-radius: 0.5rem;
}
.item-rank {
width: 36px;
height: 36px;
background: #3b82f6;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
font-size: 0.875rem;
flex-shrink: 0;
}
.item-info {
flex: 1;
}
.item-name {
color: #f8fafc;
font-weight: 600;
margin-bottom: 0.5rem;
font-size: 1rem;
}
.item-bar {
height: 6px;
background: #334155;
border-radius: 1rem;
overflow: hidden;
margin-bottom: 0.5rem;
}
.item-bar-fill {
height: 100%;
background: linear-gradient(to right, #10b981, #34d399);
transition: width 0.3s;
}
.item-stats {
display: flex;
justify-content: space-between;
font-size: 0.875rem;
}
.item-quantity {
color: #94a3b8;
}
.item-revenue {
color: #10b981;
font-weight: 700;
}