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