From 7ee40342c8c39a72c6fb6ca7c9f4f9972b9e2efa Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 17:42:59 -0500 Subject: [PATCH] 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 --- servers/clover/README.md | 317 +++++++++++++--- servers/clover/package.json | 37 +- servers/clover/scripts/copy-assets.js | 36 ++ servers/clover/src/clients/clover.ts | 105 ++++++ servers/clover/src/global.d.ts | 16 + servers/clover/src/index.ts | 349 ------------------ servers/clover/src/main.ts | 46 +++ servers/clover/src/server.ts | 181 +++++++++ servers/clover/src/tools/cash-tools.ts | 78 ++++ servers/clover/src/tools/customers-tools.ts | 187 ++++++++++ servers/clover/src/tools/discounts-tools.ts | 105 ++++++ servers/clover/src/tools/employees-tools.ts | 206 +++++++++++ servers/clover/src/tools/inventory-tools.ts | 253 +++++++++++++ servers/clover/src/tools/merchants-tools.ts | 85 +++++ servers/clover/src/tools/orders-tools.ts | 208 +++++++++++ servers/clover/src/tools/payments-tools.ts | 101 +++++ servers/clover/src/tools/reports-tools.ts | 256 +++++++++++++ servers/clover/src/tools/taxes-tools.ts | 105 ++++++ servers/clover/src/types/index.ts | 302 +++++++++++++++ .../clover/src/ui/react-app/cash-drawer.tsx | 116 ++++++ .../src/ui/react-app/category-manager.tsx | 88 +++++ .../src/ui/react-app/customer-detail.tsx | 153 ++++++++ .../clover/src/ui/react-app/customer-grid.tsx | 148 ++++++++ .../src/ui/react-app/device-manager.tsx | 64 ++++ .../src/ui/react-app/discount-manager.tsx | 120 ++++++ .../src/ui/react-app/employee-dashboard.tsx | 184 +++++++++ .../src/ui/react-app/employee-schedule.tsx | 100 +++++ .../src/ui/react-app/inventory-dashboard.tsx | 141 +++++++ .../src/ui/react-app/inventory-detail.tsx | 131 +++++++ .../src/ui/react-app/order-dashboard.tsx | 159 ++++++++ .../clover/src/ui/react-app/order-detail.tsx | 181 +++++++++ .../clover/src/ui/react-app/order-grid.tsx | 150 ++++++++ .../src/ui/react-app/payment-history.tsx | 102 +++++ .../src/ui/react-app/revenue-by-category.tsx | 105 ++++++ .../src/ui/react-app/revenue-by-item.tsx | 96 +++++ .../src/ui/react-app/sales-dashboard.tsx | 120 ++++++ .../clover/src/ui/react-app/tax-manager.tsx | 108 ++++++ servers/clover/tsconfig.json | 12 +- 38 files changed, 4834 insertions(+), 417 deletions(-) create mode 100644 servers/clover/scripts/copy-assets.js create mode 100644 servers/clover/src/clients/clover.ts create mode 100644 servers/clover/src/global.d.ts delete mode 100644 servers/clover/src/index.ts create mode 100644 servers/clover/src/main.ts create mode 100644 servers/clover/src/server.ts create mode 100644 servers/clover/src/tools/cash-tools.ts create mode 100644 servers/clover/src/tools/customers-tools.ts create mode 100644 servers/clover/src/tools/discounts-tools.ts create mode 100644 servers/clover/src/tools/employees-tools.ts create mode 100644 servers/clover/src/tools/inventory-tools.ts create mode 100644 servers/clover/src/tools/merchants-tools.ts create mode 100644 servers/clover/src/tools/orders-tools.ts create mode 100644 servers/clover/src/tools/payments-tools.ts create mode 100644 servers/clover/src/tools/reports-tools.ts create mode 100644 servers/clover/src/tools/taxes-tools.ts create mode 100644 servers/clover/src/types/index.ts create mode 100644 servers/clover/src/ui/react-app/cash-drawer.tsx create mode 100644 servers/clover/src/ui/react-app/category-manager.tsx create mode 100644 servers/clover/src/ui/react-app/customer-detail.tsx create mode 100644 servers/clover/src/ui/react-app/customer-grid.tsx create mode 100644 servers/clover/src/ui/react-app/device-manager.tsx create mode 100644 servers/clover/src/ui/react-app/discount-manager.tsx create mode 100644 servers/clover/src/ui/react-app/employee-dashboard.tsx create mode 100644 servers/clover/src/ui/react-app/employee-schedule.tsx create mode 100644 servers/clover/src/ui/react-app/inventory-dashboard.tsx create mode 100644 servers/clover/src/ui/react-app/inventory-detail.tsx create mode 100644 servers/clover/src/ui/react-app/order-dashboard.tsx create mode 100644 servers/clover/src/ui/react-app/order-detail.tsx create mode 100644 servers/clover/src/ui/react-app/order-grid.tsx create mode 100644 servers/clover/src/ui/react-app/payment-history.tsx create mode 100644 servers/clover/src/ui/react-app/revenue-by-category.tsx create mode 100644 servers/clover/src/ui/react-app/revenue-by-item.tsx create mode 100644 servers/clover/src/ui/react-app/sales-dashboard.tsx create mode 100644 servers/clover/src/ui/react-app/tax-manager.tsx diff --git a/servers/clover/README.md b/servers/clover/README.md index 789b6a1..35e5518 100644 --- a/servers/clover/README.md +++ b/servers/clover/README.md @@ -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 diff --git a/servers/clover/package.json b/servers/clover/package.json index f9e3571..51283a1 100644 --- a/servers/clover/package.json +++ b/servers/clover/package.json @@ -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" } } diff --git a/servers/clover/scripts/copy-assets.js b/servers/clover/scripts/copy-assets.js new file mode 100644 index 0000000..9b3c2f4 --- /dev/null +++ b/servers/clover/scripts/copy-assets.js @@ -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); +} diff --git a/servers/clover/src/clients/clover.ts b/servers/clover/src/clients/clover.ts new file mode 100644 index 0000000..0816bff --- /dev/null +++ b/servers/clover/src/clients/clover.ts @@ -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(endpoint: string, params: Record = {}): Promise { + const url = `/v3/merchants/${this.merchantId}${endpoint}`; + const response = await this.client.get(url, { params }); + return response.data; + } + + // Generic POST method + async post(endpoint: string, data: any): Promise { + const url = `/v3/merchants/${this.merchantId}${endpoint}`; + const response = await this.client.post(url, data); + return response.data; + } + + // Generic PUT method + async put(endpoint: string, data: any): Promise { + const url = `/v3/merchants/${this.merchantId}${endpoint}`; + const response = await this.client.put(url, data); + return response.data; + } + + // Generic DELETE method + async delete(endpoint: string): Promise { + const url = `/v3/merchants/${this.merchantId}${endpoint}`; + const response = await this.client.delete(url); + return response.data; + } + + // Paginated fetch helper + async fetchPaginated( + endpoint: string, + params: Record = {}, + limit?: number + ): Promise { + const allItems: T[] = []; + let offset = 0; + const pageSize = 100; + + while (true) { + const response = await this.get>(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; + } +} diff --git a/servers/clover/src/global.d.ts b/servers/clover/src/global.d.ts new file mode 100644 index 0000000..661553d --- /dev/null +++ b/servers/clover/src/global.d.ts @@ -0,0 +1,16 @@ +// Global type declarations for React MCP apps + +interface MCPClient { + callTool(name: string, args?: any): Promise; + showApp(name: string, params?: any): void; + listResources(): Promise; + readResource(uri: string): Promise; +} + +declare global { + interface Window { + mcp: MCPClient; + } +} + +export {}; diff --git a/servers/clover/src/index.ts b/servers/clover/src/index.ts deleted file mode 100644 index 95c6174..0000000 --- a/servers/clover/src/index.ts +++ /dev/null @@ -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); diff --git a/servers/clover/src/main.ts b/servers/clover/src/main.ts new file mode 100644 index 0000000..d04b3f4 --- /dev/null +++ b/servers/clover/src/main.ts @@ -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(); diff --git a/servers/clover/src/server.ts b/servers/clover/src/server.ts new file mode 100644 index 0000000..b959957 --- /dev/null +++ b/servers/clover/src/server.ts @@ -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; + private apps: Map; + + 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); + } +} diff --git a/servers/clover/src/tools/cash-tools.ts b/servers/clover/src/tools/cash-tools.ts new file mode 100644 index 0000000..9621cf6 --- /dev/null +++ b/servers/clover/src/tools/cash-tools.ts @@ -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( + '/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( + '/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, + }; + }, + }, + }; +} diff --git a/servers/clover/src/tools/customers-tools.ts b/servers/clover/src/tools/customers-tools.ts new file mode 100644 index 0000000..3eda4e9 --- /dev/null +++ b/servers/clover/src/tools/customers-tools.ts @@ -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( + '/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(`/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('/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(`/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( + '/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( + `/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`); + }, + }, + }; +} diff --git a/servers/clover/src/tools/discounts-tools.ts b/servers/clover/src/tools/discounts-tools.ts new file mode 100644 index 0000000..85cdf63 --- /dev/null +++ b/servers/clover/src/tools/discounts-tools.ts @@ -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( + '/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(`/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('/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(`/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 }; + }, + }, + }; +} diff --git a/servers/clover/src/tools/employees-tools.ts b/servers/clover/src/tools/employees-tools.ts new file mode 100644 index 0000000..dc82dbe --- /dev/null +++ b/servers/clover/src/tools/employees-tools.ts @@ -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( + '/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(`/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('/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(`/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( + `/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( + `/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() } + ); + }, + }, + }; +} diff --git a/servers/clover/src/tools/inventory-tools.ts b/servers/clover/src/tools/inventory-tools.ts new file mode 100644 index 0000000..6ee9ebe --- /dev/null +++ b/servers/clover/src/tools/inventory-tools.ts @@ -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( + '/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(`/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('/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(`/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( + '/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('/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( + '/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( + `/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, + }); + }, + }, + }; +} diff --git a/servers/clover/src/tools/merchants-tools.ts b/servers/clover/src/tools/merchants-tools.ts new file mode 100644 index 0000000..82a965c --- /dev/null +++ b/servers/clover/src/tools/merchants-tools.ts @@ -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('', { + 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('', 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( + '/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(`/devices/${args.deviceId}`); + }, + }, + + clover_list_tender_types: { + description: 'List tender types (payment methods)', + inputSchema: { + type: 'object', + properties: {}, + }, + handler: async () => { + const tenders = await client.fetchPaginated('/tenders'); + return { tenders, count: tenders.length }; + }, + }, + }; +} diff --git a/servers/clover/src/tools/orders-tools.ts b/servers/clover/src/tools/orders-tools.ts new file mode 100644 index 0000000..a9d5642 --- /dev/null +++ b/servers/clover/src/tools/orders-tools.ts @@ -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( + '/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(`/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('/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(`/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( + `/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>( + `/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`, {}); + }, + }, + }; +} diff --git a/servers/clover/src/tools/payments-tools.ts b/servers/clover/src/tools/payments-tools.ts new file mode 100644 index 0000000..0f2e14e --- /dev/null +++ b/servers/clover/src/tools/payments-tools.ts @@ -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( + '/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(`/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( + `/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` + ); + }, + }, + }; +} diff --git a/servers/clover/src/tools/reports-tools.ts b/servers/clover/src/tools/reports-tools.ts new file mode 100644 index 0000000..64bde3c --- /dev/null +++ b/servers/clover/src/tools/reports-tools.ts @@ -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('/orders', { + filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`, + expand: 'lineItems', + }); + + const payments = await client.fetchPaginated('/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('/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('/orders', { + filter: `createdTime>=${args.startDate} AND createdTime<=${args.endDate}`, + expand: 'lineItems', + }); + + // Build item-to-category map + const itemCategories = new Map(); + items.forEach((item: any) => { + if (item.categories) { + itemCategories.set(item.id, item.categories); + } + }); + + const categoryStats = new Map< + string, + { name: string; itemCount: Set; 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(), + 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('/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 }; + }, + }, + }; +} diff --git a/servers/clover/src/tools/taxes-tools.ts b/servers/clover/src/tools/taxes-tools.ts new file mode 100644 index 0000000..99e20a2 --- /dev/null +++ b/servers/clover/src/tools/taxes-tools.ts @@ -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( + '/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(`/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('/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(`/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 }; + }, + }, + }; +} diff --git a/servers/clover/src/types/index.ts b/servers/clover/src/types/index.ts new file mode 100644 index 0000000..6d68453 --- /dev/null +++ b/servers/clover/src/types/index.ts @@ -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 { + 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; +} diff --git a/servers/clover/src/ui/react-app/cash-drawer.tsx b/servers/clover/src/ui/react-app/cash-drawer.tsx new file mode 100644 index 0000000..45ccc7e --- /dev/null +++ b/servers/clover/src/ui/react-app/cash-drawer.tsx @@ -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(null); + const [events, setEvents] = useState([]); + 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 ( +
+

Cash Drawer

+ +
+
+ setDeviceId(e.target.value)} + placeholder="Enter Device ID" + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg" + /> + +
+
+ + {drawerInfo && ( + <> +
+
+ +
+

+ ${(drawerInfo.currentBalance / 100).toFixed(2)} +

+

Current Balance

+
+
+

+ {drawerInfo.eventCount} total events +

+
+ +
+

+ + Recent Events +

+ {events.length > 0 ? ( +
+ {events.map((event) => ( +
+
+ {event.amountChange > 0 ? ( + + ) : ( + + )} +
+

{event.type}

+

+ {new Date(event.timestamp).toLocaleString()} +

+ {event.note && ( +

{event.note}

+ )} +
+
+

0 ? 'text-green-600' : 'text-red-600' + }`} + > + {event.amountChange > 0 ? '+' : ''} + ${(event.amountChange / 100).toFixed(2)} +

+
+ ))} +
+ ) : ( +

No events found

+ )} +
+ + )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/category-manager.tsx b/servers/clover/src/ui/react-app/category-manager.tsx new file mode 100644 index 0000000..a9a2b98 --- /dev/null +++ b/servers/clover/src/ui/react-app/category-manager.tsx @@ -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([]); + 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 ( +
+
+

Category Manager

+ +
+ + {loading ? ( +
+ Loading categories... +
+ ) : ( +
+ {categories.map((category) => ( +
+
+
+ +
+

+ {category.name} +

+

+ {category.items?.length || 0} items +

+
+
+
+ +
+ +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/customer-detail.tsx b/servers/clover/src/ui/react-app/customer-detail.tsx new file mode 100644 index 0000000..29dce30 --- /dev/null +++ b/servers/clover/src/ui/react-app/customer-detail.tsx @@ -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(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 ( +
+

Customer Details

+ +
+
+ setCustomerId(e.target.value)} + placeholder="Enter Customer ID" + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg" + /> + +
+
+ + {customer && ( + <> +
+
+
+ +
+
+

+ {customer.firstName} {customer.lastName} +

+

Customer ID: {customer.id}

+
+
+ +
+
+

+ + Email Addresses +

+ {customer.emailAddresses?.length > 0 ? ( +
    + {customer.emailAddresses.map((email: any) => ( +
  • + {email.emailAddress} + {email.primaryEmail && ( + + Primary + + )} +
  • + ))} +
+ ) : ( +

No email addresses

+ )} +
+ +
+

+ + Phone Numbers +

+ {customer.phoneNumbers?.length > 0 ? ( +
    + {customer.phoneNumbers.map((phone: any) => ( +
  • + {phone.phoneNumber} +
  • + ))} +
+ ) : ( +

No phone numbers

+ )} +
+ +
+

+ + Addresses +

+ {customer.addresses?.length > 0 ? ( +
    + {customer.addresses.map((addr: any) => ( +
  • + {addr.address1} + {addr.address2 && `, ${addr.address2}`} +
    + {addr.city}, {addr.state} {addr.zip} +
  • + ))} +
+ ) : ( +

No addresses

+ )} +
+ +
+

+ + Payment Cards +

+ {customer.cards?.length > 0 ? ( +
    + {customer.cards.map((card: any) => ( +
  • + {card.cardType} •••• {card.last4} +
    + + Exp: {card.expirationDate} + +
  • + ))} +
+ ) : ( +

No saved cards

+ )} +
+
+
+ + )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/customer-grid.tsx b/servers/clover/src/ui/react-app/customer-grid.tsx new file mode 100644 index 0000000..637ff10 --- /dev/null +++ b/servers/clover/src/ui/react-app/customer-grid.tsx @@ -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([]); + 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 ( +
+
+

Customers

+ +
+ +
+
+
+ + 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" + /> +
+ +
+
+ + {loading ? ( +
+ Loading customers... +
+ ) : ( +
+ {customers.map((customer) => ( +
+
+
+
+ +
+
+

+ {customer.firstName} {customer.lastName} +

+

+ ID: {customer.id.slice(0, 8)}... +

+
+
+
+ + {customer.emailAddresses?.[0] && ( +

+ 📧 {customer.emailAddresses[0].emailAddress} +

+ )} + {customer.phoneNumbers?.[0] && ( +

+ 📞 {customer.phoneNumbers[0].phoneNumber} +

+ )} + + +
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/device-manager.tsx b/servers/clover/src/ui/react-app/device-manager.tsx new file mode 100644 index 0000000..937f68f --- /dev/null +++ b/servers/clover/src/ui/react-app/device-manager.tsx @@ -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([]); + 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 ; + if (model.includes('Mobile')) return ; + return ; + }; + + return ( +
+

Device Manager

+ + {loading ? ( +
Loading devices...
+ ) : ( +
+ {devices.map((device) => ( +
+
+ {getDeviceIcon(device.model)} +
+

+ {device.productName || device.model} +

+

Serial: {device.serial}

+

Model: {device.model}

+
+
+ + {device.terminalPrefix && ( +
+

+ Terminal: {device.terminalPrefix} +

+
+ )} +
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/discount-manager.tsx b/servers/clover/src/ui/react-app/discount-manager.tsx new file mode 100644 index 0000000..3a3ab41 --- /dev/null +++ b/servers/clover/src/ui/react-app/discount-manager.tsx @@ -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([]); + 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 ( +
+
+

Discount Manager

+ +
+ + {loading ? ( +
Loading...
+ ) : ( +
+ {discounts.map((discount) => ( +
+
+
+ +
+

{discount.name}

+ {discount.amount && ( +

+ ${(discount.amount / 100).toFixed(2)} off +

+ )} + {discount.percentage && ( +

+ {discount.percentage}% off +

+ )} +
+
+
+ +
+ + +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/employee-dashboard.tsx b/servers/clover/src/ui/react-app/employee-dashboard.tsx new file mode 100644 index 0000000..8a4f4a6 --- /dev/null +++ b/servers/clover/src/ui/react-app/employee-dashboard.tsx @@ -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(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 ( +
+
Loading dashboard...
+
+ ); + } + + return ( +
+

+ Employee Dashboard +

+ +
+ } + title="Total Employees" + value={stats?.totalEmployees || 0} + bgColor="bg-blue-50" + /> + } + title="Active Employees" + value={stats?.activeEmployees || 0} + bgColor="bg-green-50" + /> + } + title="Clocked In" + value="N/A" + bgColor="bg-purple-50" + /> +
+ +
+

Top Performers (Today)

+ {stats?.topPerformers.length > 0 ? ( +
+ {stats.topPerformers.map((emp: any, idx: number) => ( +
+
+
+ #{idx + 1} +
+
+

{emp.employeeName}

+

+ {emp.orderCount} orders +

+
+
+
+

+ ${(emp.totalSales / 100).toFixed(2)} +

+

+ Avg: ${(emp.averageOrderValue / 100).toFixed(2)} +

+
+
+ ))} +
+ ) : ( +

No performance data for today

+ )} +
+ +
+

Quick Actions

+
+ + + + +
+
+
+ ); +} + +function StatCard({ + icon, + title, + value, + bgColor, +}: { + icon: React.ReactNode; + title: string; + value: string | number; + bgColor: string; +}) { + return ( +
+
{icon}
+

{title}

+

{value}

+
+ ); +} diff --git a/servers/clover/src/ui/react-app/employee-schedule.tsx b/servers/clover/src/ui/react-app/employee-schedule.tsx new file mode 100644 index 0000000..1697225 --- /dev/null +++ b/servers/clover/src/ui/react-app/employee-schedule.tsx @@ -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([]); + const [employees, setEmployees] = useState([]); + 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 ( +
+

Employee Schedule

+ +
+

Active Employees

+ {loading ? ( +

Loading...

+ ) : ( +
+ {employees.map((emp) => ( +
+
+ +
+

{emp.name}

+

{emp.role}

+
+
+ +
+ ))} +
+ )} +
+ +
+

Recent Shifts

+ {shifts.length > 0 ? ( +
+ {shifts.map((shift) => ( +
+
+

Shift #{shift.id.slice(0, 8)}

+

+ {new Date(shift.inTime).toLocaleString()} +

+
+ {shift.outTime && ( +

+ Out: {new Date(shift.outTime).toLocaleString()} +

+ )} +
+ ))} +
+ ) : ( +

No shifts found

+ )} +
+
+ ); +} diff --git a/servers/clover/src/ui/react-app/inventory-dashboard.tsx b/servers/clover/src/ui/react-app/inventory-dashboard.tsx new file mode 100644 index 0000000..d3110d3 --- /dev/null +++ b/servers/clover/src/ui/react-app/inventory-dashboard.tsx @@ -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(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 ( +
+
Loading inventory...
+
+ ); + } + + return ( +
+

Inventory Dashboard

+ +
+ } + title="Total Items" + value={stats?.totalItems || 0} + bgColor="bg-blue-50" + /> + } + title="Visible Items" + value={stats?.visibleItems || 0} + bgColor="bg-green-50" + /> + } + title="Low Stock" + value={stats?.lowStockItems || 0} + bgColor="bg-red-50" + /> + } + title="Total Value" + value={`$${((stats?.totalValue || 0) / 100).toFixed(2)}`} + bgColor="bg-purple-50" + /> +
+ +
+

Quick Actions

+
+ + + + +
+
+
+ ); +} + +function StatCard({ + icon, + title, + value, + bgColor, +}: { + icon: React.ReactNode; + title: string; + value: string | number; + bgColor: string; +}) { + return ( +
+
{icon}
+

{title}

+

{value}

+
+ ); +} diff --git a/servers/clover/src/ui/react-app/inventory-detail.tsx b/servers/clover/src/ui/react-app/inventory-detail.tsx new file mode 100644 index 0000000..ffba073 --- /dev/null +++ b/servers/clover/src/ui/react-app/inventory-detail.tsx @@ -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([]); + 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 ( +
+
+

Inventory Items

+ +
+ +
+ setSearchTerm(e.target.value)} + placeholder="Search items..." + className="w-full px-4 py-2 border border-gray-300 rounded-lg" + /> +
+ + {loading ? ( +
Loading items...
+ ) : ( +
+ {filteredItems.map((item) => ( +
+
+
+

+ {item.name} +

+

+ ${(item.price / 100).toFixed(2)} +

+
+ +
+ +
+ {item.sku && ( +

SKU: {item.sku}

+ )} + {item.stockCount !== undefined && ( +

+ Stock: {item.stockCount} +

+ )} + {item.hidden && ( + + Hidden + + )} +
+ +
+ + +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/order-dashboard.tsx b/servers/clover/src/ui/react-app/order-dashboard.tsx new file mode 100644 index 0000000..2c5f05b --- /dev/null +++ b/servers/clover/src/ui/react-app/order-dashboard.tsx @@ -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(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 ( +
+
Loading dashboard...
+
+ ); + } + + return ( +
+
+

Order Dashboard

+ +
+ +
+ } + title="Total Orders" + value={stats?.totalOrders || 0} + bgColor="bg-blue-50" + /> + } + title="Open Orders" + value={stats?.openOrders || 0} + bgColor="bg-yellow-50" + /> + } + title="Total Revenue" + value={`$${((stats?.totalRevenue || 0) / 100).toFixed(2)}`} + bgColor="bg-green-50" + /> + } + title="Avg Order Value" + value={`$${((stats?.averageOrderValue || 0) / 100).toFixed(2)}`} + bgColor="bg-purple-50" + /> +
+ +
+

Quick Actions

+
+ + + + +
+
+
+ ); +} + +function StatCard({ + icon, + title, + value, + bgColor, +}: { + icon: React.ReactNode; + title: string; + value: string | number; + bgColor: string; +}) { + return ( +
+
+ {icon} +
+

{title}

+

{value}

+
+ ); +} diff --git a/servers/clover/src/ui/react-app/order-detail.tsx b/servers/clover/src/ui/react-app/order-detail.tsx new file mode 100644 index 0000000..ebd8c2c --- /dev/null +++ b/servers/clover/src/ui/react-app/order-detail.tsx @@ -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(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 ( +
+

Order Details

+ +
+
+ setOrderId(e.target.value)} + placeholder="Enter Order ID" + className="flex-1 px-4 py-2 border border-gray-300 rounded-lg" + /> + +
+
+ + {order && ( + <> +
+
+ } + label="Order ID" + value={order.id} + /> + } + label="Total" + value={`$${((order.total || 0) / 100).toFixed(2)}`} + /> + } + label="State" + value={order.state} + /> + } + label="Currency" + value={order.currency} + /> +
+ +
+ + +
+
+ +
+

Line Items

+ {order.lineItems && order.lineItems.length > 0 ? ( +
+ {order.lineItems.map((item: any) => ( +
+
+

{item.name}

+

+ Qty: {item.unitQty || 1} × ${(item.price / 100).toFixed(2)} +

+
+
+ + ${((item.price * (item.unitQty || 1)) / 100).toFixed(2)} + + +
+
+ ))} +
+ ) : ( +

No items in this order

+ )} +
+ + )} +
+ ); +} + +function InfoItem({ icon, label, value }: any) { + return ( +
+
{icon}
+
+

{label}

+

{value}

+
+
+ ); +} diff --git a/servers/clover/src/ui/react-app/order-grid.tsx b/servers/clover/src/ui/react-app/order-grid.tsx new file mode 100644 index 0000000..2fa615f --- /dev/null +++ b/servers/clover/src/ui/react-app/order-grid.tsx @@ -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([]); + 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 ( +
+

All Orders

+ +
+
+
+ + setSearchTerm(e.target.value)} + placeholder="Search orders by ID..." + className="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg" + /> +
+ +
+
+ + {loading ? ( +
Loading orders...
+ ) : ( +
+ + + + + + + + + + + + {filteredOrders.map((order) => ( + + + + + + + + ))} + +
+ Order ID + + State + + Items + + Total + + Actions +
+ {order.id} + + + {order.state} + + + {order.lineItems?.length || 0} + + ${((order.total || 0) / 100).toFixed(2)} + +
+ + +
+
+
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/payment-history.tsx b/servers/clover/src/ui/react-app/payment-history.tsx new file mode 100644 index 0000000..b727d02 --- /dev/null +++ b/servers/clover/src/ui/react-app/payment-history.tsx @@ -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([]); + 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 ( +
+

Payment History

+ +
+ +
+ + {loading ? ( +
Loading payments...
+ ) : ( +
+ {payments.map((payment) => ( +
+
+
+
+ {payment.result === 'SUCCESS' ? ( + + ) : ( + + )} +
+
+

+ ${(payment.amount / 100).toFixed(2)} +

+

+ Payment ID: {payment.id} +

+ {payment.cardTransaction && ( +

+ {payment.cardTransaction.cardType} •••• {payment.cardTransaction.last4} +

+ )} +

+ {new Date(payment.createdTime).toLocaleString()} +

+
+
+
+ + {payment.result} + + {payment.refunds && payment.refunds.length > 0 && ( +

+ {payment.refunds.length} refund(s) +

+ )} +
+
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/revenue-by-category.tsx b/servers/clover/src/ui/react-app/revenue-by-category.tsx new file mode 100644 index 0000000..ebaca85 --- /dev/null +++ b/servers/clover/src/ui/react-app/revenue-by-category.tsx @@ -0,0 +1,105 @@ +import React, { useState, useEffect } from 'react'; +import { Folder, BarChart } from 'lucide-react'; + +export default function RevenueByCategory() { + const [categories, setCategories] = useState([]); + 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 ( +
+
+

Revenue by Category

+ +
+ + {loading ? ( +
Loading...
+ ) : ( + <> +
+

Total Revenue

+

+ ${(totalRevenue / 100).toFixed(2)} +

+
+ +
+ {sortedCategories.map((category, idx) => { + const percentage = totalRevenue > 0 + ? ((category.totalRevenue / totalRevenue) * 100).toFixed(1) + : 0; + + return ( +
+
+
+ +
+

{category.categoryName}

+

{category.itemCount} items

+
+
+
+

+ ${(category.totalRevenue / 100).toFixed(2)} +

+

{percentage}%

+
+
+
+
+
+
+ ); + })} +
+ + )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/revenue-by-item.tsx b/servers/clover/src/ui/react-app/revenue-by-item.tsx new file mode 100644 index 0000000..a2b231c --- /dev/null +++ b/servers/clover/src/ui/react-app/revenue-by-item.tsx @@ -0,0 +1,96 @@ +import React, { useState, useEffect } from 'react'; +import { Package, TrendingUp } from 'lucide-react'; + +export default function RevenueByItem() { + const [items, setItems] = useState([]); + 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 ( +
+
+

Revenue by Item

+ +
+ + {loading ? ( +
Loading...
+ ) : ( +
+ {sortedItems.map((item, idx) => ( +
+
+
+
+ #{idx + 1} + +
+

{item.itemName}

+
+
+ +
+
+ Revenue: + + ${(item.totalRevenue / 100).toFixed(2)} + +
+
+ Quantity Sold: + {item.quantitySold} +
+
+ Avg Price: + + ${(item.averagePrice / 100).toFixed(2)} + +
+
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/src/ui/react-app/sales-dashboard.tsx b/servers/clover/src/ui/react-app/sales-dashboard.tsx new file mode 100644 index 0000000..3305439 --- /dev/null +++ b/servers/clover/src/ui/react-app/sales-dashboard.tsx @@ -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(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
Loading...
; + } + + return ( +
+
+

Sales Dashboard

+ +
+ +
+ } + title="Total Sales" + value={`$${((summary.totalSales || 0) / 100).toFixed(2)}`} + bgColor="bg-green-50" + /> + } + title="Total Orders" + value={summary.totalOrders || 0} + bgColor="bg-blue-50" + /> + } + title="Avg Order Value" + value={`$${((summary.averageOrderValue || 0) / 100).toFixed(2)}`} + bgColor="bg-purple-50" + /> + } + title="Refunds" + value={`$${((summary.totalRefunds || 0) / 100).toFixed(2)}`} + bgColor="bg-red-50" + /> +
+ +
+
+

Net Sales

+

+ ${((summary.netSales || 0) / 100).toFixed(2)} +

+

+ After refunds: ${((summary.totalRefunds || 0) / 100).toFixed(2)} +

+
+ +
+

Tax & Tips

+
+

+ Tax: ${((summary.totalTax || 0) / 100).toFixed(2)} +

+

+ Tips: ${((summary.totalTips || 0) / 100).toFixed(2)} +

+
+
+
+
+ ); +} + +function StatCard({ icon, title, value, bgColor }: any) { + return ( +
+
{icon}
+

{title}

+

{value}

+
+ ); +} diff --git a/servers/clover/src/ui/react-app/tax-manager.tsx b/servers/clover/src/ui/react-app/tax-manager.tsx new file mode 100644 index 0000000..5426f55 --- /dev/null +++ b/servers/clover/src/ui/react-app/tax-manager.tsx @@ -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([]); + 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 ( +
+
+

Tax Manager

+ +
+ + {loading ? ( +
Loading...
+ ) : ( +
+ {taxRates.map((taxRate) => ( +
+
+
+ +
+

{taxRate.name}

+

+ {taxRate.rate}% +

+ {taxRate.isDefault && ( + + Default + + )} +
+
+
+ +
+ + +
+
+ ))} +
+ )} +
+ ); +} diff --git a/servers/clover/tsconfig.json b/servers/clover/tsconfig.json index de6431e..1482be8 100644 --- a/servers/clover/tsconfig.json +++ b/servers/clover/tsconfig.json @@ -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"]