Clover: Complete MCP server with 50+ tools and 18 React apps

- API client with Clover REST API v3 integration (OAuth2 + API key auth)
- 50+ comprehensive tools across 10 categories:
  * Orders: list, get, create, update, delete, add/remove line items, discounts, payments, fire order
  * Inventory: items, categories, modifiers, stock management
  * Customers: CRUD, search, addresses, payment cards
  * Employees: CRUD, roles, shifts, clock in/out
  * Payments: list, get, refunds
  * Merchants: settings, devices, tender types
  * Discounts: CRUD operations
  * Taxes: CRUD, tax rates
  * Reports: sales summary, revenue by item/category, employee performance
  * Cash: cash drawer tracking and events

- 18 React MCP apps with full UI:
  * Order management: dashboard, detail, grid
  * Inventory: dashboard, detail, category manager
  * Customer: detail, grid
  * Employee: dashboard, schedule
  * Payment history
  * Analytics: sales dashboard, revenue by item, revenue by category
  * Configuration: discount manager, tax manager, device manager
  * Cash drawer

- Complete TypeScript types for Clover API
- Pagination support with automatic result fetching
- Comprehensive error handling
- Full README with examples and setup guide
This commit is contained in:
Jake Shore 2026-02-12 17:42:59 -05:00
parent 60c457b8fb
commit 7ee40342c8
38 changed files with 4834 additions and 417 deletions

View File

@ -1,95 +1,302 @@
# Clover MCP Server
MCP server for [Clover POS](https://www.clover.com/) API integration. Access orders, inventory, customers, payments, and merchant data.
A comprehensive Model Context Protocol (MCP) server for the Clover POS platform, providing 50+ tools and 18 React applications for complete point-of-sale management.
## Setup
## Features
### 🛠️ 50+ MCP Tools
#### Orders Management (10 tools)
- `clover_list_orders` - List orders with filtering and pagination
- `clover_get_order` - Get order details with expanded data
- `clover_create_order` - Create new orders
- `clover_update_order` - Update existing orders
- `clover_delete_order` - Delete orders
- `clover_add_line_item` - Add items to orders
- `clover_remove_line_item` - Remove items from orders
- `clover_add_order_discount` - Apply discounts to orders
- `clover_list_order_payments` - List payments for an order
- `clover_fire_order` - Send order to kitchen
#### Inventory Management (11 tools)
- `clover_list_items` - List inventory items
- `clover_get_item` - Get item details
- `clover_create_item` - Create new items
- `clover_update_item` - Update existing items
- `clover_delete_item` - Delete items
- `clover_list_categories` - List item categories
- `clover_create_category` - Create new category
- `clover_list_modifier_groups` - List modifier groups
- `clover_create_modifier` - Create modifiers
- `clover_list_item_stocks` - List stock levels
- `clover_update_item_stock` - Update stock quantities
#### Customer Management (9 tools)
- `clover_list_customers` - List customers
- `clover_get_customer` - Get customer details
- `clover_create_customer` - Create new customer
- `clover_update_customer` - Update customer info
- `clover_delete_customer` - Delete customer
- `clover_search_customers` - Search by name/phone/email
- `clover_list_customer_addresses` - List customer addresses
- `clover_add_customer_address` - Add address to customer
- `clover_list_customer_cards` - List saved payment cards
#### Employee Management (10 tools)
- `clover_list_employees` - List employees
- `clover_get_employee` - Get employee details
- `clover_create_employee` - Create new employee
- `clover_update_employee` - Update employee info
- `clover_delete_employee` - Delete employee
- `clover_list_employee_roles` - List available roles
- `clover_list_employee_shifts` - List shifts for employee
- `clover_create_shift` - Create a shift
- `clover_clock_in_employee` - Clock in employee
- `clover_clock_out_employee` - Clock out employee
#### Payment Management (4 tools)
- `clover_list_payments` - List payments
- `clover_get_payment` - Get payment details
- `clover_create_refund` - Create refund
- `clover_list_refunds` - List refunds for payment
#### Merchant Settings (5 tools)
- `clover_get_merchant` - Get merchant information
- `clover_update_merchant` - Update merchant settings
- `clover_list_devices` - List POS devices
- `clover_get_device` - Get device details
- `clover_list_tender_types` - List payment methods
#### Discounts (5 tools)
- `clover_list_discounts` - List discounts
- `clover_get_discount` - Get discount details
- `clover_create_discount` - Create discount
- `clover_update_discount` - Update discount
- `clover_delete_discount` - Delete discount
#### Tax Management (5 tools)
- `clover_list_tax_rates` - List tax rates
- `clover_get_tax_rate` - Get tax rate details
- `clover_create_tax_rate` - Create tax rate
- `clover_update_tax_rate` - Update tax rate
- `clover_delete_tax_rate` - Delete tax rate
#### Reports & Analytics (4 tools)
- `clover_sales_summary` - Get sales summary for date range
- `clover_revenue_by_item` - Revenue breakdown by item
- `clover_revenue_by_category` - Revenue breakdown by category
- `clover_employee_performance` - Employee performance report
#### Cash Management (2 tools)
- `clover_list_cash_events` - List cash drawer events
- `clover_get_cash_drawer` - Get cash drawer status
### 📱 18 React MCP Apps
#### Order Management
- **order-dashboard** - Overview of order metrics and quick actions
- **order-detail** - Detailed order view with line items
- **order-grid** - Searchable grid of all orders
#### Inventory Management
- **inventory-dashboard** - Inventory metrics and quick actions
- **inventory-detail** - Item management with search
- **category-manager** - Category organization
#### Customer Management
- **customer-detail** - Detailed customer profile
- **customer-grid** - Customer directory with search
#### Employee Management
- **employee-dashboard** - Employee metrics and performance
- **employee-schedule** - Shift management and time tracking
#### Payments
- **payment-history** - Payment transactions and refunds
#### Reports & Analytics
- **sales-dashboard** - Sales metrics and trends
- **revenue-by-item** - Top performing items
- **revenue-by-category** - Category performance
#### Configuration
- **discount-manager** - Discount creation and management
- **tax-manager** - Tax rate configuration
- **device-manager** - POS device inventory
#### Cash Management
- **cash-drawer** - Cash drawer tracking and events
## Installation
```bash
npm install
npm run build
```
## Environment Variables
## Configuration
| Variable | Required | Description |
|----------|----------|-------------|
| `CLOVER_API_KEY` | Yes | OAuth access token or API token |
| `CLOVER_MERCHANT_ID` | Yes | 13-character merchant ID |
| `CLOVER_SANDBOX` | No | Set to `"true"` for sandbox environment |
| `CLOVER_REGION` | No | `"US"` (default), `"EU"`, or `"LA"` |
Set the following environment variables:
## API Endpoints
```bash
# Required
export CLOVER_MERCHANT_ID="your_merchant_id"
- **Production US/Canada:** `https://api.clover.com`
- **Production Europe:** `https://api.eu.clover.com`
- **Production LATAM:** `https://api.la.clover.com`
- **Sandbox:** `https://apisandbox.dev.clover.com`
# One of these is required
export CLOVER_API_KEY="your_api_key"
# OR
export CLOVER_ACCESS_TOKEN="your_oauth_token"
## Tools
# Optional (default: sandbox)
export CLOVER_ENVIRONMENT="sandbox" # or "production"
```
### Orders
- **list_orders** - List orders with optional filtering by state
- **get_order** - Get order details including line items and payments
- **create_order** - Create new orders (supports atomic orders with line items)
### Getting Clover Credentials
### Inventory
- **list_items** - List products/menu items available for sale
- **get_inventory** - Get stock counts for items
1. **Sandbox Access**: Sign up at https://sandbox.dev.clover.com/
2. **API Key**: Go to Setup → API Tokens in your Clover dashboard
3. **OAuth Token**: Implement OAuth2 flow for production apps
### Customers & Payments
- **list_customers** - List customer database entries
- **list_payments** - List payment transactions
## Usage
### Merchant
- **get_merchant** - Get merchant account information
### Running the Server
## Usage with Claude Desktop
```bash
npm start
```
Add to your `claude_desktop_config.json`:
### MCP Client Configuration
Add to your MCP client settings (e.g., Claude Desktop):
```json
{
"mcpServers": {
"clover": {
"command": "node",
"args": ["/path/to/mcp-servers/clover/dist/index.js"],
"args": ["/path/to/clover/dist/main.js"],
"env": {
"CLOVER_API_KEY": "your-api-token",
"CLOVER_MERCHANT_ID": "your-merchant-id",
"CLOVER_SANDBOX": "true"
"CLOVER_MERCHANT_ID": "your_merchant_id",
"CLOVER_API_KEY": "your_api_key",
"CLOVER_ENVIRONMENT": "sandbox"
}
}
}
}
```
## Authentication
## API Reference
Clover uses OAuth 2.0. You need either:
1. **Test API Token** - Generate in Clover Developer Dashboard for sandbox testing
2. **OAuth Access Token** - Obtained through OAuth flow for production apps
### Tool Usage Examples
See [Clover Authentication Docs](https://docs.clover.com/dev/docs/use-oauth) for details.
## Examples
List open orders:
```
list_orders(filter: "state=open", limit: 10)
#### List Orders
```typescript
// List all open orders
await mcp.callTool('clover_list_orders', {
filter: 'state=open',
expand: 'lineItems,customers'
});
```
Get order with line items:
```
get_order(order_id: "ABC123", expand: "lineItems,payments")
#### Create Order with Items
```typescript
// Create order
const order = await mcp.callTool('clover_create_order', {
state: 'open',
title: 'Table 5'
});
// Add items
await mcp.callTool('clover_add_line_item', {
orderId: order.id,
itemId: 'ITEM_ID',
unitQty: 2
});
```
Create an order with items:
#### Generate Sales Report
```typescript
const summary = await mcp.callTool('clover_sales_summary', {
startDate: Date.now() - 7 * 24 * 60 * 60 * 1000, // 7 days ago
endDate: Date.now()
});
```
create_order(
title: "Table 5",
line_items: [
{ item_id: "ITEM123", quantity: 2 },
{ name: "Custom Item", price: 999 }
]
)
### React App Resources
Apps are available as MCP resources:
```typescript
const apps = await mcp.listResources();
// Returns: clover://app/order-dashboard, clover://app/inventory-detail, etc.
const appCode = await mcp.readResource('clover://app/order-dashboard');
// Returns the React component source
```
## Architecture
```
src/
├── clients/
│ └── clover.ts # Clover API client with pagination
├── tools/
│ ├── orders-tools.ts # Order management tools
│ ├── inventory-tools.ts # Inventory tools
│ ├── customers-tools.ts # Customer tools
│ ├── employees-tools.ts # Employee tools
│ ├── payments-tools.ts # Payment tools
│ ├── merchants-tools.ts # Merchant settings tools
│ ├── discounts-tools.ts # Discount tools
│ ├── taxes-tools.ts # Tax tools
│ ├── reports-tools.ts # Analytics tools
│ └── cash-tools.ts # Cash management tools
├── ui/
│ └── react-app/ # 18 React MCP apps
├── types/
│ └── index.ts # TypeScript type definitions
├── server.ts # MCP server implementation
└── main.ts # Entry point
```
## Development
### Build
```bash
npm run build
```
### Watch Mode
```bash
npm run dev
```
## API Coverage
This server implements the Clover REST API v3:
- Base URL (Sandbox): `https://sandbox.dev.clover.com`
- Base URL (Production): `https://api.clover.com`
- Documentation: https://docs.clover.com/docs
## License
MIT
## Contributing
Contributions welcome! Please ensure:
- TypeScript types are properly defined
- Tools follow the existing naming convention
- React apps use the standard UI patterns
- All tools include proper error handling
## Support
For issues related to:
- **This MCP server**: Open a GitHub issue
- **Clover API**: Check https://docs.clover.com/
- **MCP Protocol**: See https://modelcontextprotocol.io/
---
Built with ❤️ for the MCPEngine ecosystem

View File

@ -1,20 +1,37 @@
{
"name": "mcp-server-clover",
"name": "@mcpengine/clover-server",
"version": "1.0.0",
"description": "MCP server for Clover POS platform with comprehensive tools and React apps",
"type": "module",
"main": "dist/index.js",
"main": "dist/main.js",
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"build": "tsc && node scripts/copy-assets.js",
"dev": "tsc --watch",
"start": "node dist/main.js",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"clover",
"pos",
"point-of-sale",
"payments",
"inventory",
"orders"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"lucide-react": "^0.469.0"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^22.10.2",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,36 @@
#!/usr/bin/env node
import { copyFileSync, mkdirSync, readdirSync, statSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const srcDir = join(__dirname, '..', 'src', 'ui');
const distDir = join(__dirname, '..', 'dist', 'ui');
function copyDir(src, dest) {
mkdirSync(dest, { recursive: true });
const entries = readdirSync(src);
for (const entry of entries) {
const srcPath = join(src, entry);
const destPath = join(dest, entry);
if (statSync(srcPath).isDirectory()) {
copyDir(srcPath, destPath);
} else if (entry.endsWith('.tsx') || entry.endsWith('.jsx')) {
copyFileSync(srcPath, destPath);
}
}
}
try {
console.log('Copying React apps to dist...');
copyDir(srcDir, distDir);
console.log('Assets copied successfully');
} catch (error) {
console.error('Error copying assets:', error);
process.exit(1);
}

View File

@ -0,0 +1,105 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import { CloverConfig, PaginatedResponse } from '../types/index.js';
export class CloverClient {
private client: AxiosInstance;
private merchantId: string;
constructor(config: CloverConfig) {
this.merchantId = config.merchantId;
const baseURL = config.environment === 'production'
? 'https://api.clover.com'
: 'https://sandbox.dev.clover.com';
this.client = axios.create({
baseURL,
headers: {
'Content-Type': 'application/json',
...(config.accessToken && { 'Authorization': `Bearer ${config.accessToken}` }),
},
params: {
...(config.apiKey && { access_token: config.apiKey }),
},
});
// Response interceptor for error handling
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response) {
throw new Error(`Clover API Error: ${error.response.status} - ${JSON.stringify(error.response.data)}`);
} else if (error.request) {
throw new Error('Clover API Error: No response received');
} else {
throw new Error(`Clover API Error: ${error.message}`);
}
}
);
}
// Generic GET method with pagination support
async get<T>(endpoint: string, params: Record<string, any> = {}): Promise<T> {
const url = `/v3/merchants/${this.merchantId}${endpoint}`;
const response = await this.client.get<T>(url, { params });
return response.data;
}
// Generic POST method
async post<T>(endpoint: string, data: any): Promise<T> {
const url = `/v3/merchants/${this.merchantId}${endpoint}`;
const response = await this.client.post<T>(url, data);
return response.data;
}
// Generic PUT method
async put<T>(endpoint: string, data: any): Promise<T> {
const url = `/v3/merchants/${this.merchantId}${endpoint}`;
const response = await this.client.put<T>(url, data);
return response.data;
}
// Generic DELETE method
async delete<T>(endpoint: string): Promise<T> {
const url = `/v3/merchants/${this.merchantId}${endpoint}`;
const response = await this.client.delete<T>(url);
return response.data;
}
// Paginated fetch helper
async fetchPaginated<T>(
endpoint: string,
params: Record<string, any> = {},
limit?: number
): Promise<T[]> {
const allItems: T[] = [];
let offset = 0;
const pageSize = 100;
while (true) {
const response = await this.get<PaginatedResponse<T>>(endpoint, {
...params,
limit: pageSize,
offset,
});
allItems.push(...response.elements);
if (response.elements.length < pageSize || (limit && allItems.length >= limit)) {
break;
}
offset += pageSize;
if (limit && allItems.length >= limit) {
return allItems.slice(0, limit);
}
}
return allItems;
}
getMerchantId(): string {
return this.merchantId;
}
}

16
servers/clover/src/global.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
// Global type declarations for React MCP apps
interface MCPClient {
callTool(name: string, args?: any): Promise<any>;
showApp(name: string, params?: any): void;
listResources(): Promise<any>;
readResource(uri: string): Promise<any>;
}
declare global {
interface Window {
mcp: MCPClient;
}
}
export {};

View File

@ -1,349 +0,0 @@
#!/usr/bin/env node
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js";
// ============================================
// CONFIGURATION
// ============================================
const MCP_NAME = "clover";
const MCP_VERSION = "1.0.0";
// Clover API base URLs
// Production: https://api.clover.com (US/Canada), https://api.eu.clover.com (Europe), https://api.la.clover.com (LATAM)
// Sandbox: https://apisandbox.dev.clover.com
const API_BASE_URL = process.env.CLOVER_SANDBOX === "true"
? "https://apisandbox.dev.clover.com"
: (process.env.CLOVER_REGION === "EU"
? "https://api.eu.clover.com"
: process.env.CLOVER_REGION === "LA"
? "https://api.la.clover.com"
: "https://api.clover.com");
// ============================================
// API CLIENT
// ============================================
class CloverClient {
private apiKey: string;
private merchantId: string;
private baseUrl: string;
constructor(apiKey: string, merchantId: string) {
this.apiKey = apiKey;
this.merchantId = merchantId;
this.baseUrl = API_BASE_URL;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Clover API error: ${response.status} ${response.statusText} - ${errorText}`);
}
return response.json();
}
async get(endpoint: string) {
return this.request(endpoint, { method: "GET" });
}
async post(endpoint: string, data: any) {
return this.request(endpoint, {
method: "POST",
body: JSON.stringify(data),
});
}
async put(endpoint: string, data: any) {
return this.request(endpoint, {
method: "PUT",
body: JSON.stringify(data),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
getMerchantId() {
return this.merchantId;
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_orders",
description: "List orders for the merchant. Returns paginated list of orders with details like totals, state, and timestamps.",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max orders to return (default 100)" },
offset: { type: "number", description: "Pagination offset" },
filter: { type: "string", description: "Filter query (e.g., 'state=open')" },
expand: { type: "string", description: "Expand related objects (e.g., 'lineItems,payments')" },
},
},
},
{
name: "get_order",
description: "Get a specific order by ID with full details including line items, payments, and discounts.",
inputSchema: {
type: "object" as const,
properties: {
order_id: { type: "string", description: "Order ID" },
expand: { type: "string", description: "Expand related objects (e.g., 'lineItems,payments,discounts')" },
},
required: ["order_id"],
},
},
{
name: "create_order",
description: "Create a new order. Use atomic_order for complete orders with line items in one call.",
inputSchema: {
type: "object" as const,
properties: {
state: { type: "string", description: "Order state: 'open', 'locked', etc." },
title: { type: "string", description: "Order title/note" },
note: { type: "string", description: "Additional order notes" },
order_type_id: { type: "string", description: "Order type ID" },
line_items: {
type: "array",
description: "Array of line items with item_id, quantity, and optional modifications",
items: {
type: "object",
properties: {
item_id: { type: "string" },
quantity: { type: "number" },
price: { type: "number" },
name: { type: "string" },
}
}
},
},
},
},
{
name: "list_items",
description: "List inventory items (products) available for sale. Returns item details, prices, and stock info.",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max items to return (default 100)" },
offset: { type: "number", description: "Pagination offset" },
filter: { type: "string", description: "Filter by name, SKU, etc." },
expand: { type: "string", description: "Expand related objects (e.g., 'categories,modifierGroups,tags')" },
},
},
},
{
name: "get_inventory",
description: "Get inventory stock counts for items. Shows current quantity and tracking status.",
inputSchema: {
type: "object" as const,
properties: {
item_id: { type: "string", description: "Specific item ID (optional - omit to get all)" },
},
},
},
{
name: "list_customers",
description: "List customers in the merchant's customer database. Includes contact info and marketing preferences.",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max customers to return (default 100)" },
offset: { type: "number", description: "Pagination offset" },
filter: { type: "string", description: "Filter by name, email, phone" },
expand: { type: "string", description: "Expand related objects (e.g., 'addresses,emailAddresses,phoneNumbers')" },
},
},
},
{
name: "list_payments",
description: "List payments processed by the merchant. Includes payment method, amount, status, and related order.",
inputSchema: {
type: "object" as const,
properties: {
limit: { type: "number", description: "Max payments to return (default 100)" },
offset: { type: "number", description: "Pagination offset" },
filter: { type: "string", description: "Filter by result (SUCCESS, DECLINED, etc.)" },
expand: { type: "string", description: "Expand related objects (e.g., 'tender,order')" },
},
},
},
{
name: "get_merchant",
description: "Get merchant account information including business name, address, timezone, and settings.",
inputSchema: {
type: "object" as const,
properties: {
expand: { type: "string", description: "Expand related objects (e.g., 'address,openingHours,owner')" },
},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: CloverClient, name: string, args: any) {
const mId = client.getMerchantId();
switch (name) {
case "list_orders": {
const { limit = 100, offset = 0, filter, expand } = args;
let endpoint = `/v3/merchants/${mId}/orders?limit=${limit}&offset=${offset}`;
if (filter) endpoint += `&filter=${encodeURIComponent(filter)}`;
if (expand) endpoint += `&expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
case "get_order": {
const { order_id, expand } = args;
let endpoint = `/v3/merchants/${mId}/orders/${order_id}`;
if (expand) endpoint += `?expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
case "create_order": {
const { state = "open", title, note, order_type_id, line_items } = args;
// If line_items provided, use atomic order endpoint
if (line_items && line_items.length > 0) {
const orderData: any = {
orderCart: {
lineItems: line_items.map((item: any) => ({
item: item.item_id ? { id: item.item_id } : undefined,
name: item.name,
price: item.price,
unitQty: item.quantity || 1,
})),
},
};
if (title) orderData.orderCart.title = title;
if (note) orderData.orderCart.note = note;
if (order_type_id) orderData.orderCart.orderType = { id: order_type_id };
return await client.post(`/v3/merchants/${mId}/atomic_order/orders`, orderData);
}
// Simple order creation
const orderData: any = { state };
if (title) orderData.title = title;
if (note) orderData.note = note;
if (order_type_id) orderData.orderType = { id: order_type_id };
return await client.post(`/v3/merchants/${mId}/orders`, orderData);
}
case "list_items": {
const { limit = 100, offset = 0, filter, expand } = args;
let endpoint = `/v3/merchants/${mId}/items?limit=${limit}&offset=${offset}`;
if (filter) endpoint += `&filter=${encodeURIComponent(filter)}`;
if (expand) endpoint += `&expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
case "get_inventory": {
const { item_id } = args;
if (item_id) {
return await client.get(`/v3/merchants/${mId}/item_stocks/${item_id}`);
}
return await client.get(`/v3/merchants/${mId}/item_stocks`);
}
case "list_customers": {
const { limit = 100, offset = 0, filter, expand } = args;
let endpoint = `/v3/merchants/${mId}/customers?limit=${limit}&offset=${offset}`;
if (filter) endpoint += `&filter=${encodeURIComponent(filter)}`;
if (expand) endpoint += `&expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
case "list_payments": {
const { limit = 100, offset = 0, filter, expand } = args;
let endpoint = `/v3/merchants/${mId}/payments?limit=${limit}&offset=${offset}`;
if (filter) endpoint += `&filter=${encodeURIComponent(filter)}`;
if (expand) endpoint += `&expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
case "get_merchant": {
const { expand } = args;
let endpoint = `/v3/merchants/${mId}`;
if (expand) endpoint += `?expand=${encodeURIComponent(expand)}`;
return await client.get(endpoint);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const apiKey = process.env.CLOVER_API_KEY;
const merchantId = process.env.CLOVER_MERCHANT_ID;
if (!apiKey) {
console.error("Error: CLOVER_API_KEY environment variable required");
process.exit(1);
}
if (!merchantId) {
console.error("Error: CLOVER_MERCHANT_ID environment variable required");
process.exit(1);
}
const client = new CloverClient(apiKey, merchantId);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,46 @@
#!/usr/bin/env node
import { CloverServer } from './server.js';
import { CloverConfig } from './types/index.js';
function getConfig(): CloverConfig {
const merchantId = process.env.CLOVER_MERCHANT_ID;
const apiKey = process.env.CLOVER_API_KEY;
const accessToken = process.env.CLOVER_ACCESS_TOKEN;
const environment = (process.env.CLOVER_ENVIRONMENT || 'sandbox') as 'sandbox' | 'production';
if (!merchantId) {
console.error('Error: CLOVER_MERCHANT_ID environment variable is required');
process.exit(1);
}
if (!apiKey && !accessToken) {
console.error('Error: Either CLOVER_API_KEY or CLOVER_ACCESS_TOKEN must be set');
process.exit(1);
}
return {
merchantId,
apiKey,
accessToken,
environment,
};
}
async function main() {
try {
const config = getConfig();
const server = new CloverServer(config);
console.error('Clover MCP Server starting...');
console.error(`Merchant ID: ${config.merchantId}`);
console.error(`Environment: ${config.environment}`);
console.error('Server running. Ready for MCP connections.');
await server.run();
} catch (error: any) {
console.error('Fatal error:', error.message);
process.exit(1);
}
}
main();

View File

@ -0,0 +1,181 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { CloverClient } from './clients/clover.js';
import { CloverConfig } from './types/index.js';
// Tool imports
import { createOrdersTools } from './tools/orders-tools.js';
import { createInventoryTools } from './tools/inventory-tools.js';
import { createCustomersTools } from './tools/customers-tools.js';
import { createEmployeesTools } from './tools/employees-tools.js';
import { createPaymentsTools } from './tools/payments-tools.js';
import { createMerchantsTools } from './tools/merchants-tools.js';
import { createDiscountsTools } from './tools/discounts-tools.js';
import { createTaxesTools } from './tools/taxes-tools.js';
import { createReportsTools } from './tools/reports-tools.js';
import { createCashTools } from './tools/cash-tools.js';
// React app imports
import { readFileSync, readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
export class CloverServer {
private server: Server;
private client: CloverClient;
private tools: Map<string, any>;
private apps: Map<string, string>;
constructor(config: CloverConfig) {
this.server = new Server(
{
name: 'clover-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
resources: {},
},
}
);
this.client = new CloverClient(config);
this.tools = new Map();
this.apps = new Map();
this.initializeTools();
this.loadApps();
this.setupHandlers();
}
private initializeTools() {
const toolGroups = [
createOrdersTools(this.client),
createInventoryTools(this.client),
createCustomersTools(this.client),
createEmployeesTools(this.client),
createPaymentsTools(this.client),
createMerchantsTools(this.client),
createDiscountsTools(this.client),
createTaxesTools(this.client),
createReportsTools(this.client),
createCashTools(this.client),
];
toolGroups.forEach((group) => {
Object.entries(group).forEach(([name, tool]) => {
this.tools.set(name, tool);
});
});
}
private loadApps() {
const appsDir = join(__dirname, 'ui', 'react-app');
try {
const files = readdirSync(appsDir).filter((f) => f.endsWith('.tsx'));
files.forEach((file) => {
const appName = file.replace('.tsx', '');
const content = readFileSync(join(appsDir, file), 'utf-8');
this.apps.set(appName, content);
});
} catch (error) {
console.warn('Could not load React apps:', error);
}
}
private setupHandlers() {
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = Array.from(this.tools.entries()).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.inputSchema,
}));
return { tools };
});
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
const tool = this.tools.get(name);
if (!tool) {
throw new Error(`Unknown tool: ${name}`);
}
try {
const result = await tool.handler(args || {});
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error: any) {
return {
content: [
{
type: 'text',
text: `Error: ${error.message}`,
},
],
isError: true,
};
}
});
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
const resources = Array.from(this.apps.keys()).map((appName) => ({
uri: `clover://app/${appName}`,
name: appName.replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()),
mimeType: 'text/tsx',
description: `Clover ${appName.replace(/-/g, ' ')} React app`,
}));
return { resources };
});
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const uri = request.params.uri;
const match = uri.match(/^clover:\/\/app\/(.+)$/);
if (!match) {
throw new Error(`Invalid resource URI: ${uri}`);
}
const appName = match[1];
const appCode = this.apps.get(appName);
if (!appCode) {
throw new Error(`App not found: ${appName}`);
}
return {
contents: [
{
uri,
mimeType: 'text/tsx',
text: appCode,
},
],
};
});
}
async run() {
const transport = new StdioServerTransport();
await this.server.connect(transport);
}
}

View File

@ -0,0 +1,78 @@
import { CloverClient } from '../clients/clover.js';
import { CloverCashEvent } from '../types/index.js';
export function createCashTools(client: CloverClient) {
return {
clover_list_cash_events: {
description: 'List cash drawer events',
inputSchema: {
type: 'object',
properties: {
deviceId: {
type: 'string',
description: 'Filter by device ID',
},
employeeId: {
type: 'string',
description: 'Filter by employee ID',
},
filter: {
type: 'string',
description: 'Additional filter expression',
},
limit: {
type: 'number',
description: 'Maximum number of events to return',
},
},
},
handler: async (args: any) => {
let filter = args.filter || '';
if (args.deviceId) {
filter += (filter ? ' AND ' : '') + `device.id='${args.deviceId}'`;
}
if (args.employeeId) {
filter += (filter ? ' AND ' : '') + `employee.id='${args.employeeId}'`;
}
const events = await client.fetchPaginated<CloverCashEvent>(
'/cash_events',
{ filter },
args.limit
);
return { cashEvents: events, count: events.length };
},
},
clover_get_cash_drawer: {
description: 'Get current cash drawer status for a device',
inputSchema: {
type: 'object',
properties: {
deviceId: { type: 'string', description: 'Device ID' },
},
required: ['deviceId'],
},
handler: async (args: any) => {
// Get all cash events for this device
const events = await client.fetchPaginated<CloverCashEvent>(
'/cash_events',
{ filter: `device.id='${args.deviceId}'` }
);
// Calculate current balance
let balance = 0;
events.forEach((event) => {
balance += event.amountChange;
});
return {
deviceId: args.deviceId,
currentBalance: balance,
eventCount: events.length,
lastEvent: events[0] || null,
};
},
},
};
}

View File

@ -0,0 +1,187 @@
import { CloverClient } from '../clients/clover.js';
import { CloverCustomer, CloverAddress, CloverEmailAddress, CloverPhoneNumber } from '../types/index.js';
export function createCustomersTools(client: CloverClient) {
return {
clover_list_customers: {
description: 'List customers',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression',
},
expand: {
type: 'string',
description: 'Comma-separated fields to expand (addresses, emailAddresses, phoneNumbers, cards)',
},
limit: {
type: 'number',
description: 'Maximum number of customers to return',
},
},
},
handler: async (args: any) => {
const customers = await client.fetchPaginated<CloverCustomer>(
'/customers',
{ filter: args.filter, expand: args.expand },
args.limit
);
return { customers, count: customers.length };
},
},
clover_get_customer: {
description: 'Get a specific customer by ID',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
expand: {
type: 'string',
description: 'Comma-separated fields to expand',
},
},
required: ['customerId'],
},
handler: async (args: any) => {
return await client.get<CloverCustomer>(`/customers/${args.customerId}`, {
expand: args.expand,
});
},
},
clover_create_customer: {
description: 'Create a new customer',
inputSchema: {
type: 'object',
properties: {
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
marketingAllowed: {
type: 'boolean',
description: 'Marketing opt-in',
},
},
required: ['firstName', 'lastName'],
},
handler: async (args: any) => {
return await client.post<CloverCustomer>('/customers', {
firstName: args.firstName,
lastName: args.lastName,
marketingAllowed: args.marketingAllowed,
});
},
},
clover_update_customer: {
description: 'Update an existing customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
firstName: { type: 'string', description: 'First name' },
lastName: { type: 'string', description: 'Last name' },
marketingAllowed: {
type: 'boolean',
description: 'Marketing opt-in',
},
},
required: ['customerId'],
},
handler: async (args: any) => {
const { customerId, ...updateData } = args;
return await client.post<CloverCustomer>(`/customers/${customerId}`, updateData);
},
},
clover_delete_customer: {
description: 'Delete a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (args: any) => {
await client.delete(`/customers/${args.customerId}`);
return { success: true, customerId: args.customerId };
},
},
clover_search_customers: {
description: 'Search customers by name, phone, or email',
inputSchema: {
type: 'object',
properties: {
query: {
type: 'string',
description: 'Search query (name, phone, or email)',
},
},
required: ['query'],
},
handler: async (args: any) => {
const customers = await client.fetchPaginated<CloverCustomer>(
'/customers',
{ filter: `firstName~'${args.query}' OR lastName~'${args.query}'` }
);
return { customers, count: customers.length };
},
},
clover_list_customer_addresses: {
description: 'List addresses for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (args: any) => {
return await client.get(`/customers/${args.customerId}/addresses`);
},
},
clover_add_customer_address: {
description: 'Add an address to a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
address1: { type: 'string', description: 'Address line 1' },
address2: { type: 'string', description: 'Address line 2' },
city: { type: 'string', description: 'City' },
state: { type: 'string', description: 'State/Province' },
zip: { type: 'string', description: 'ZIP/Postal code' },
country: { type: 'string', description: 'Country code' },
},
required: ['customerId', 'address1', 'city', 'state', 'zip'],
},
handler: async (args: any) => {
const { customerId, ...addressData } = args;
return await client.post<CloverAddress>(
`/customers/${customerId}/addresses`,
addressData
);
},
},
clover_list_customer_cards: {
description: 'List payment cards for a customer',
inputSchema: {
type: 'object',
properties: {
customerId: { type: 'string', description: 'Customer ID' },
},
required: ['customerId'],
},
handler: async (args: any) => {
return await client.get(`/customers/${args.customerId}/cards`);
},
},
};
}

View File

@ -0,0 +1,105 @@
import { CloverClient } from '../clients/clover.js';
import { CloverDiscount } from '../types/index.js';
export function createDiscountsTools(client: CloverClient) {
return {
clover_list_discounts: {
description: 'List all discounts',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression',
},
},
},
handler: async (args: any) => {
const discounts = await client.fetchPaginated<CloverDiscount>(
'/discounts',
{ filter: args.filter }
);
return { discounts, count: discounts.length };
},
},
clover_get_discount: {
description: 'Get a specific discount by ID',
inputSchema: {
type: 'object',
properties: {
discountId: { type: 'string', description: 'Discount ID' },
},
required: ['discountId'],
},
handler: async (args: any) => {
return await client.get<CloverDiscount>(`/discounts/${args.discountId}`);
},
},
clover_create_discount: {
description: 'Create a new discount',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Discount name' },
amount: {
type: 'number',
description: 'Fixed discount amount in cents (use this OR percentage)',
},
percentage: {
type: 'number',
description: 'Percentage discount (0-100, use this OR amount)',
},
},
required: ['name'],
},
handler: async (args: any) => {
return await client.post<CloverDiscount>('/discounts', {
name: args.name,
amount: args.amount,
percentage: args.percentage,
});
},
},
clover_update_discount: {
description: 'Update an existing discount',
inputSchema: {
type: 'object',
properties: {
discountId: { type: 'string', description: 'Discount ID' },
name: { type: 'string', description: 'Discount name' },
amount: {
type: 'number',
description: 'Fixed discount amount in cents',
},
percentage: {
type: 'number',
description: 'Percentage discount (0-100)',
},
},
required: ['discountId'],
},
handler: async (args: any) => {
const { discountId, ...updateData } = args;
return await client.post<CloverDiscount>(`/discounts/${discountId}`, updateData);
},
},
clover_delete_discount: {
description: 'Delete a discount',
inputSchema: {
type: 'object',
properties: {
discountId: { type: 'string', description: 'Discount ID' },
},
required: ['discountId'],
},
handler: async (args: any) => {
await client.delete(`/discounts/${args.discountId}`);
return { success: true, discountId: args.discountId };
},
},
};
}

View File

@ -0,0 +1,206 @@
import { CloverClient } from '../clients/clover.js';
import { CloverEmployee, CloverShift } from '../types/index.js';
export function createEmployeesTools(client: CloverClient) {
return {
clover_list_employees: {
description: 'List employees',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression',
},
expand: {
type: 'string',
description: 'Comma-separated fields to expand (shifts, roles)',
},
limit: {
type: 'number',
description: 'Maximum number of employees to return',
},
},
},
handler: async (args: any) => {
const employees = await client.fetchPaginated<CloverEmployee>(
'/employees',
{ filter: args.filter, expand: args.expand },
args.limit
);
return { employees, count: employees.length };
},
},
clover_get_employee: {
description: 'Get a specific employee by ID',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
expand: {
type: 'string',
description: 'Comma-separated fields to expand',
},
},
required: ['employeeId'],
},
handler: async (args: any) => {
return await client.get<CloverEmployee>(`/employees/${args.employeeId}`, {
expand: args.expand,
});
},
},
clover_create_employee: {
description: 'Create a new employee',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Employee name' },
nickname: { type: 'string', description: 'Nickname' },
email: { type: 'string', description: 'Email address' },
pin: { type: 'string', description: 'PIN code (4-10 digits)' },
role: { type: 'string', description: 'Employee role' },
},
required: ['name', 'role'],
},
handler: async (args: any) => {
return await client.post<CloverEmployee>('/employees', {
name: args.name,
nickname: args.nickname,
email: args.email,
pin: args.pin,
role: args.role,
});
},
},
clover_update_employee: {
description: 'Update an existing employee',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
name: { type: 'string', description: 'Employee name' },
nickname: { type: 'string', description: 'Nickname' },
email: { type: 'string', description: 'Email address' },
pin: { type: 'string', description: 'PIN code' },
role: { type: 'string', description: 'Employee role' },
},
required: ['employeeId'],
},
handler: async (args: any) => {
const { employeeId, ...updateData } = args;
return await client.post<CloverEmployee>(`/employees/${employeeId}`, updateData);
},
},
clover_delete_employee: {
description: 'Delete an employee',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
},
required: ['employeeId'],
},
handler: async (args: any) => {
await client.delete(`/employees/${args.employeeId}`);
return { success: true, employeeId: args.employeeId };
},
},
clover_list_employee_roles: {
description: 'List all employee roles',
inputSchema: {
type: 'object',
properties: {},
},
handler: async () => {
return await client.get('/roles');
},
},
clover_list_employee_shifts: {
description: 'List shifts for an employee',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
filter: {
type: 'string',
description: 'Filter expression (e.g., "inTime>1234567890")',
},
},
required: ['employeeId'],
},
handler: async (args: any) => {
return await client.get(`/employees/${args.employeeId}/shifts`, {
filter: args.filter,
});
},
},
clover_create_shift: {
description: 'Create a shift for an employee',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
inTime: {
type: 'number',
description: 'Clock-in timestamp (Unix ms)',
},
outTime: {
type: 'number',
description: 'Clock-out timestamp (Unix ms, optional)',
},
},
required: ['employeeId', 'inTime'],
},
handler: async (args: any) => {
const { employeeId, ...shiftData } = args;
return await client.post<CloverShift>(
`/employees/${employeeId}/shifts`,
shiftData
);
},
},
clover_clock_in_employee: {
description: 'Clock in an employee (start a shift)',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
},
required: ['employeeId'],
},
handler: async (args: any) => {
return await client.post<CloverShift>(
`/employees/${args.employeeId}/shifts`,
{ inTime: Date.now() }
);
},
},
clover_clock_out_employee: {
description: 'Clock out an employee (end a shift)',
inputSchema: {
type: 'object',
properties: {
employeeId: { type: 'string', description: 'Employee ID' },
shiftId: { type: 'string', description: 'Shift ID' },
},
required: ['employeeId', 'shiftId'],
},
handler: async (args: any) => {
return await client.post(
`/employees/${args.employeeId}/shifts/${args.shiftId}`,
{ outTime: Date.now() }
);
},
},
};
}

View File

@ -0,0 +1,253 @@
import { CloverClient } from '../clients/clover.js';
import { CloverItem, CloverCategory, CloverModifierGroup, CloverModifier } from '../types/index.js';
export function createInventoryTools(client: CloverClient) {
return {
clover_list_items: {
description: 'List inventory items',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression (e.g., "hidden=false")',
},
expand: {
type: 'string',
description: 'Comma-separated fields to expand (categories, tags, modifierGroups)',
},
limit: {
type: 'number',
description: 'Maximum number of items to return',
},
},
},
handler: async (args: any) => {
const items = await client.fetchPaginated<CloverItem>(
'/items',
{ filter: args.filter, expand: args.expand },
args.limit
);
return { items, count: items.length };
},
},
clover_get_item: {
description: 'Get a specific inventory item by ID',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
expand: {
type: 'string',
description: 'Comma-separated fields to expand',
},
},
required: ['itemId'],
},
handler: async (args: any) => {
return await client.get<CloverItem>(`/items/${args.itemId}`, {
expand: args.expand,
});
},
},
clover_create_item: {
description: 'Create a new inventory item',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Item name' },
price: { type: 'number', description: 'Price in cents' },
priceType: {
type: 'string',
enum: ['FIXED', 'PER_UNIT', 'VARIABLE'],
description: 'Price type (default: FIXED)',
},
sku: { type: 'string', description: 'SKU code' },
code: { type: 'string', description: 'Item code' },
cost: { type: 'number', description: 'Cost in cents' },
isRevenue: {
type: 'boolean',
description: 'Is this a revenue item (default: true)',
},
stockCount: { type: 'number', description: 'Initial stock count' },
},
required: ['name', 'price'],
},
handler: async (args: any) => {
return await client.post<CloverItem>('/items', {
name: args.name,
price: args.price,
priceType: args.priceType || 'FIXED',
sku: args.sku,
code: args.code,
cost: args.cost,
isRevenue: args.isRevenue !== false,
stockCount: args.stockCount,
});
},
},
clover_update_item: {
description: 'Update an existing inventory item',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
name: { type: 'string', description: 'Item name' },
price: { type: 'number', description: 'Price in cents' },
sku: { type: 'string', description: 'SKU code' },
code: { type: 'string', description: 'Item code' },
cost: { type: 'number', description: 'Cost in cents' },
hidden: { type: 'boolean', description: 'Hide item from menus' },
},
required: ['itemId'],
},
handler: async (args: any) => {
const { itemId, ...updateData } = args;
return await client.post<CloverItem>(`/items/${itemId}`, updateData);
},
},
clover_delete_item: {
description: 'Delete an inventory item',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
},
required: ['itemId'],
},
handler: async (args: any) => {
await client.delete(`/items/${args.itemId}`);
return { success: true, itemId: args.itemId };
},
},
clover_list_categories: {
description: 'List inventory categories',
inputSchema: {
type: 'object',
properties: {
expand: {
type: 'string',
description: 'Comma-separated fields to expand (items)',
},
},
},
handler: async (args: any) => {
const categories = await client.fetchPaginated<CloverCategory>(
'/categories',
{ expand: args.expand }
);
return { categories, count: categories.length };
},
},
clover_create_category: {
description: 'Create a new category',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Category name' },
sortOrder: {
type: 'number',
description: 'Sort order for display',
},
},
required: ['name'],
},
handler: async (args: any) => {
return await client.post<CloverCategory>('/categories', {
name: args.name,
sortOrder: args.sortOrder,
});
},
},
clover_list_modifier_groups: {
description: 'List modifier groups',
inputSchema: {
type: 'object',
properties: {
expand: {
type: 'string',
description: 'Comma-separated fields to expand (modifiers)',
},
},
},
handler: async (args: any) => {
const groups = await client.fetchPaginated<CloverModifierGroup>(
'/modifier_groups',
{ expand: args.expand }
);
return { modifierGroups: groups, count: groups.length };
},
},
clover_create_modifier: {
description: 'Create a new modifier',
inputSchema: {
type: 'object',
properties: {
modifierGroupId: {
type: 'string',
description: 'Modifier group ID',
},
name: { type: 'string', description: 'Modifier name' },
price: {
type: 'number',
description: 'Additional price in cents (default: 0)',
},
},
required: ['modifierGroupId', 'name'],
},
handler: async (args: any) => {
return await client.post<CloverModifier>(
`/modifier_groups/${args.modifierGroupId}/modifiers`,
{
name: args.name,
price: args.price || 0,
}
);
},
},
clover_list_item_stocks: {
description: 'List stock levels for all items',
inputSchema: {
type: 'object',
properties: {
itemId: {
type: 'string',
description: 'Optional: filter by specific item ID',
},
},
},
handler: async (args: any) => {
const endpoint = args.itemId
? `/items/${args.itemId}/stock`
: '/item_stocks';
return await client.get(endpoint);
},
},
clover_update_item_stock: {
description: 'Update stock count for an item',
inputSchema: {
type: 'object',
properties: {
itemId: { type: 'string', description: 'Item ID' },
quantity: { type: 'number', description: 'New stock quantity' },
},
required: ['itemId', 'quantity'],
},
handler: async (args: any) => {
return await client.post(`/items/${args.itemId}/stock`, {
quantity: args.quantity,
});
},
},
};
}

View File

@ -0,0 +1,85 @@
import { CloverClient } from '../clients/clover.js';
import { CloverMerchant, CloverDevice, CloverTenderType } from '../types/index.js';
export function createMerchantsTools(client: CloverClient) {
return {
clover_get_merchant: {
description: 'Get merchant information',
inputSchema: {
type: 'object',
properties: {
expand: {
type: 'string',
description: 'Comma-separated fields to expand (address, owner)',
},
},
},
handler: async (args: any) => {
return await client.get<CloverMerchant>('', {
expand: args.expand,
});
},
},
clover_update_merchant: {
description: 'Update merchant settings',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Merchant name' },
phoneNumber: { type: 'string', description: 'Phone number' },
website: { type: 'string', description: 'Website URL' },
},
},
handler: async (args: any) => {
return await client.post<CloverMerchant>('', args);
},
},
clover_list_devices: {
description: 'List all devices for the merchant',
inputSchema: {
type: 'object',
properties: {
expand: {
type: 'string',
description: 'Comma-separated fields to expand',
},
},
},
handler: async (args: any) => {
const devices = await client.fetchPaginated<CloverDevice>(
'/devices',
{ expand: args.expand }
);
return { devices, count: devices.length };
},
},
clover_get_device: {
description: 'Get a specific device by ID',
inputSchema: {
type: 'object',
properties: {
deviceId: { type: 'string', description: 'Device ID' },
},
required: ['deviceId'],
},
handler: async (args: any) => {
return await client.get<CloverDevice>(`/devices/${args.deviceId}`);
},
},
clover_list_tender_types: {
description: 'List tender types (payment methods)',
inputSchema: {
type: 'object',
properties: {},
},
handler: async () => {
const tenders = await client.fetchPaginated<CloverTenderType>('/tenders');
return { tenders, count: tenders.length };
},
},
};
}

View File

@ -0,0 +1,208 @@
import { CloverClient } from '../clients/clover.js';
import { CloverOrder, CloverLineItem, PaginatedResponse } from '../types/index.js';
export function createOrdersTools(client: CloverClient) {
return {
clover_list_orders: {
description: 'List orders from Clover POS',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression (e.g., "state=open", "createdTime>1234567890")',
},
expand: {
type: 'string',
description: 'Comma-separated list of fields to expand (lineItems, customers, payments)',
},
limit: {
type: 'number',
description: 'Maximum number of orders to return',
},
},
},
handler: async (args: any) => {
const orders = await client.fetchPaginated<CloverOrder>(
'/orders',
{ filter: args.filter, expand: args.expand },
args.limit
);
return { orders, count: orders.length };
},
},
clover_get_order: {
description: 'Get a specific order by ID',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
expand: {
type: 'string',
description: 'Comma-separated list of fields to expand',
},
},
required: ['orderId'],
},
handler: async (args: any) => {
return await client.get<CloverOrder>(`/orders/${args.orderId}`, {
expand: args.expand,
});
},
},
clover_create_order: {
description: 'Create a new order',
inputSchema: {
type: 'object',
properties: {
state: {
type: 'string',
enum: ['open', 'locked'],
description: 'Order state (default: open)',
},
title: { type: 'string', description: 'Order title' },
note: { type: 'string', description: 'Order note' },
manualTransaction: {
type: 'boolean',
description: 'Manual transaction flag',
},
groupLineItems: { type: 'boolean', description: 'Group line items' },
},
},
handler: async (args: any) => {
return await client.post<CloverOrder>('/orders', {
state: args.state || 'open',
title: args.title,
note: args.note,
manualTransaction: args.manualTransaction,
groupLineItems: args.groupLineItems,
});
},
},
clover_update_order: {
description: 'Update an existing order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
title: { type: 'string', description: 'Order title' },
note: { type: 'string', description: 'Order note' },
state: { type: 'string', enum: ['open', 'locked'], description: 'Order state' },
},
required: ['orderId'],
},
handler: async (args: any) => {
const { orderId, ...updateData } = args;
return await client.post<CloverOrder>(`/orders/${orderId}`, updateData);
},
},
clover_delete_order: {
description: 'Delete an order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
},
required: ['orderId'],
},
handler: async (args: any) => {
await client.delete(`/orders/${args.orderId}`);
return { success: true, orderId: args.orderId };
},
},
clover_add_line_item: {
description: 'Add a line item to an order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
itemId: { type: 'string', description: 'Item ID from inventory' },
name: { type: 'string', description: 'Line item name (optional override)' },
price: { type: 'number', description: 'Price in cents (optional override)' },
unitQty: { type: 'number', description: 'Unit quantity (default: 1)' },
},
required: ['orderId', 'itemId'],
},
handler: async (args: any) => {
return await client.post<CloverLineItem>(
`/orders/${args.orderId}/line_items`,
{
item: { id: args.itemId },
name: args.name,
price: args.price,
unitQty: args.unitQty,
}
);
},
},
clover_remove_line_item: {
description: 'Remove a line item from an order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
lineItemId: { type: 'string', description: 'Line item ID' },
},
required: ['orderId', 'lineItemId'],
},
handler: async (args: any) => {
await client.delete(`/orders/${args.orderId}/line_items/${args.lineItemId}`);
return { success: true, lineItemId: args.lineItemId };
},
},
clover_add_order_discount: {
description: 'Add a discount to an order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
discountId: { type: 'string', description: 'Discount ID' },
},
required: ['orderId', 'discountId'],
},
handler: async (args: any) => {
return await client.post(
`/orders/${args.orderId}/discounts`,
{ discount: { id: args.discountId } }
);
},
},
clover_list_order_payments: {
description: 'List payments for an order',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
},
required: ['orderId'],
},
handler: async (args: any) => {
return await client.get<PaginatedResponse<any>>(
`/orders/${args.orderId}/payments`
);
},
},
clover_fire_order: {
description: 'Fire/send an order to the kitchen (mark as ready for preparation)',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
},
required: ['orderId'],
},
handler: async (args: any) => {
return await client.post(`/orders/${args.orderId}/fire`, {});
},
},
};
}

View File

@ -0,0 +1,101 @@
import { CloverClient } from '../clients/clover.js';
import { CloverPayment, CloverRefund } from '../types/index.js';
export function createPaymentsTools(client: CloverClient) {
return {
clover_list_payments: {
description: 'List payments',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression (e.g., "result=SUCCESS")',
},
expand: {
type: 'string',
description: 'Comma-separated fields to expand (cardTransaction, refunds)',
},
limit: {
type: 'number',
description: 'Maximum number of payments to return',
},
},
},
handler: async (args: any) => {
const payments = await client.fetchPaginated<CloverPayment>(
'/payments',
{ filter: args.filter, expand: args.expand },
args.limit
);
return { payments, count: payments.length };
},
},
clover_get_payment: {
description: 'Get a specific payment by ID',
inputSchema: {
type: 'object',
properties: {
paymentId: { type: 'string', description: 'Payment ID' },
expand: {
type: 'string',
description: 'Comma-separated fields to expand',
},
},
required: ['paymentId'],
},
handler: async (args: any) => {
return await client.get<CloverPayment>(`/payments/${args.paymentId}`, {
expand: args.expand,
});
},
},
clover_create_refund: {
description: 'Create a refund for a payment',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
paymentId: { type: 'string', description: 'Payment ID' },
amount: {
type: 'number',
description: 'Refund amount in cents',
},
fullRefund: {
type: 'boolean',
description: 'Full refund flag (default: false)',
},
},
required: ['orderId', 'paymentId', 'amount'],
},
handler: async (args: any) => {
return await client.post<CloverRefund>(
`/orders/${args.orderId}/payments/${args.paymentId}/refunds`,
{
amount: args.amount,
fullRefund: args.fullRefund,
}
);
},
},
clover_list_refunds: {
description: 'List refunds for a payment',
inputSchema: {
type: 'object',
properties: {
orderId: { type: 'string', description: 'Order ID' },
paymentId: { type: 'string', description: 'Payment ID' },
},
required: ['orderId', 'paymentId'],
},
handler: async (args: any) => {
return await client.get(
`/orders/${args.orderId}/payments/${args.paymentId}/refunds`
);
},
},
};
}

View File

@ -0,0 +1,256 @@
import { CloverClient } from '../clients/clover.js';
import {
CloverOrder,
CloverPayment,
SalesSummary,
RevenueByItem,
RevenueByCategory,
EmployeePerformance,
} from '../types/index.js';
export function createReportsTools(client: CloverClient) {
return {
clover_sales_summary: {
description: 'Get sales summary report for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
// Fetch orders and payments in date range
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
const payments = await client.fetchPaginated<CloverPayment>('/payments', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
});
const totalSales = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + p.amount, 0);
const totalRefunds = payments
.filter((p) => p.refunds && p.refunds.length > 0)
.reduce(
(sum, p) =>
sum + p.refunds!.reduce((rsum, r) => rsum + r.amount, 0),
0
);
const totalTax = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + (p.taxAmount || 0), 0);
const totalTips = payments
.filter((p) => p.result === 'SUCCESS')
.reduce((sum, p) => sum + (p.tipAmount || 0), 0);
const summary: SalesSummary = {
totalSales,
totalOrders: orders.length,
averageOrderValue: orders.length > 0 ? totalSales / orders.length : 0,
totalRefunds,
netSales: totalSales - totalRefunds,
totalTax,
totalTips,
};
return summary;
},
},
clover_revenue_by_item: {
description: 'Get revenue breakdown by item for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
const itemStats = new Map<
string,
{ name: string; quantity: number; revenue: number }
>();
orders.forEach((order) => {
order.lineItems?.forEach((lineItem) => {
const itemId = lineItem.item.id;
const existing = itemStats.get(itemId) || {
name: lineItem.name,
quantity: 0,
revenue: 0,
};
existing.quantity += lineItem.unitQty || 1;
existing.revenue += lineItem.price * (lineItem.unitQty || 1);
itemStats.set(itemId, existing);
});
});
const results: RevenueByItem[] = Array.from(itemStats.entries()).map(
([itemId, stats]) => ({
itemId,
itemName: stats.name,
quantitySold: stats.quantity,
totalRevenue: stats.revenue,
averagePrice: stats.revenue / stats.quantity,
})
);
return { items: results, count: results.length };
},
},
clover_revenue_by_category: {
description: 'Get revenue breakdown by category for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
// Fetch items with categories
const items = await client.fetchPaginated('/items', {
expand: 'categories',
});
const orders = await client.fetchPaginated<CloverOrder>('/orders', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'lineItems',
});
// Build item-to-category map
const itemCategories = new Map<string, any[]>();
items.forEach((item: any) => {
if (item.categories) {
itemCategories.set(item.id, item.categories);
}
});
const categoryStats = new Map<
string,
{ name: string; itemCount: Set<string>; revenue: number }
>();
orders.forEach((order) => {
order.lineItems?.forEach((lineItem) => {
const categories = itemCategories.get(lineItem.item.id) || [];
categories.forEach((cat: any) => {
const existing = categoryStats.get(cat.id) || {
name: cat.name,
itemCount: new Set<string>(),
revenue: 0,
};
existing.itemCount.add(lineItem.item.id);
existing.revenue += lineItem.price * (lineItem.unitQty || 1);
categoryStats.set(cat.id, existing);
});
});
});
const results: RevenueByCategory[] = Array.from(
categoryStats.entries()
).map(([categoryId, stats]) => ({
categoryId,
categoryName: stats.name,
itemCount: stats.itemCount.size,
totalRevenue: stats.revenue,
}));
return { categories: results, count: results.length };
},
},
clover_employee_performance: {
description: 'Get employee performance report for a date range',
inputSchema: {
type: 'object',
properties: {
startDate: {
type: 'number',
description: 'Start date (Unix timestamp in milliseconds)',
},
endDate: {
type: 'number',
description: 'End date (Unix timestamp in milliseconds)',
},
},
required: ['startDate', 'endDate'],
},
handler: async (args: any) => {
const payments = await client.fetchPaginated<CloverPayment>('/payments', {
filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`,
expand: 'employee',
});
const employeeStats = new Map<
string,
{ name: string; sales: number; orderCount: number }
>();
payments
.filter((p) => p.result === 'SUCCESS')
.forEach((payment) => {
const empId = payment.employee.id;
const existing = employeeStats.get(empId) || {
name: (payment.employee as any).name || 'Unknown',
sales: 0,
orderCount: 0,
};
existing.sales += payment.amount;
existing.orderCount += 1;
employeeStats.set(empId, existing);
});
const results: EmployeePerformance[] = Array.from(
employeeStats.entries()
).map(([employeeId, stats]) => ({
employeeId,
employeeName: stats.name,
totalSales: stats.sales,
orderCount: stats.orderCount,
averageOrderValue: stats.sales / stats.orderCount,
}));
return { employees: results, count: results.length };
},
},
};
}

View File

@ -0,0 +1,105 @@
import { CloverClient } from '../clients/clover.js';
import { CloverTaxRate } from '../types/index.js';
export function createTaxesTools(client: CloverClient) {
return {
clover_list_tax_rates: {
description: 'List all tax rates',
inputSchema: {
type: 'object',
properties: {
filter: {
type: 'string',
description: 'Filter expression',
},
},
},
handler: async (args: any) => {
const taxRates = await client.fetchPaginated<CloverTaxRate>(
'/tax_rates',
{ filter: args.filter }
);
return { taxRates, count: taxRates.length };
},
},
clover_get_tax_rate: {
description: 'Get a specific tax rate by ID',
inputSchema: {
type: 'object',
properties: {
taxRateId: { type: 'string', description: 'Tax rate ID' },
},
required: ['taxRateId'],
},
handler: async (args: any) => {
return await client.get<CloverTaxRate>(`/tax_rates/${args.taxRateId}`);
},
},
clover_create_tax_rate: {
description: 'Create a new tax rate',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Tax rate name' },
rate: {
type: 'number',
description: 'Tax rate (e.g., 8.5 for 8.5%)',
},
isDefault: {
type: 'boolean',
description: 'Set as default tax rate',
},
},
required: ['name', 'rate'],
},
handler: async (args: any) => {
return await client.post<CloverTaxRate>('/tax_rates', {
name: args.name,
rate: args.rate,
isDefault: args.isDefault,
});
},
},
clover_update_tax_rate: {
description: 'Update an existing tax rate',
inputSchema: {
type: 'object',
properties: {
taxRateId: { type: 'string', description: 'Tax rate ID' },
name: { type: 'string', description: 'Tax rate name' },
rate: {
type: 'number',
description: 'Tax rate (e.g., 8.5 for 8.5%)',
},
isDefault: {
type: 'boolean',
description: 'Set as default tax rate',
},
},
required: ['taxRateId'],
},
handler: async (args: any) => {
const { taxRateId, ...updateData } = args;
return await client.post<CloverTaxRate>(`/tax_rates/${taxRateId}`, updateData);
},
},
clover_delete_tax_rate: {
description: 'Delete a tax rate',
inputSchema: {
type: 'object',
properties: {
taxRateId: { type: 'string', description: 'Tax rate ID' },
},
required: ['taxRateId'],
},
handler: async (args: any) => {
await client.delete(`/tax_rates/${args.taxRateId}`);
return { success: true, taxRateId: args.taxRateId };
},
},
};
}

View File

@ -0,0 +1,302 @@
// Clover API Types
export interface CloverConfig {
apiKey?: string;
accessToken?: string;
merchantId: string;
environment?: 'sandbox' | 'production';
}
export interface CloverOrder {
id: string;
currency: string;
employee?: { id: string };
total: number;
title?: string;
note?: string;
state: 'open' | 'locked' | 'paid';
manualTransaction?: boolean;
groupLineItems?: boolean;
testMode?: boolean;
createdTime?: number;
clientCreatedTime?: number;
modifiedTime?: number;
deletedTime?: number;
lineItems?: CloverLineItem[];
customers?: CloverCustomer[];
}
export interface CloverLineItem {
id: string;
orderRef: { id: string };
item: { id: string };
name: string;
price: number;
unitQty?: number;
printed?: boolean;
exchanged?: boolean;
refunded?: boolean;
isRevenue?: boolean;
modifications?: CloverModification[];
discounts?: CloverDiscount[];
}
export interface CloverModification {
id: string;
name: string;
amount: number;
modifier: { id: string };
}
export interface CloverItem {
id: string;
hidden?: boolean;
name: string;
price: number;
priceType?: 'FIXED' | 'PER_UNIT' | 'VARIABLE';
defaultTaxRates?: boolean;
cost?: number;
isRevenue?: boolean;
stockCount?: number;
sku?: string;
code?: string;
modifiedTime?: number;
categories?: { id: string }[];
tags?: CloverTag[];
modifierGroups?: CloverModifierGroup[];
}
export interface CloverCategory {
id: string;
name: string;
sortOrder?: number;
deleted?: boolean;
modifiedTime?: number;
items?: { id: string }[];
}
export interface CloverModifierGroup {
id: string;
name: string;
showByDefault?: boolean;
modifiers?: CloverModifier[];
}
export interface CloverModifier {
id: string;
name: string;
price: number;
modifierGroup?: { id: string };
}
export interface CloverTag {
id: string;
name: string;
items?: { id: string }[];
}
export interface CloverCustomer {
id: string;
firstName: string;
lastName: string;
marketingAllowed?: boolean;
customerSince?: number;
orders?: CloverOrder[];
addresses?: CloverAddress[];
emailAddresses?: CloverEmailAddress[];
phoneNumbers?: CloverPhoneNumber[];
cards?: CloverCard[];
}
export interface CloverAddress {
id: string;
address1?: string;
address2?: string;
address3?: string;
city?: string;
state?: string;
zip?: string;
country?: string;
}
export interface CloverEmailAddress {
id: string;
emailAddress: string;
primaryEmail?: boolean;
}
export interface CloverPhoneNumber {
id: string;
phoneNumber: string;
}
export interface CloverCard {
id: string;
first6: string;
last4: string;
cardType: string;
expirationDate: string;
token: string;
}
export interface CloverEmployee {
id: string;
name: string;
nickname?: string;
customId?: string;
email?: string;
inviteSent?: boolean;
claimedTime?: number;
deletedTime?: number;
pin?: string;
role: string;
shifts?: CloverShift[];
}
export interface CloverShift {
id: string;
employee: { id: string };
cashTipsCollected?: number;
serverBanking?: boolean;
inTime?: number;
outTime?: number;
}
export interface CloverPayment {
id: string;
order: { id: string };
device?: { id: string };
amount: number;
tipAmount?: number;
taxAmount?: number;
cashbackAmount?: number;
cashTendered?: number;
externalPaymentId?: string;
employee: { id: string };
createdTime: number;
clientCreatedTime?: number;
modifiedTime?: number;
offline?: boolean;
result: 'SUCCESS' | 'FAIL' | 'INITIATED' | 'VOIDED' | 'VOIDING' | 'AUTH' | 'AUTH_COMPLETED';
cardTransaction?: CloverCardTransaction;
refunds?: CloverRefund[];
}
export interface CloverCardTransaction {
cardType: string;
entryType: string;
first6: string;
last4: string;
authCode?: string;
referenceId?: string;
transactionNo?: string;
state: 'PENDING' | 'CLOSED';
extra?: any;
}
export interface CloverRefund {
id: string;
orderRef: { id: string };
device?: { id: string };
amount: number;
taxAmount?: number;
tipAmount?: number;
employee: { id: string };
createdTime: number;
clientCreatedTime?: number;
payment: { id: string };
}
export interface CloverMerchant {
id: string;
name: string;
address?: CloverAddress;
phoneNumber?: string;
website?: string;
owner?: { id: string };
createdTime?: number;
modifiedTime?: number;
}
export interface CloverDevice {
id: string;
serial: string;
model: string;
merchant: { id: string };
pedType?: string;
terminalPrefix?: string;
productName?: string;
}
export interface CloverTenderType {
id: string;
editable?: boolean;
labelKey: string;
label: string;
opensCashDrawer?: boolean;
enabled?: boolean;
visible?: boolean;
}
export interface CloverDiscount {
id: string;
name: string;
amount?: number;
percentage?: number;
}
export interface CloverTaxRate {
id: string;
name: string;
rate: number;
isDefault?: boolean;
}
export interface CloverCashEvent {
id: string;
device: { id: string };
employee: { id: string };
timestamp: number;
amountChange: number;
type: 'ADD' | 'REMOVE' | 'OPEN_DRAWER';
note?: string;
}
export interface PaginatedResponse<T> {
elements: T[];
href?: string;
}
export interface SalesSummary {
totalSales: number;
totalOrders: number;
averageOrderValue: number;
totalRefunds: number;
netSales: number;
totalTax: number;
totalTips: number;
}
export interface RevenueByItem {
itemId: string;
itemName: string;
quantitySold: number;
totalRevenue: number;
averagePrice: number;
}
export interface RevenueByCategory {
categoryId: string;
categoryName: string;
itemCount: number;
totalRevenue: number;
}
export interface EmployeePerformance {
employeeId: string;
employeeName: string;
totalSales: number;
orderCount: number;
averageOrderValue: number;
totalHours?: number;
}

View File

@ -0,0 +1,116 @@
import React, { useState, useEffect } from 'react';
import { DollarSign, TrendingUp, TrendingDown, List } from 'lucide-react';
export default function CashDrawer() {
const [deviceId, setDeviceId] = useState('');
const [drawerInfo, setDrawerInfo] = useState<any>(null);
const [events, setEvents] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const fetchDrawerInfo = async () => {
if (!deviceId) return;
setLoading(true);
try {
const result = await window.mcp.callTool('clover_get_cash_drawer', { deviceId });
setDrawerInfo(result);
const eventsResult = await window.mcp.callTool('clover_list_cash_events', {
deviceId,
limit: 20,
});
setEvents(eventsResult.cashEvents || []);
} catch (error) {
alert('Failed to load cash drawer');
} finally {
setLoading(false);
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Cash Drawer</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex gap-4">
<input
type="text"
value={deviceId}
onChange={(e) => setDeviceId(e.target.value)}
placeholder="Enter Device ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg"
/>
<button
onClick={fetchDrawerInfo}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Loading...' : 'Load Drawer'}
</button>
</div>
</div>
{drawerInfo && (
<>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex items-center gap-4 mb-4">
<DollarSign className="w-12 h-12 text-green-600" />
<div>
<h2 className="text-2xl font-bold text-gray-900">
${(drawerInfo.currentBalance / 100).toFixed(2)}
</h2>
<p className="text-gray-600">Current Balance</p>
</div>
</div>
<p className="text-sm text-gray-500">
{drawerInfo.eventCount} total events
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4 flex items-center gap-2">
<List className="w-6 h-6" />
Recent Events
</h2>
{events.length > 0 ? (
<div className="space-y-3">
{events.map((event) => (
<div
key={event.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-3">
{event.amountChange > 0 ? (
<TrendingUp className="w-5 h-5 text-green-600" />
) : (
<TrendingDown className="w-5 h-5 text-red-600" />
)}
<div>
<p className="font-medium">{event.type}</p>
<p className="text-sm text-gray-600">
{new Date(event.timestamp).toLocaleString()}
</p>
{event.note && (
<p className="text-sm text-gray-500">{event.note}</p>
)}
</div>
</div>
<p
className={`text-lg font-semibold ${
event.amountChange > 0 ? 'text-green-600' : 'text-red-600'
}`}
>
{event.amountChange > 0 ? '+' : ''}
${(event.amountChange / 100).toFixed(2)}
</p>
</div>
))}
</div>
) : (
<p className="text-gray-500">No events found</p>
)}
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,88 @@
import React, { useState, useEffect } from 'react';
import { Folder, Plus, Edit, Trash2 } from 'lucide-react';
export default function CategoryManager() {
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchCategories();
}, []);
const fetchCategories = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_categories', {
expand: 'items',
});
setCategories(result.categories || []);
} catch (error) {
console.error('Failed to fetch categories:', error);
} finally {
setLoading(false);
}
};
const createCategory = async () => {
const name = prompt('Category name:');
if (!name) return;
try {
await window.mcp.callTool('clover_create_category', { name });
fetchCategories();
} catch (error) {
alert('Failed to create category');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Category Manager</h1>
<button
onClick={createCategory}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="w-5 h-5" />
New Category
</button>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">
Loading categories...
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{categories.map((category) => (
<div key={category.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<Folder className="w-8 h-8 text-blue-600" />
<div>
<h3 className="text-lg font-semibold text-gray-900">
{category.name}
</h3>
<p className="text-sm text-gray-600">
{category.items?.length || 0} items
</p>
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => alert('Edit feature coming soon')}
className="flex-1 px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 flex items-center justify-center gap-2"
>
<Edit className="w-4 h-4" />
Edit
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,153 @@
import React, { useState } from 'react';
import { User, Mail, Phone, MapPin, CreditCard } from 'lucide-react';
export default function CustomerDetail() {
const [customerId, setCustomerId] = useState('');
const [customer, setCustomer] = useState<any>(null);
const [loading, setLoading] = useState(false);
const fetchCustomer = async () => {
if (!customerId) return;
setLoading(true);
try {
const result = await window.mcp.callTool('clover_get_customer', {
customerId,
expand: 'addresses,emailAddresses,phoneNumbers,cards',
});
setCustomer(result);
} catch (error) {
alert('Failed to load customer');
} finally {
setLoading(false);
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Customer Details</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex gap-4">
<input
type="text"
value={customerId}
onChange={(e) => setCustomerId(e.target.value)}
placeholder="Enter Customer ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg"
/>
<button
onClick={fetchCustomer}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Loading...' : 'Load Customer'}
</button>
</div>
</div>
{customer && (
<>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex items-center gap-4 mb-6">
<div className="w-16 h-16 bg-blue-100 rounded-full flex items-center justify-center">
<User className="w-8 h-8 text-blue-600" />
</div>
<div>
<h2 className="text-2xl font-bold text-gray-900">
{customer.firstName} {customer.lastName}
</h2>
<p className="text-gray-600">Customer ID: {customer.id}</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<Mail className="w-5 h-5 text-blue-600" />
Email Addresses
</h3>
{customer.emailAddresses?.length > 0 ? (
<ul className="space-y-2">
{customer.emailAddresses.map((email: any) => (
<li key={email.id} className="text-gray-700">
{email.emailAddress}
{email.primaryEmail && (
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
Primary
</span>
)}
</li>
))}
</ul>
) : (
<p className="text-gray-500">No email addresses</p>
)}
</div>
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<Phone className="w-5 h-5 text-green-600" />
Phone Numbers
</h3>
{customer.phoneNumbers?.length > 0 ? (
<ul className="space-y-2">
{customer.phoneNumbers.map((phone: any) => (
<li key={phone.id} className="text-gray-700">
{phone.phoneNumber}
</li>
))}
</ul>
) : (
<p className="text-gray-500">No phone numbers</p>
)}
</div>
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<MapPin className="w-5 h-5 text-red-600" />
Addresses
</h3>
{customer.addresses?.length > 0 ? (
<ul className="space-y-2">
{customer.addresses.map((addr: any) => (
<li key={addr.id} className="text-gray-700">
{addr.address1}
{addr.address2 && `, ${addr.address2}`}
<br />
{addr.city}, {addr.state} {addr.zip}
</li>
))}
</ul>
) : (
<p className="text-gray-500">No addresses</p>
)}
</div>
<div>
<h3 className="text-lg font-semibold mb-3 flex items-center gap-2">
<CreditCard className="w-5 h-5 text-purple-600" />
Payment Cards
</h3>
{customer.cards?.length > 0 ? (
<ul className="space-y-2">
{customer.cards.map((card: any) => (
<li key={card.id} className="text-gray-700">
{card.cardType} {card.last4}
<br />
<span className="text-sm text-gray-500">
Exp: {card.expirationDate}
</span>
</li>
))}
</ul>
) : (
<p className="text-gray-500">No saved cards</p>
)}
</div>
</div>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,148 @@
import React, { useState, useEffect } from 'react';
import { Search, User, Plus, Eye } from 'lucide-react';
export default function CustomerGrid() {
const [customers, setCustomers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchCustomers();
}, []);
const fetchCustomers = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_customers', {
expand: 'emailAddresses,phoneNumbers',
limit: 100,
});
setCustomers(result.customers || []);
} catch (error) {
console.error('Failed to fetch customers:', error);
} finally {
setLoading(false);
}
};
const searchCustomers = async () => {
if (!searchTerm) {
fetchCustomers();
return;
}
setLoading(true);
try {
const result = await window.mcp.callTool('clover_search_customers', {
query: searchTerm,
});
setCustomers(result.customers || []);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
};
const createCustomer = async () => {
const firstName = prompt('First name:');
const lastName = prompt('Last name:');
if (!firstName || !lastName) return;
try {
await window.mcp.callTool('clover_create_customer', {
firstName,
lastName,
});
fetchCustomers();
} catch (error) {
alert('Failed to create customer');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Customers</h1>
<button
onClick={createCustomer}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="w-5 h-5" />
New Customer
</button>
</div>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && searchCustomers()}
placeholder="Search customers by name..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg"
/>
</div>
<button
onClick={searchCustomers}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Search
</button>
</div>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">
Loading customers...
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{customers.map((customer) => (
<div key={customer.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-blue-100 rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">
{customer.firstName} {customer.lastName}
</h3>
<p className="text-sm text-gray-600">
ID: {customer.id.slice(0, 8)}...
</p>
</div>
</div>
</div>
{customer.emailAddresses?.[0] && (
<p className="text-sm text-gray-600 mb-2">
📧 {customer.emailAddresses[0].emailAddress}
</p>
)}
{customer.phoneNumbers?.[0] && (
<p className="text-sm text-gray-600 mb-4">
📞 {customer.phoneNumbers[0].phoneNumber}
</p>
)}
<button
onClick={() =>
window.mcp.showApp('customer-detail', { customerId: customer.id })
}
className="w-full px-4 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 flex items-center justify-center gap-2"
>
<Eye className="w-4 h-4" />
View Details
</button>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,64 @@
import React, { useState, useEffect } from 'react';
import { Monitor, Smartphone, Tablet } from 'lucide-react';
export default function DeviceManager() {
const [devices, setDevices] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDevices();
}, []);
const fetchDevices = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_devices', {});
setDevices(result.devices || []);
} catch (error) {
console.error('Failed to fetch devices:', error);
} finally {
setLoading(false);
}
};
const getDeviceIcon = (model: string) => {
if (model.includes('Station')) return <Monitor className="w-8 h-8 text-blue-600" />;
if (model.includes('Mobile')) return <Smartphone className="w-8 h-8 text-green-600" />;
return <Tablet className="w-8 h-8 text-purple-600" />;
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Device Manager</h1>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading devices...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{devices.map((device) => (
<div key={device.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start gap-4 mb-4">
{getDeviceIcon(device.model)}
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900">
{device.productName || device.model}
</h3>
<p className="text-sm text-gray-600">Serial: {device.serial}</p>
<p className="text-sm text-gray-600">Model: {device.model}</p>
</div>
</div>
{device.terminalPrefix && (
<div className="mt-3 pt-3 border-t border-gray-200">
<p className="text-sm text-gray-600">
Terminal: {device.terminalPrefix}
</p>
</div>
)}
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { Tag, Plus, Edit, Trash2 } from 'lucide-react';
export default function DiscountManager() {
const [discounts, setDiscounts] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDiscounts();
}, []);
const fetchDiscounts = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_discounts', {});
setDiscounts(result.discounts || []);
} catch (error) {
console.error('Failed to fetch discounts:', error);
} finally {
setLoading(false);
}
};
const createDiscount = async () => {
const name = prompt('Discount name:');
if (!name) return;
const type = prompt('Type (amount/percentage):');
if (type === 'amount') {
const amount = prompt('Amount in cents:');
if (amount) {
await window.mcp.callTool('clover_create_discount', {
name,
amount: parseInt(amount),
});
fetchDiscounts();
}
} else if (type === 'percentage') {
const percentage = prompt('Percentage (0-100):');
if (percentage) {
await window.mcp.callTool('clover_create_discount', {
name,
percentage: parseFloat(percentage),
});
fetchDiscounts();
}
}
};
const deleteDiscount = async (discountId: string) => {
if (!confirm('Delete this discount?')) return;
try {
await window.mcp.callTool('clover_delete_discount', { discountId });
fetchDiscounts();
} catch (error) {
alert('Failed to delete discount');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Discount Manager</h1>
<button
onClick={createDiscount}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="w-5 h-5" />
New Discount
</button>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{discounts.map((discount) => (
<div key={discount.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<Tag className="w-8 h-8 text-green-600" />
<div>
<h3 className="text-lg font-semibold">{discount.name}</h3>
{discount.amount && (
<p className="text-xl font-bold text-green-600">
${(discount.amount / 100).toFixed(2)} off
</p>
)}
{discount.percentage && (
<p className="text-xl font-bold text-green-600">
{discount.percentage}% off
</p>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => alert('Edit coming soon')}
className="flex-1 px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 flex items-center justify-center gap-2"
>
<Edit className="w-4 h-4" />
Edit
</button>
<button
onClick={() => deleteDiscount(discount.id)}
className="px-3 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,184 @@
import React, { useState, useEffect } from 'react';
import { Users, UserCheck, Clock, DollarSign } from 'lucide-react';
export default function EmployeeDashboard() {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, []);
const fetchStats = async () => {
setLoading(true);
try {
const employeesResult = await window.mcp.callTool('clover_list_employees', {
limit: 100,
});
const employees = employeesResult.employees || [];
const totalEmployees = employees.length;
const activeEmployees = employees.filter(
(e: any) => !e.deletedTime
).length;
// Get performance for today
const startDate = new Date().setHours(0, 0, 0, 0);
const endDate = Date.now();
const perfResult = await window.mcp.callTool(
'clover_employee_performance',
{
startDate,
endDate,
}
);
setStats({
totalEmployees,
activeEmployees,
topPerformers: perfResult.employees?.slice(0, 3) || [],
});
} catch (error) {
console.error('Failed to fetch employee stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading dashboard...</div>
</div>
);
}
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">
Employee Dashboard
</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<StatCard
icon={<Users className="w-8 h-8 text-blue-600" />}
title="Total Employees"
value={stats?.totalEmployees || 0}
bgColor="bg-blue-50"
/>
<StatCard
icon={<UserCheck className="w-8 h-8 text-green-600" />}
title="Active Employees"
value={stats?.activeEmployees || 0}
bgColor="bg-green-50"
/>
<StatCard
icon={<Clock className="w-8 h-8 text-purple-600" />}
title="Clocked In"
value="N/A"
bgColor="bg-purple-50"
/>
</div>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Top Performers (Today)</h2>
{stats?.topPerformers.length > 0 ? (
<div className="space-y-4">
{stats.topPerformers.map((emp: any, idx: number) => (
<div
key={emp.employeeId}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div className="flex items-center gap-4">
<div className="w-10 h-10 bg-blue-100 rounded-full flex items-center justify-center font-bold text-blue-600">
#{idx + 1}
</div>
<div>
<p className="font-semibold">{emp.employeeName}</p>
<p className="text-sm text-gray-600">
{emp.orderCount} orders
</p>
</div>
</div>
<div className="text-right">
<p className="text-lg font-bold text-green-600">
${(emp.totalSales / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-600">
Avg: ${(emp.averageOrderValue / 100).toFixed(2)}
</p>
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500">No performance data for today</p>
)}
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={async () => {
const result = await window.mcp.callTool('clover_list_employees', {});
alert(`Found ${result.count} employees`);
}}
className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
View All
</button>
<button
onClick={() => window.mcp.showApp('employee-schedule')}
className="px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Schedule
</button>
<button
onClick={async () => {
const name = prompt('Employee name:');
const role = prompt('Role:');
if (name && role) {
await window.mcp.callTool('clover_create_employee', {
name,
role,
});
fetchStats();
}
}}
className="px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Add Employee
</button>
<button
onClick={() => alert('Roles feature coming soon')}
className="px-4 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
>
Manage Roles
</button>
</div>
</div>
</div>
);
}
function StatCard({
icon,
title,
value,
bgColor,
}: {
icon: React.ReactNode;
title: string;
value: string | number;
bgColor: string;
}) {
return (
<div className={`${bgColor} rounded-lg p-6 shadow-sm`}>
<div className="flex items-center justify-between mb-3">{icon}</div>
<h3 className="text-gray-600 text-sm font-medium mb-1">{title}</h3>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
);
}

View File

@ -0,0 +1,100 @@
import React, { useState, useEffect } from 'react';
import { Calendar, Clock, User } from 'lucide-react';
export default function EmployeeSchedule() {
const [shifts, setShifts] = useState<any[]>([]);
const [employees, setEmployees] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
setLoading(true);
try {
const empResult = await window.mcp.callTool('clover_list_employees', {});
setEmployees(empResult.employees || []);
// Get shifts for first employee if available
if (empResult.employees?.length > 0) {
const shiftResult = await window.mcp.callTool('clover_list_employee_shifts', {
employeeId: empResult.employees[0].id,
});
setShifts(shiftResult.elements || []);
}
} catch (error) {
console.error('Failed to fetch schedule:', error);
} finally {
setLoading(false);
}
};
const clockIn = async (employeeId: string) => {
try {
await window.mcp.callTool('clover_clock_in_employee', { employeeId });
alert('Clocked in successfully');
fetchData();
} catch (error) {
alert('Failed to clock in');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Employee Schedule</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-4">Active Employees</h2>
{loading ? (
<p className="text-gray-500">Loading...</p>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{employees.map((emp) => (
<div key={emp.id} className="p-4 bg-gray-50 rounded-lg">
<div className="flex items-center gap-3 mb-3">
<User className="w-6 h-6 text-blue-600" />
<div>
<p className="font-semibold">{emp.name}</p>
<p className="text-sm text-gray-600">{emp.role}</p>
</div>
</div>
<button
onClick={() => clockIn(emp.id)}
className="w-full px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
>
Clock In
</button>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Recent Shifts</h2>
{shifts.length > 0 ? (
<div className="space-y-3">
{shifts.map((shift) => (
<div key={shift.id} className="p-4 bg-gray-50 rounded-lg flex justify-between">
<div>
<p className="font-medium">Shift #{shift.id.slice(0, 8)}</p>
<p className="text-sm text-gray-600">
{new Date(shift.inTime).toLocaleString()}
</p>
</div>
{shift.outTime && (
<p className="text-sm text-gray-600">
Out: {new Date(shift.outTime).toLocaleString()}
</p>
)}
</div>
))}
</div>
) : (
<p className="text-gray-500">No shifts found</p>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,141 @@
import React, { useState, useEffect } from 'react';
import { Package, DollarSign, TrendingDown, Archive } from 'lucide-react';
export default function InventoryDashboard() {
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchStats();
}, []);
const fetchStats = async () => {
setLoading(true);
try {
const itemsResult = await window.mcp.callTool('clover_list_items', {
limit: 1000,
});
const items = itemsResult.items || [];
const totalItems = items.length;
const visibleItems = items.filter((i: any) => !i.hidden).length;
const lowStockItems = items.filter((i: any) => (i.stockCount || 0) < 10).length;
const totalValue = items.reduce(
(sum: number, i: any) => sum + (i.price || 0) * (i.stockCount || 0),
0
);
setStats({
totalItems,
visibleItems,
lowStockItems,
totalValue,
});
} catch (error) {
console.error('Failed to fetch inventory stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading inventory...</div>
</div>
);
}
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Inventory Dashboard</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
icon={<Package className="w-8 h-8 text-blue-600" />}
title="Total Items"
value={stats?.totalItems || 0}
bgColor="bg-blue-50"
/>
<StatCard
icon={<Archive className="w-8 h-8 text-green-600" />}
title="Visible Items"
value={stats?.visibleItems || 0}
bgColor="bg-green-50"
/>
<StatCard
icon={<TrendingDown className="w-8 h-8 text-red-600" />}
title="Low Stock"
value={stats?.lowStockItems || 0}
bgColor="bg-red-50"
/>
<StatCard
icon={<DollarSign className="w-8 h-8 text-purple-600" />}
title="Total Value"
value={`$${((stats?.totalValue || 0) / 100).toFixed(2)}`}
bgColor="bg-purple-50"
/>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={() => window.mcp.showApp('inventory-detail')}
className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
View Items
</button>
<button
onClick={() => window.mcp.showApp('category-manager')}
className="px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
Categories
</button>
<button
onClick={async () => {
const name = prompt('Item name:');
const price = prompt('Price (in cents):');
if (name && price) {
await window.mcp.callTool('clover_create_item', {
name,
price: parseInt(price),
});
fetchStats();
}
}}
className="px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Add Item
</button>
<button
onClick={() => window.mcp.callTool('clover_list_item_stocks', {})}
className="px-4 py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
>
Stock Report
</button>
</div>
</div>
</div>
);
}
function StatCard({
icon,
title,
value,
bgColor,
}: {
icon: React.ReactNode;
title: string;
value: string | number;
bgColor: string;
}) {
return (
<div className={`${bgColor} rounded-lg p-6 shadow-sm`}>
<div className="flex items-center justify-between mb-3">{icon}</div>
<h3 className="text-gray-600 text-sm font-medium mb-1">{title}</h3>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
);
}

View File

@ -0,0 +1,131 @@
import React, { useState, useEffect } from 'react';
import { Package, Edit, Trash2, Plus } from 'lucide-react';
export default function InventoryDetail() {
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchItems();
}, []);
const fetchItems = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_items', {
expand: 'categories',
limit: 100,
});
setItems(result.items || []);
} catch (error) {
console.error('Failed to fetch items:', error);
} finally {
setLoading(false);
}
};
const deleteItem = async (itemId: string) => {
if (!confirm('Delete this item?')) return;
try {
await window.mcp.callTool('clover_delete_item', { itemId });
fetchItems();
} catch (error) {
alert('Failed to delete item');
}
};
const filteredItems = items.filter((item) =>
item.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Inventory Items</h1>
<button
onClick={async () => {
const name = prompt('Item name:');
const price = prompt('Price (in cents):');
if (name && price) {
await window.mcp.callTool('clover_create_item', {
name,
price: parseInt(price),
});
fetchItems();
}
}}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="w-5 h-5" />
Add Item
</button>
</div>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search items..."
className="w-full px-4 py-2 border border-gray-300 rounded-lg"
/>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading items...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredItems.map((item) => (
<div key={item.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<h3 className="text-lg font-semibold text-gray-900 mb-1">
{item.name}
</h3>
<p className="text-2xl font-bold text-blue-600">
${(item.price / 100).toFixed(2)}
</p>
</div>
<Package className="w-8 h-8 text-gray-400" />
</div>
<div className="space-y-2 mb-4">
{item.sku && (
<p className="text-sm text-gray-600">SKU: {item.sku}</p>
)}
{item.stockCount !== undefined && (
<p className="text-sm text-gray-600">
Stock: {item.stockCount}
</p>
)}
{item.hidden && (
<span className="inline-block px-2 py-1 text-xs bg-red-100 text-red-800 rounded">
Hidden
</span>
)}
</div>
<div className="flex gap-2">
<button
onClick={() => alert('Edit feature coming soon')}
className="flex-1 px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 flex items-center justify-center gap-2"
>
<Edit className="w-4 h-4" />
Edit
</button>
<button
onClick={() => deleteItem(item.id)}
className="px-3 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,159 @@
import React, { useState, useEffect } from 'react';
import { ShoppingCart, DollarSign, Clock, TrendingUp } from 'lucide-react';
interface OrderStats {
totalOrders: number;
openOrders: number;
totalRevenue: number;
averageOrderValue: number;
}
export default function OrderDashboard() {
const [stats, setStats] = useState<OrderStats | null>(null);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('today');
useEffect(() => {
fetchStats();
}, [timeRange]);
const fetchStats = async () => {
setLoading(true);
try {
const endDate = Date.now();
let startDate = endDate;
if (timeRange === 'today') {
startDate = new Date().setHours(0, 0, 0, 0);
} else if (timeRange === 'week') {
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
} else if (timeRange === 'month') {
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
}
const summary = await window.mcp.callTool('clover_sales_summary', {
startDate,
endDate,
});
const openOrders = await window.mcp.callTool('clover_list_orders', {
filter: 'state=open',
});
setStats({
totalOrders: summary.totalOrders || 0,
openOrders: openOrders.count || 0,
totalRevenue: summary.totalSales || 0,
averageOrderValue: summary.averageOrderValue || 0,
});
} catch (error) {
console.error('Failed to fetch order stats:', error);
} finally {
setLoading(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="text-gray-500">Loading dashboard...</div>
</div>
);
}
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="mb-6 flex justify-between items-center">
<h1 className="text-3xl font-bold text-gray-900">Order Dashboard</h1>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
<StatCard
icon={<ShoppingCart className="w-8 h-8 text-blue-600" />}
title="Total Orders"
value={stats?.totalOrders || 0}
bgColor="bg-blue-50"
/>
<StatCard
icon={<Clock className="w-8 h-8 text-yellow-600" />}
title="Open Orders"
value={stats?.openOrders || 0}
bgColor="bg-yellow-50"
/>
<StatCard
icon={<DollarSign className="w-8 h-8 text-green-600" />}
title="Total Revenue"
value={`$${((stats?.totalRevenue || 0) / 100).toFixed(2)}`}
bgColor="bg-green-50"
/>
<StatCard
icon={<TrendingUp className="w-8 h-8 text-purple-600" />}
title="Avg Order Value"
value={`$${((stats?.averageOrderValue || 0) / 100).toFixed(2)}`}
bgColor="bg-purple-50"
/>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<button
onClick={() => window.mcp.showApp('order-grid')}
className="px-4 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
View All Orders
</button>
<button
onClick={() => window.mcp.callTool('clover_create_order', {})}
className="px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700"
>
New Order
</button>
<button
onClick={() => window.mcp.showApp('order-detail')}
className="px-4 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
>
Order Details
</button>
<button
onClick={() => window.mcp.showApp('payment-history')}
className="px-4 py-3 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"
>
Payment History
</button>
</div>
</div>
</div>
);
}
function StatCard({
icon,
title,
value,
bgColor,
}: {
icon: React.ReactNode;
title: string;
value: string | number;
bgColor: string;
}) {
return (
<div className={`${bgColor} rounded-lg p-6 shadow-sm`}>
<div className="flex items-center justify-between mb-3">
{icon}
</div>
<h3 className="text-gray-600 text-sm font-medium mb-1">{title}</h3>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
);
}

View File

@ -0,0 +1,181 @@
import React, { useState, useEffect } from 'react';
import { Package, User, Calendar, DollarSign, Trash2, Plus } from 'lucide-react';
export default function OrderDetail() {
const [orderId, setOrderId] = useState('');
const [order, setOrder] = useState<any>(null);
const [loading, setLoading] = useState(false);
const fetchOrder = async () => {
if (!orderId) return;
setLoading(true);
try {
const result = await window.mcp.callTool('clover_get_order', {
orderId,
expand: 'lineItems,customers,payments',
});
setOrder(result);
} catch (error) {
console.error('Failed to fetch order:', error);
alert('Failed to load order');
} finally {
setLoading(false);
}
};
const addLineItem = async () => {
const itemId = prompt('Enter item ID:');
if (!itemId) return;
try {
await window.mcp.callTool('clover_add_line_item', {
orderId: order.id,
itemId,
});
fetchOrder();
} catch (error) {
alert('Failed to add line item');
}
};
const removeLineItem = async (lineItemId: string) => {
if (!confirm('Remove this item?')) return;
try {
await window.mcp.callTool('clover_remove_line_item', {
orderId: order.id,
lineItemId,
});
fetchOrder();
} catch (error) {
alert('Failed to remove line item');
}
};
const fireOrder = async () => {
if (!confirm('Fire this order to kitchen?')) return;
try {
await window.mcp.callTool('clover_fire_order', { orderId: order.id });
alert('Order fired!');
} catch (error) {
alert('Failed to fire order');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Order Details</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex gap-4">
<input
type="text"
value={orderId}
onChange={(e) => setOrderId(e.target.value)}
placeholder="Enter Order ID"
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg"
/>
<button
onClick={fetchOrder}
disabled={loading}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400"
>
{loading ? 'Loading...' : 'Load Order'}
</button>
</div>
</div>
{order && (
<>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-6">
<InfoItem
icon={<Package className="w-5 h-5" />}
label="Order ID"
value={order.id}
/>
<InfoItem
icon={<DollarSign className="w-5 h-5" />}
label="Total"
value={`$${((order.total || 0) / 100).toFixed(2)}`}
/>
<InfoItem
icon={<Calendar className="w-5 h-5" />}
label="State"
value={order.state}
/>
<InfoItem
icon={<User className="w-5 h-5" />}
label="Currency"
value={order.currency}
/>
</div>
<div className="flex gap-4">
<button
onClick={addLineItem}
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center gap-2"
>
<Plus className="w-4 h-4" />
Add Item
</button>
<button
onClick={fireOrder}
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"
>
Fire Order
</button>
</div>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Line Items</h2>
{order.lineItems && order.lineItems.length > 0 ? (
<div className="space-y-3">
{order.lineItems.map((item: any) => (
<div
key={item.id}
className="flex items-center justify-between p-4 bg-gray-50 rounded-lg"
>
<div>
<p className="font-medium">{item.name}</p>
<p className="text-sm text-gray-600">
Qty: {item.unitQty || 1} × ${(item.price / 100).toFixed(2)}
</p>
</div>
<div className="flex items-center gap-4">
<span className="font-semibold">
${((item.price * (item.unitQty || 1)) / 100).toFixed(2)}
</span>
<button
onClick={() => removeLineItem(item.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</div>
))}
</div>
) : (
<p className="text-gray-500">No items in this order</p>
)}
</div>
</>
)}
</div>
);
}
function InfoItem({ icon, label, value }: any) {
return (
<div className="flex items-start gap-3">
<div className="text-blue-600 mt-1">{icon}</div>
<div>
<p className="text-sm text-gray-600">{label}</p>
<p className="font-semibold">{value}</p>
</div>
</div>
);
}

View File

@ -0,0 +1,150 @@
import React, { useState, useEffect } from 'react';
import { Search, Filter, Eye, Trash2 } from 'lucide-react';
export default function OrderGrid() {
const [orders, setOrders] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [stateFilter, setStateFilter] = useState('all');
useEffect(() => {
fetchOrders();
}, [stateFilter]);
const fetchOrders = async () => {
setLoading(true);
try {
const filter = stateFilter !== 'all' ? `state=${stateFilter}` : undefined;
const result = await window.mcp.callTool('clover_list_orders', {
filter,
expand: 'lineItems',
limit: 50,
});
setOrders(result.orders || []);
} catch (error) {
console.error('Failed to fetch orders:', error);
} finally {
setLoading(false);
}
};
const deleteOrder = async (orderId: string) => {
if (!confirm('Delete this order?')) return;
try {
await window.mcp.callTool('clover_delete_order', { orderId });
fetchOrders();
} catch (error) {
alert('Failed to delete order');
}
};
const filteredOrders = orders.filter((order) =>
order.id.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">All Orders</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 text-gray-400 w-5 h-5" />
<input
type="text"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search orders by ID..."
className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg"
/>
</div>
<select
value={stateFilter}
onChange={(e) => setStateFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="all">All States</option>
<option value="open">Open</option>
<option value="locked">Locked</option>
<option value="paid">Paid</option>
</select>
</div>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading orders...</div>
) : (
<div className="bg-white rounded-lg shadow overflow-hidden">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Order ID
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
State
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Items
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Total
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{filteredOrders.map((order) => (
<tr key={order.id} className="hover:bg-gray-50">
<td className="px-6 py-4 text-sm font-medium text-gray-900">
{order.id}
</td>
<td className="px-6 py-4">
<span
className={`px-2 py-1 text-xs rounded-full ${
order.state === 'open'
? 'bg-green-100 text-green-800'
: order.state === 'paid'
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{order.state}
</span>
</td>
<td className="px-6 py-4 text-sm text-gray-600">
{order.lineItems?.length || 0}
</td>
<td className="px-6 py-4 text-sm font-semibold text-gray-900">
${((order.total || 0) / 100).toFixed(2)}
</td>
<td className="px-6 py-4">
<div className="flex gap-2">
<button
onClick={() =>
window.mcp.showApp('order-detail', { orderId: order.id })
}
className="text-blue-600 hover:text-blue-700"
>
<Eye className="w-5 h-5" />
</button>
<button
onClick={() => deleteOrder(order.id)}
className="text-red-600 hover:text-red-700"
>
<Trash2 className="w-5 h-5" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}

View File

@ -0,0 +1,102 @@
import React, { useState, useEffect } from 'react';
import { CreditCard, DollarSign, Calendar, CheckCircle, XCircle } from 'lucide-react';
export default function PaymentHistory() {
const [payments, setPayments] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
useEffect(() => {
fetchPayments();
}, [filter]);
const fetchPayments = async () => {
setLoading(true);
try {
const filterExpr = filter !== 'all' ? `result=${filter}` : undefined;
const result = await window.mcp.callTool('clover_list_payments', {
filter: filterExpr,
expand: 'cardTransaction,refunds',
limit: 50,
});
setPayments(result.payments || []);
} catch (error) {
console.error('Failed to fetch payments:', error);
} finally {
setLoading(false);
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<h1 className="text-3xl font-bold text-gray-900 mb-6">Payment History</h1>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="all">All Payments</option>
<option value="SUCCESS">Successful</option>
<option value="FAIL">Failed</option>
<option value="VOIDED">Voided</option>
</select>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading payments...</div>
) : (
<div className="space-y-4">
{payments.map((payment) => (
<div key={payment.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4">
<div className={`p-3 rounded-full ${
payment.result === 'SUCCESS' ? 'bg-green-100' : 'bg-red-100'
}`}>
{payment.result === 'SUCCESS' ? (
<CheckCircle className="w-6 h-6 text-green-600" />
) : (
<XCircle className="w-6 h-6 text-red-600" />
)}
</div>
<div>
<p className="font-semibold text-lg">
${(payment.amount / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-600">
Payment ID: {payment.id}
</p>
{payment.cardTransaction && (
<p className="text-sm text-gray-600">
{payment.cardTransaction.cardType} {payment.cardTransaction.last4}
</p>
)}
<p className="text-sm text-gray-500">
{new Date(payment.createdTime).toLocaleString()}
</p>
</div>
</div>
<div className="text-right">
<span className={`px-3 py-1 rounded-full text-sm ${
payment.result === 'SUCCESS'
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{payment.result}
</span>
{payment.refunds && payment.refunds.length > 0 && (
<p className="text-sm text-red-600 mt-2">
{payment.refunds.length} refund(s)
</p>
)}
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,105 @@
import React, { useState, useEffect } from 'react';
import { Folder, BarChart } from 'lucide-react';
export default function RevenueByCategory() {
const [categories, setCategories] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('today');
useEffect(() => {
fetchRevenue();
}, [timeRange]);
const fetchRevenue = async () => {
setLoading(true);
try {
const endDate = Date.now();
let startDate = endDate;
if (timeRange === 'today') {
startDate = new Date().setHours(0, 0, 0, 0);
} else if (timeRange === 'week') {
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
} else if (timeRange === 'month') {
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
}
const result = await window.mcp.callTool('clover_revenue_by_category', {
startDate,
endDate,
});
setCategories(result.categories || []);
} catch (error) {
console.error('Failed to fetch revenue:', error);
} finally {
setLoading(false);
}
};
const sortedCategories = [...categories].sort((a, b) => b.totalRevenue - a.totalRevenue);
const totalRevenue = categories.reduce((sum, c) => sum + c.totalRevenue, 0);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Revenue by Category</h1>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading...</div>
) : (
<>
<div className="bg-white rounded-lg shadow p-6 mb-6">
<h2 className="text-xl font-semibold mb-2">Total Revenue</h2>
<p className="text-3xl font-bold text-green-600">
${(totalRevenue / 100).toFixed(2)}
</p>
</div>
<div className="space-y-4">
{sortedCategories.map((category, idx) => {
const percentage = totalRevenue > 0
? ((category.totalRevenue / totalRevenue) * 100).toFixed(1)
: 0;
return (
<div key={category.categoryId} className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<Folder className="w-6 h-6 text-blue-600" />
<div>
<h3 className="text-lg font-semibold">{category.categoryName}</h3>
<p className="text-sm text-gray-600">{category.itemCount} items</p>
</div>
</div>
<div className="text-right">
<p className="text-2xl font-bold text-green-600">
${(category.totalRevenue / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-600">{percentage}%</p>
</div>
</div>
<div className="w-full bg-gray-200 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full"
style={{ width: `${percentage}%` }}
/>
</div>
</div>
);
})}
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,96 @@
import React, { useState, useEffect } from 'react';
import { Package, TrendingUp } from 'lucide-react';
export default function RevenueByItem() {
const [items, setItems] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('today');
useEffect(() => {
fetchRevenue();
}, [timeRange]);
const fetchRevenue = async () => {
setLoading(true);
try {
const endDate = Date.now();
let startDate = endDate;
if (timeRange === 'today') {
startDate = new Date().setHours(0, 0, 0, 0);
} else if (timeRange === 'week') {
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
} else if (timeRange === 'month') {
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
}
const result = await window.mcp.callTool('clover_revenue_by_item', {
startDate,
endDate,
});
setItems(result.items || []);
} catch (error) {
console.error('Failed to fetch revenue:', error);
} finally {
setLoading(false);
}
};
const sortedItems = [...items].sort((a, b) => b.totalRevenue - a.totalRevenue);
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Revenue by Item</h1>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{sortedItems.map((item, idx) => (
<div key={item.itemId} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<span className="text-2xl font-bold text-blue-600">#{idx + 1}</span>
<Package className="w-6 h-6 text-gray-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900">{item.itemName}</h3>
</div>
</div>
<div className="space-y-2">
<div className="flex justify-between">
<span className="text-gray-600">Revenue:</span>
<span className="font-bold text-green-600">
${(item.totalRevenue / 100).toFixed(2)}
</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Quantity Sold:</span>
<span className="font-semibold">{item.quantitySold}</span>
</div>
<div className="flex justify-between">
<span className="text-gray-600">Avg Price:</span>
<span className="font-semibold">
${(item.averagePrice / 100).toFixed(2)}
</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -0,0 +1,120 @@
import React, { useState, useEffect } from 'react';
import { DollarSign, TrendingUp, ShoppingBag, RefreshCw } from 'lucide-react';
export default function SalesDashboard() {
const [summary, setSummary] = useState<any>(null);
const [loading, setLoading] = useState(true);
const [timeRange, setTimeRange] = useState('today');
useEffect(() => {
fetchSummary();
}, [timeRange]);
const fetchSummary = async () => {
setLoading(true);
try {
const endDate = Date.now();
let startDate = endDate;
if (timeRange === 'today') {
startDate = new Date().setHours(0, 0, 0, 0);
} else if (timeRange === 'week') {
startDate = endDate - 7 * 24 * 60 * 60 * 1000;
} else if (timeRange === 'month') {
startDate = endDate - 30 * 24 * 60 * 60 * 1000;
}
const result = await window.mcp.callTool('clover_sales_summary', {
startDate,
endDate,
});
setSummary(result);
} catch (error) {
console.error('Failed to fetch summary:', error);
} finally {
setLoading(false);
}
};
if (loading || !summary) {
return <div className="p-6">Loading...</div>;
}
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Sales Dashboard</h1>
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value)}
className="px-4 py-2 border border-gray-300 rounded-lg"
>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<StatCard
icon={<DollarSign className="w-8 h-8 text-green-600" />}
title="Total Sales"
value={`$${((summary.totalSales || 0) / 100).toFixed(2)}`}
bgColor="bg-green-50"
/>
<StatCard
icon={<ShoppingBag className="w-8 h-8 text-blue-600" />}
title="Total Orders"
value={summary.totalOrders || 0}
bgColor="bg-blue-50"
/>
<StatCard
icon={<TrendingUp className="w-8 h-8 text-purple-600" />}
title="Avg Order Value"
value={`$${((summary.averageOrderValue || 0) / 100).toFixed(2)}`}
bgColor="bg-purple-50"
/>
<StatCard
icon={<RefreshCw className="w-8 h-8 text-red-600" />}
title="Refunds"
value={`$${((summary.totalRefunds || 0) / 100).toFixed(2)}`}
bgColor="bg-red-50"
/>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Net Sales</h2>
<p className="text-3xl font-bold text-green-600">
${((summary.netSales || 0) / 100).toFixed(2)}
</p>
<p className="text-sm text-gray-600 mt-2">
After refunds: ${((summary.totalRefunds || 0) / 100).toFixed(2)}
</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h2 className="text-xl font-semibold mb-4">Tax & Tips</h2>
<div className="space-y-2">
<p className="text-lg">
Tax: <span className="font-semibold">${((summary.totalTax || 0) / 100).toFixed(2)}</span>
</p>
<p className="text-lg">
Tips: <span className="font-semibold">${((summary.totalTips || 0) / 100).toFixed(2)}</span>
</p>
</div>
</div>
</div>
</div>
);
}
function StatCard({ icon, title, value, bgColor }: any) {
return (
<div className={`${bgColor} rounded-lg p-6 shadow-sm`}>
<div className="flex items-center justify-between mb-3">{icon}</div>
<h3 className="text-gray-600 text-sm font-medium mb-1">{title}</h3>
<p className="text-2xl font-bold text-gray-900">{value}</p>
</div>
);
}

View File

@ -0,0 +1,108 @@
import React, { useState, useEffect } from 'react';
import { FileText, Plus, Edit, Trash2 } from 'lucide-react';
export default function TaxManager() {
const [taxRates, setTaxRates] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchTaxRates();
}, []);
const fetchTaxRates = async () => {
setLoading(true);
try {
const result = await window.mcp.callTool('clover_list_tax_rates', {});
setTaxRates(result.taxRates || []);
} catch (error) {
console.error('Failed to fetch tax rates:', error);
} finally {
setLoading(false);
}
};
const createTaxRate = async () => {
const name = prompt('Tax rate name:');
const rate = prompt('Tax rate (e.g., 8.5 for 8.5%):');
if (!name || !rate) return;
try {
await window.mcp.callTool('clover_create_tax_rate', {
name,
rate: parseFloat(rate),
});
fetchTaxRates();
} catch (error) {
alert('Failed to create tax rate');
}
};
const deleteTaxRate = async (taxRateId: string) => {
if (!confirm('Delete this tax rate?')) return;
try {
await window.mcp.callTool('clover_delete_tax_rate', { taxRateId });
fetchTaxRates();
} catch (error) {
alert('Failed to delete tax rate');
}
};
return (
<div className="p-6 bg-gray-50 min-h-screen">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold text-gray-900">Tax Manager</h1>
<button
onClick={createTaxRate}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<Plus className="w-5 h-5" />
New Tax Rate
</button>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Loading...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{taxRates.map((taxRate) => (
<div key={taxRate.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<FileText className="w-8 h-8 text-blue-600" />
<div>
<h3 className="text-lg font-semibold">{taxRate.name}</h3>
<p className="text-2xl font-bold text-blue-600">
{taxRate.rate}%
</p>
{taxRate.isDefault && (
<span className="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
Default
</span>
)}
</div>
</div>
</div>
<div className="flex gap-2">
<button
onClick={() => alert('Edit coming soon')}
className="flex-1 px-3 py-2 bg-blue-100 text-blue-700 rounded hover:bg-blue-200 flex items-center justify-center gap-2"
>
<Edit className="w-4 h-4" />
Edit
</button>
<button
onClick={() => deleteTaxRate(taxRate.id)}
className="px-3 py-2 bg-red-100 text-red-700 rounded hover:bg-red-200"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
))}
</div>
)}
</div>
);
}

View File

@ -1,14 +1,20 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]