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:
parent
60c457b8fb
commit
7ee40342c8
@ -1,95 +1,302 @@
|
|||||||
# Clover MCP Server
|
# 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
|
```bash
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
## Environment Variables
|
## Configuration
|
||||||
|
|
||||||
| Variable | Required | Description |
|
Set the following environment variables:
|
||||||
|----------|----------|-------------|
|
|
||||||
| `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"` |
|
|
||||||
|
|
||||||
## API Endpoints
|
```bash
|
||||||
|
# Required
|
||||||
|
export CLOVER_MERCHANT_ID="your_merchant_id"
|
||||||
|
|
||||||
- **Production US/Canada:** `https://api.clover.com`
|
# One of these is required
|
||||||
- **Production Europe:** `https://api.eu.clover.com`
|
export CLOVER_API_KEY="your_api_key"
|
||||||
- **Production LATAM:** `https://api.la.clover.com`
|
# OR
|
||||||
- **Sandbox:** `https://apisandbox.dev.clover.com`
|
export CLOVER_ACCESS_TOKEN="your_oauth_token"
|
||||||
|
|
||||||
## Tools
|
# Optional (default: sandbox)
|
||||||
|
export CLOVER_ENVIRONMENT="sandbox" # or "production"
|
||||||
|
```
|
||||||
|
|
||||||
### Orders
|
### Getting Clover Credentials
|
||||||
- **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)
|
|
||||||
|
|
||||||
### Inventory
|
1. **Sandbox Access**: Sign up at https://sandbox.dev.clover.com/
|
||||||
- **list_items** - List products/menu items available for sale
|
2. **API Key**: Go to Setup → API Tokens in your Clover dashboard
|
||||||
- **get_inventory** - Get stock counts for items
|
3. **OAuth Token**: Implement OAuth2 flow for production apps
|
||||||
|
|
||||||
### Customers & Payments
|
## Usage
|
||||||
- **list_customers** - List customer database entries
|
|
||||||
- **list_payments** - List payment transactions
|
|
||||||
|
|
||||||
### Merchant
|
### Running the Server
|
||||||
- **get_merchant** - Get merchant account information
|
|
||||||
|
|
||||||
## 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
|
```json
|
||||||
{
|
{
|
||||||
"mcpServers": {
|
"mcpServers": {
|
||||||
"clover": {
|
"clover": {
|
||||||
"command": "node",
|
"command": "node",
|
||||||
"args": ["/path/to/mcp-servers/clover/dist/index.js"],
|
"args": ["/path/to/clover/dist/main.js"],
|
||||||
"env": {
|
"env": {
|
||||||
"CLOVER_API_KEY": "your-api-token",
|
"CLOVER_MERCHANT_ID": "your_merchant_id",
|
||||||
"CLOVER_MERCHANT_ID": "your-merchant-id",
|
"CLOVER_API_KEY": "your_api_key",
|
||||||
"CLOVER_SANDBOX": "true"
|
"CLOVER_ENVIRONMENT": "sandbox"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Authentication
|
## API Reference
|
||||||
|
|
||||||
Clover uses OAuth 2.0. You need either:
|
### Tool Usage Examples
|
||||||
1. **Test API Token** - Generate in Clover Developer Dashboard for sandbox testing
|
|
||||||
2. **OAuth Access Token** - Obtained through OAuth flow for production apps
|
|
||||||
|
|
||||||
See [Clover Authentication Docs](https://docs.clover.com/dev/docs/use-oauth) for details.
|
#### List Orders
|
||||||
|
```typescript
|
||||||
## Examples
|
// List all open orders
|
||||||
|
await mcp.callTool('clover_list_orders', {
|
||||||
List open orders:
|
filter: 'state=open',
|
||||||
```
|
expand: 'lineItems,customers'
|
||||||
list_orders(filter: "state=open", limit: 10)
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
Get order with line items:
|
#### Create Order with Items
|
||||||
```
|
```typescript
|
||||||
get_order(order_id: "ABC123", expand: "lineItems,payments")
|
// 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",
|
### React App Resources
|
||||||
line_items: [
|
|
||||||
{ item_id: "ITEM123", quantity: 2 },
|
Apps are available as MCP resources:
|
||||||
{ name: "Custom Item", price: 999 }
|
|
||||||
]
|
```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
|
||||||
|
|||||||
@ -1,20 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "mcp-server-clover",
|
"name": "@mcpengine/clover-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
"description": "MCP server for Clover POS platform with comprehensive tools and React apps",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc",
|
"build": "tsc && node scripts/copy-assets.js",
|
||||||
"start": "node dist/index.js",
|
"dev": "tsc --watch",
|
||||||
"dev": "tsx src/index.ts"
|
"start": "node dist/main.js",
|
||||||
|
"prepare": "npm run build"
|
||||||
},
|
},
|
||||||
|
"keywords": [
|
||||||
|
"mcp",
|
||||||
|
"clover",
|
||||||
|
"pos",
|
||||||
|
"point-of-sale",
|
||||||
|
"payments",
|
||||||
|
"inventory",
|
||||||
|
"orders"
|
||||||
|
],
|
||||||
|
"author": "MCPEngine",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
"zod": "^3.22.4"
|
"axios": "^1.7.9",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"lucide-react": "^0.469.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^22.10.2",
|
||||||
"tsx": "^4.7.0",
|
"@types/react": "^18.3.18",
|
||||||
"typescript": "^5.3.0"
|
"@types/react-dom": "^18.3.5",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
servers/clover/scripts/copy-assets.js
Normal file
36
servers/clover/scripts/copy-assets.js
Normal 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);
|
||||||
|
}
|
||||||
105
servers/clover/src/clients/clover.ts
Normal file
105
servers/clover/src/clients/clover.ts
Normal 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
16
servers/clover/src/global.d.ts
vendored
Normal 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 {};
|
||||||
@ -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);
|
|
||||||
46
servers/clover/src/main.ts
Normal file
46
servers/clover/src/main.ts
Normal 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();
|
||||||
181
servers/clover/src/server.ts
Normal file
181
servers/clover/src/server.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
servers/clover/src/tools/cash-tools.ts
Normal file
78
servers/clover/src/tools/cash-tools.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
187
servers/clover/src/tools/customers-tools.ts
Normal file
187
servers/clover/src/tools/customers-tools.ts
Normal 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`);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
105
servers/clover/src/tools/discounts-tools.ts
Normal file
105
servers/clover/src/tools/discounts-tools.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
206
servers/clover/src/tools/employees-tools.ts
Normal file
206
servers/clover/src/tools/employees-tools.ts
Normal 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() }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
253
servers/clover/src/tools/inventory-tools.ts
Normal file
253
servers/clover/src/tools/inventory-tools.ts
Normal 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,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
85
servers/clover/src/tools/merchants-tools.ts
Normal file
85
servers/clover/src/tools/merchants-tools.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
208
servers/clover/src/tools/orders-tools.ts
Normal file
208
servers/clover/src/tools/orders-tools.ts
Normal 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`, {});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
101
servers/clover/src/tools/payments-tools.ts
Normal file
101
servers/clover/src/tools/payments-tools.ts
Normal 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`
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
256
servers/clover/src/tools/reports-tools.ts
Normal file
256
servers/clover/src/tools/reports-tools.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
105
servers/clover/src/tools/taxes-tools.ts
Normal file
105
servers/clover/src/tools/taxes-tools.ts
Normal 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 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
302
servers/clover/src/types/index.ts
Normal file
302
servers/clover/src/types/index.ts
Normal 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;
|
||||||
|
}
|
||||||
116
servers/clover/src/ui/react-app/cash-drawer.tsx
Normal file
116
servers/clover/src/ui/react-app/cash-drawer.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
88
servers/clover/src/ui/react-app/category-manager.tsx
Normal file
88
servers/clover/src/ui/react-app/category-manager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
153
servers/clover/src/ui/react-app/customer-detail.tsx
Normal file
153
servers/clover/src/ui/react-app/customer-detail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
servers/clover/src/ui/react-app/customer-grid.tsx
Normal file
148
servers/clover/src/ui/react-app/customer-grid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
64
servers/clover/src/ui/react-app/device-manager.tsx
Normal file
64
servers/clover/src/ui/react-app/device-manager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
servers/clover/src/ui/react-app/discount-manager.tsx
Normal file
120
servers/clover/src/ui/react-app/discount-manager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
184
servers/clover/src/ui/react-app/employee-dashboard.tsx
Normal file
184
servers/clover/src/ui/react-app/employee-dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
servers/clover/src/ui/react-app/employee-schedule.tsx
Normal file
100
servers/clover/src/ui/react-app/employee-schedule.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
141
servers/clover/src/ui/react-app/inventory-dashboard.tsx
Normal file
141
servers/clover/src/ui/react-app/inventory-dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
131
servers/clover/src/ui/react-app/inventory-detail.tsx
Normal file
131
servers/clover/src/ui/react-app/inventory-detail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
159
servers/clover/src/ui/react-app/order-dashboard.tsx
Normal file
159
servers/clover/src/ui/react-app/order-dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
181
servers/clover/src/ui/react-app/order-detail.tsx
Normal file
181
servers/clover/src/ui/react-app/order-detail.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
150
servers/clover/src/ui/react-app/order-grid.tsx
Normal file
150
servers/clover/src/ui/react-app/order-grid.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
102
servers/clover/src/ui/react-app/payment-history.tsx
Normal file
102
servers/clover/src/ui/react-app/payment-history.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
105
servers/clover/src/ui/react-app/revenue-by-category.tsx
Normal file
105
servers/clover/src/ui/react-app/revenue-by-category.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
96
servers/clover/src/ui/react-app/revenue-by-item.tsx
Normal file
96
servers/clover/src/ui/react-app/revenue-by-item.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
servers/clover/src/ui/react-app/sales-dashboard.tsx
Normal file
120
servers/clover/src/ui/react-app/sales-dashboard.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
servers/clover/src/ui/react-app/tax-manager.tsx
Normal file
108
servers/clover/src/ui/react-app/tax-manager.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,14 +1,20 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "NodeNext",
|
"module": "Node16",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Node16",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user