housecall-pro: Complete MCP server with 47 tools, 16 React apps, full API client
- API Client: Housecall Pro API with auth, pagination, error handling - 47 Tools across 10 categories: - jobs-tools (10): list, get, create, update, complete, cancel, line items, schedule, reschedule - customers-tools (7): list, get, create, update, delete, search, addresses - estimates-tools (8): list, get, create, update, send, approve, decline, convert to job - invoices-tools (6): list, get, create, send, mark paid, list payments - employees-tools (6): list, get, create, update, schedule, time entries - dispatch-tools (3): dispatch board, assign employee, availability - tags-tools (5): list, create, delete, add to job/customer - notifications-tools (3): list, send, mark read - reviews-tools (3): list, get, request review - reporting-tools (3): revenue, job completion, employee performance - 16 React MCP Apps for rich UI: - job-dashboard, job-detail, job-grid - customer-detail, customer-grid - estimate-builder, estimate-grid - invoice-dashboard, invoice-detail - dispatch-board - employee-schedule, employee-performance - review-dashboard, revenue-dashboard - tag-manager, notification-center - Complete types, comprehensive README, full package.json
This commit is contained in:
parent
7de8a68173
commit
5adccfd36e
381
servers/housecall-pro/README.md
Normal file
381
servers/housecall-pro/README.md
Normal file
@ -0,0 +1,381 @@
|
||||
# Housecall Pro MCP Server
|
||||
|
||||
Complete Model Context Protocol server for Housecall Pro field service management platform.
|
||||
|
||||
## Features
|
||||
|
||||
### 🛠️ **47 Tools Across 10 Categories**
|
||||
|
||||
#### Job Management (10 tools)
|
||||
- `list_jobs` - List jobs with filters (status, customer, dates, tags)
|
||||
- `get_job` - Get detailed job information
|
||||
- `create_job` - Create new jobs
|
||||
- `update_job` - Update job details
|
||||
- `complete_job` - Mark jobs as completed
|
||||
- `cancel_job` - Cancel jobs with reason
|
||||
- `list_job_line_items` - List line items for a job
|
||||
- `add_job_line_item` - Add line items to jobs
|
||||
- `schedule_job` - Schedule jobs
|
||||
- `reschedule_job` - Reschedule existing jobs
|
||||
|
||||
#### Customer Management (7 tools)
|
||||
- `list_customers` - List customers with filters
|
||||
- `get_customer` - Get customer details
|
||||
- `create_customer` - Create new customers
|
||||
- `update_customer` - Update customer information
|
||||
- `delete_customer` - Delete customers
|
||||
- `search_customers` - Search by name, email, phone
|
||||
- `list_customer_addresses` - List customer addresses
|
||||
|
||||
#### Estimate Management (8 tools)
|
||||
- `list_estimates` - List estimates with filters
|
||||
- `get_estimate` - Get estimate details
|
||||
- `create_estimate` - Create new estimates
|
||||
- `update_estimate` - Update estimate details
|
||||
- `send_estimate` - Send estimates to customers
|
||||
- `approve_estimate` - Mark estimates as approved
|
||||
- `decline_estimate` - Decline estimates
|
||||
- `convert_estimate_to_job` - Convert approved estimates to jobs
|
||||
|
||||
#### Invoice Management (6 tools)
|
||||
- `list_invoices` - List invoices with filters
|
||||
- `get_invoice` - Get invoice details
|
||||
- `create_invoice` - Create new invoices
|
||||
- `send_invoice` - Send invoices to customers
|
||||
- `mark_invoice_paid` - Record invoice payments
|
||||
- `list_invoice_payments` - List all payments for an invoice
|
||||
|
||||
#### Employee Management (6 tools)
|
||||
- `list_employees` - List employees with filters
|
||||
- `get_employee` - Get employee details
|
||||
- `create_employee` - Create new employees
|
||||
- `update_employee` - Update employee information
|
||||
- `get_employee_schedule` - Get employee schedules
|
||||
- `list_employee_time_entries` - List time entries
|
||||
|
||||
#### Dispatch & Scheduling (3 tools)
|
||||
- `get_dispatch_board` - Get dispatch board view
|
||||
- `assign_employee_to_job` - Assign employees to jobs
|
||||
- `get_employee_availability` - Check employee availability
|
||||
|
||||
#### Tag Management (5 tools)
|
||||
- `list_tags` - List all tags
|
||||
- `create_tag` - Create new tags
|
||||
- `delete_tag` - Delete tags
|
||||
- `add_tag_to_job` - Tag jobs
|
||||
- `add_tag_to_customer` - Tag customers
|
||||
|
||||
#### Notification Management (3 tools)
|
||||
- `list_notifications` - List sent notifications
|
||||
- `send_notification` - Send SMS/email notifications
|
||||
- `mark_notification_read` - Mark notifications as read
|
||||
|
||||
#### Review Management (3 tools)
|
||||
- `list_reviews` - List customer reviews
|
||||
- `get_review` - Get review details
|
||||
- `request_review` - Request reviews from customers
|
||||
|
||||
#### Reporting & Analytics (3 tools)
|
||||
- `get_revenue_report` - Revenue analytics
|
||||
- `get_job_completion_report` - Job completion metrics
|
||||
- `get_employee_performance_report` - Employee performance stats
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @mcpengine/housecall-pro
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the required environment variable:
|
||||
|
||||
```bash
|
||||
export HOUSECALL_PRO_API_KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
Optional configuration:
|
||||
|
||||
```bash
|
||||
export HOUSECALL_PRO_BASE_URL="https://api.housecallpro.com" # default
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As MCP Server
|
||||
|
||||
Add to your MCP settings file:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"housecall-pro": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@mcpengine/housecall-pro"],
|
||||
"env": {
|
||||
"HOUSECALL_PRO_API_KEY": "your-api-key-here"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Programmatic Usage
|
||||
|
||||
```typescript
|
||||
import { HousecallProClient } from '@mcpengine/housecall-pro';
|
||||
|
||||
const client = new HousecallProClient({
|
||||
apiKey: process.env.HOUSECALL_PRO_API_KEY!,
|
||||
});
|
||||
|
||||
// List jobs
|
||||
const jobs = await client.listJobs({
|
||||
work_status: 'scheduled',
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-01-31',
|
||||
});
|
||||
|
||||
// Create a customer
|
||||
const customer = await client.createCustomer({
|
||||
first_name: 'John',
|
||||
last_name: 'Doe',
|
||||
email: 'john@example.com',
|
||||
mobile_number: '+1234567890',
|
||||
});
|
||||
|
||||
// Create an estimate
|
||||
const estimate = await client.createEstimate({
|
||||
customer_id: customer.id,
|
||||
line_items: [
|
||||
{
|
||||
name: 'HVAC Repair',
|
||||
description: 'Replace thermostat',
|
||||
quantity: 1,
|
||||
unit_price: 250.00,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// Send the estimate
|
||||
await client.sendEstimate(estimate.id);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Client Initialization
|
||||
|
||||
```typescript
|
||||
const client = new HousecallProClient({
|
||||
apiKey: string; // Required: Your Housecall Pro API key
|
||||
baseUrl?: string; // Optional: API base URL (default: https://api.housecallpro.com)
|
||||
});
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
The client throws `APIError` objects with the following structure:
|
||||
|
||||
```typescript
|
||||
{
|
||||
error: 'APIError' | 'NetworkError' | 'ExecutionError';
|
||||
message: string;
|
||||
status: number;
|
||||
details?: any;
|
||||
}
|
||||
```
|
||||
|
||||
Example error handling:
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const job = await client.getJob('job-123');
|
||||
} catch (error: any) {
|
||||
if (error.error === 'APIError') {
|
||||
console.error(`API Error ${error.status}: ${error.message}`);
|
||||
} else {
|
||||
console.error(`Error: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pagination
|
||||
|
||||
List methods support pagination:
|
||||
|
||||
```typescript
|
||||
// Get single page
|
||||
const page = await client.getPage('/jobs', {
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
});
|
||||
|
||||
// Get all pages (auto-pagination)
|
||||
const allJobs = await client.listJobs({
|
||||
page_size: 100, // Items per page
|
||||
});
|
||||
```
|
||||
|
||||
## Tool Examples
|
||||
|
||||
### Job Management
|
||||
|
||||
```typescript
|
||||
// Create and schedule a job
|
||||
const job = await client.createJob({
|
||||
customer_id: 'cust-123',
|
||||
description: 'Annual HVAC maintenance',
|
||||
schedule_start: '2024-02-15T09:00:00Z',
|
||||
schedule_end: '2024-02-15T11:00:00Z',
|
||||
assigned_employees: ['emp-456'],
|
||||
});
|
||||
|
||||
// Add line items
|
||||
await client.addJobLineItem(job.id, {
|
||||
name: 'Filter Replacement',
|
||||
quantity: 2,
|
||||
unit_price: 45.00,
|
||||
});
|
||||
|
||||
// Complete the job
|
||||
await client.completeJob(job.id);
|
||||
```
|
||||
|
||||
### Customer & Address Management
|
||||
|
||||
```typescript
|
||||
// Create customer with tags
|
||||
const customer = await client.createCustomer({
|
||||
first_name: 'Jane',
|
||||
last_name: 'Smith',
|
||||
email: 'jane@example.com',
|
||||
tags: ['vip', 'commercial'],
|
||||
});
|
||||
|
||||
// Get customer addresses
|
||||
const addresses = await client.listCustomerAddresses(customer.id);
|
||||
```
|
||||
|
||||
### Estimates & Invoices
|
||||
|
||||
```typescript
|
||||
// Create estimate
|
||||
const estimate = await client.createEstimate({
|
||||
customer_id: 'cust-123',
|
||||
line_items: [
|
||||
{ name: 'Labor', quantity: 2, unit_price: 85.00 },
|
||||
{ name: 'Parts', quantity: 1, unit_price: 150.00 },
|
||||
],
|
||||
valid_until: '2024-03-01',
|
||||
});
|
||||
|
||||
// Send and track
|
||||
await client.sendEstimate(estimate.id);
|
||||
await client.approveEstimate(estimate.id);
|
||||
const job = await client.convertEstimateToJob(estimate.id);
|
||||
|
||||
// Create invoice from job
|
||||
const invoice = await client.createInvoice({
|
||||
job_id: job.id,
|
||||
due_date: '2024-02-28',
|
||||
});
|
||||
|
||||
// Record payment
|
||||
await client.markInvoicePaid(invoice.id, {
|
||||
amount: 320.00,
|
||||
method: 'card',
|
||||
reference: 'ch_1234567890',
|
||||
});
|
||||
```
|
||||
|
||||
### Dispatch & Scheduling
|
||||
|
||||
```typescript
|
||||
// Check employee availability
|
||||
const availability = await client.getEmployeeAvailability('emp-123', {
|
||||
start_date: '2024-02-15',
|
||||
end_date: '2024-02-21',
|
||||
});
|
||||
|
||||
// View dispatch board
|
||||
const board = await client.getDispatchBoard({
|
||||
date: '2024-02-15',
|
||||
employee_ids: ['emp-123', 'emp-456'],
|
||||
});
|
||||
|
||||
// Assign employee
|
||||
await client.assignEmployee('job-789', 'emp-123');
|
||||
```
|
||||
|
||||
### Reports & Analytics
|
||||
|
||||
```typescript
|
||||
// Revenue report
|
||||
const revenue = await client.getRevenueReport({
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-01-31',
|
||||
group_by: 'day',
|
||||
});
|
||||
|
||||
// Job completion metrics
|
||||
const completion = await client.getJobCompletionReport({
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-01-31',
|
||||
});
|
||||
|
||||
// Employee performance
|
||||
const performance = await client.getEmployeePerformanceReport({
|
||||
employee_id: 'emp-123',
|
||||
start_date: '2024-01-01',
|
||||
end_date: '2024-01-31',
|
||||
});
|
||||
```
|
||||
|
||||
## MCP Apps (UI Components)
|
||||
|
||||
This server includes 16 React-based MCP apps for rich UI interactions:
|
||||
|
||||
- **job-dashboard** - Overview of all jobs
|
||||
- **job-detail** - Detailed job view
|
||||
- **job-grid** - Grid view of jobs
|
||||
- **customer-detail** - Customer profile view
|
||||
- **customer-grid** - Customer list/grid
|
||||
- **estimate-builder** - Interactive estimate creation
|
||||
- **estimate-grid** - Estimate list/grid
|
||||
- **invoice-dashboard** - Invoice overview
|
||||
- **invoice-detail** - Detailed invoice view
|
||||
- **dispatch-board** - Visual dispatch/scheduling board
|
||||
- **employee-schedule** - Employee schedule calendar
|
||||
- **employee-performance** - Performance metrics dashboard
|
||||
- **review-dashboard** - Review management
|
||||
- **revenue-dashboard** - Revenue analytics
|
||||
- **tag-manager** - Tag organization
|
||||
- **notification-center** - Notification management
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Build
|
||||
npm run build
|
||||
|
||||
# Development (watch mode)
|
||||
npm run dev
|
||||
|
||||
# Run server
|
||||
npm start
|
||||
```
|
||||
|
||||
## API Documentation
|
||||
|
||||
For complete API documentation, visit: https://docs.housecallpro.com/
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Support
|
||||
|
||||
For issues and feature requests, please visit: https://github.com/mcpengine/housecall-pro-mcp
|
||||
38
servers/housecall-pro/package.json
Normal file
38
servers/housecall-pro/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@mcpengine/housecall-pro",
|
||||
"version": "1.0.0",
|
||||
"description": "Housecall Pro MCP Server - Complete field service management integration",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"bin": {
|
||||
"housecall-pro-mcp": "dist/main.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && chmod +x dist/main.js",
|
||||
"dev": "tsc --watch",
|
||||
"start": "node dist/main.js",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"housecall-pro",
|
||||
"field-service",
|
||||
"scheduling",
|
||||
"dispatch",
|
||||
"crm"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.10.5",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
}
|
||||
480
servers/housecall-pro/src/clients/housecall-pro.ts
Normal file
480
servers/housecall-pro/src/clients/housecall-pro.ts
Normal file
@ -0,0 +1,480 @@
|
||||
/**
|
||||
* Housecall Pro API Client
|
||||
* https://api.housecallpro.com/
|
||||
*/
|
||||
|
||||
import {
|
||||
HousecallProConfig,
|
||||
PaginationParams,
|
||||
PaginatedResponse,
|
||||
APIError,
|
||||
} from '../types/index.js';
|
||||
|
||||
export class HousecallProClient {
|
||||
private apiKey: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(config: HousecallProConfig) {
|
||||
this.apiKey = config.apiKey;
|
||||
this.baseUrl = config.baseUrl || 'https://api.housecallpro.com';
|
||||
}
|
||||
|
||||
/**
|
||||
* Make authenticated request to Housecall Pro API
|
||||
*/
|
||||
private async request<T>(
|
||||
endpoint: string,
|
||||
options: RequestInit = {}
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${this.apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
...options.headers as Record<string, string>,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
|
||||
try {
|
||||
const errorJson = JSON.parse(errorBody);
|
||||
errorMessage = errorJson.message || errorJson.error || errorMessage;
|
||||
} catch {
|
||||
// Use default error message
|
||||
}
|
||||
|
||||
const error: APIError = {
|
||||
error: 'APIError',
|
||||
message: errorMessage,
|
||||
status: response.status,
|
||||
details: errorBody,
|
||||
};
|
||||
throw error;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data as T;
|
||||
} catch (error: any) {
|
||||
if (error.error === 'APIError') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const apiError: APIError = {
|
||||
error: 'NetworkError',
|
||||
message: error.message || 'Network request failed',
|
||||
status: 0,
|
||||
details: error,
|
||||
};
|
||||
throw apiError;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET request
|
||||
*/
|
||||
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||
let url = endpoint;
|
||||
|
||||
if (params) {
|
||||
const queryParams = new URLSearchParams();
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
if (value !== undefined && value !== null) {
|
||||
queryParams.append(key, String(value));
|
||||
}
|
||||
});
|
||||
const queryString = queryParams.toString();
|
||||
if (queryString) {
|
||||
url += `?${queryString}`;
|
||||
}
|
||||
}
|
||||
|
||||
return this.request<T>(url, { method: 'GET' });
|
||||
}
|
||||
|
||||
/**
|
||||
* POST request
|
||||
*/
|
||||
async post<T>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'POST',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT request
|
||||
*/
|
||||
async put<T>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PUT',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH request
|
||||
*/
|
||||
async patch<T>(endpoint: string, body?: any): Promise<T> {
|
||||
return this.request<T>(endpoint, {
|
||||
method: 'PATCH',
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE request
|
||||
*/
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
return this.request<T>(endpoint, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated GET request - fetches all pages
|
||||
*/
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
params?: PaginationParams & Record<string, any>
|
||||
): Promise<T[]> {
|
||||
const allItems: T[] = [];
|
||||
let currentPage = params?.page || 1;
|
||||
const pageSize = params?.page_size || 50;
|
||||
|
||||
// Create params without page/page_size for first request
|
||||
const { page, page_size, ...otherParams } = params || {};
|
||||
|
||||
while (true) {
|
||||
const response = await this.get<PaginatedResponse<T>>(endpoint, {
|
||||
...otherParams,
|
||||
page: currentPage,
|
||||
page_size: pageSize,
|
||||
});
|
||||
|
||||
allItems.push(...response.data);
|
||||
|
||||
// Check if we've reached the last page
|
||||
if (currentPage >= response.total_pages) {
|
||||
break;
|
||||
}
|
||||
|
||||
currentPage++;
|
||||
}
|
||||
|
||||
return allItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Paginated GET request - single page
|
||||
*/
|
||||
async getPage<T>(
|
||||
endpoint: string,
|
||||
params?: PaginationParams & Record<string, any>
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
return this.get<PaginatedResponse<T>>(endpoint, params);
|
||||
}
|
||||
|
||||
// ===== Customer Endpoints =====
|
||||
|
||||
async listCustomers(params?: PaginationParams & {
|
||||
search?: string;
|
||||
tags?: string[];
|
||||
lead_source?: string;
|
||||
}) {
|
||||
return this.getPaginated('/customers', params);
|
||||
}
|
||||
|
||||
async getCustomer(customerId: string) {
|
||||
return this.get(`/customers/${customerId}`);
|
||||
}
|
||||
|
||||
async createCustomer(data: any) {
|
||||
return this.post('/customers', data);
|
||||
}
|
||||
|
||||
async updateCustomer(customerId: string, data: any) {
|
||||
return this.patch(`/customers/${customerId}`, data);
|
||||
}
|
||||
|
||||
async deleteCustomer(customerId: string) {
|
||||
return this.delete(`/customers/${customerId}`);
|
||||
}
|
||||
|
||||
async searchCustomers(query: string, params?: PaginationParams) {
|
||||
return this.getPaginated('/customers', { ...params, search: query });
|
||||
}
|
||||
|
||||
async listCustomerAddresses(customerId: string) {
|
||||
return this.get(`/customers/${customerId}/addresses`);
|
||||
}
|
||||
|
||||
// ===== Job Endpoints =====
|
||||
|
||||
async listJobs(params?: PaginationParams & {
|
||||
customer_id?: string;
|
||||
work_status?: string;
|
||||
invoice_status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
tags?: string[];
|
||||
}) {
|
||||
return this.getPaginated('/jobs', params);
|
||||
}
|
||||
|
||||
async getJob(jobId: string) {
|
||||
return this.get(`/jobs/${jobId}`);
|
||||
}
|
||||
|
||||
async createJob(data: any) {
|
||||
return this.post('/jobs', data);
|
||||
}
|
||||
|
||||
async updateJob(jobId: string, data: any) {
|
||||
return this.patch(`/jobs/${jobId}`, data);
|
||||
}
|
||||
|
||||
async completeJob(jobId: string) {
|
||||
return this.post(`/jobs/${jobId}/complete`, {});
|
||||
}
|
||||
|
||||
async cancelJob(jobId: string, reason?: string) {
|
||||
return this.post(`/jobs/${jobId}/cancel`, { reason });
|
||||
}
|
||||
|
||||
async listJobLineItems(jobId: string) {
|
||||
return this.get(`/jobs/${jobId}/line_items`);
|
||||
}
|
||||
|
||||
async addJobLineItem(jobId: string, data: any) {
|
||||
return this.post(`/jobs/${jobId}/line_items`, data);
|
||||
}
|
||||
|
||||
async scheduleJob(jobId: string, schedule: any) {
|
||||
return this.post(`/jobs/${jobId}/schedule`, schedule);
|
||||
}
|
||||
|
||||
async rescheduleJob(jobId: string, schedule: any) {
|
||||
return this.patch(`/jobs/${jobId}/schedule`, schedule);
|
||||
}
|
||||
|
||||
// ===== Estimate Endpoints =====
|
||||
|
||||
async listEstimates(params?: PaginationParams & {
|
||||
customer_id?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
return this.getPaginated('/estimates', params);
|
||||
}
|
||||
|
||||
async getEstimate(estimateId: string) {
|
||||
return this.get(`/estimates/${estimateId}`);
|
||||
}
|
||||
|
||||
async createEstimate(data: any) {
|
||||
return this.post('/estimates', data);
|
||||
}
|
||||
|
||||
async updateEstimate(estimateId: string, data: any) {
|
||||
return this.patch(`/estimates/${estimateId}`, data);
|
||||
}
|
||||
|
||||
async sendEstimate(estimateId: string, options?: { email?: string; message?: string }) {
|
||||
return this.post(`/estimates/${estimateId}/send`, options);
|
||||
}
|
||||
|
||||
async approveEstimate(estimateId: string) {
|
||||
return this.post(`/estimates/${estimateId}/approve`, {});
|
||||
}
|
||||
|
||||
async declineEstimate(estimateId: string, reason?: string) {
|
||||
return this.post(`/estimates/${estimateId}/decline`, { reason });
|
||||
}
|
||||
|
||||
async convertEstimateToJob(estimateId: string) {
|
||||
return this.post(`/estimates/${estimateId}/convert`, {});
|
||||
}
|
||||
|
||||
// ===== Invoice Endpoints =====
|
||||
|
||||
async listInvoices(params?: PaginationParams & {
|
||||
customer_id?: string;
|
||||
job_id?: string;
|
||||
status?: string;
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}) {
|
||||
return this.getPaginated('/invoices', params);
|
||||
}
|
||||
|
||||
async getInvoice(invoiceId: string) {
|
||||
return this.get(`/invoices/${invoiceId}`);
|
||||
}
|
||||
|
||||
async createInvoice(data: any) {
|
||||
return this.post('/invoices', data);
|
||||
}
|
||||
|
||||
async sendInvoice(invoiceId: string, options?: { email?: string; message?: string }) {
|
||||
return this.post(`/invoices/${invoiceId}/send`, options);
|
||||
}
|
||||
|
||||
async markInvoicePaid(invoiceId: string, payment: any) {
|
||||
return this.post(`/invoices/${invoiceId}/payments`, payment);
|
||||
}
|
||||
|
||||
async listInvoicePayments(invoiceId: string) {
|
||||
return this.get(`/invoices/${invoiceId}/payments`);
|
||||
}
|
||||
|
||||
// ===== Employee Endpoints =====
|
||||
|
||||
async listEmployees(params?: PaginationParams & {
|
||||
is_active?: boolean;
|
||||
role?: string;
|
||||
}) {
|
||||
return this.getPaginated('/employees', params);
|
||||
}
|
||||
|
||||
async getEmployee(employeeId: string) {
|
||||
return this.get(`/employees/${employeeId}`);
|
||||
}
|
||||
|
||||
async createEmployee(data: any) {
|
||||
return this.post('/employees', data);
|
||||
}
|
||||
|
||||
async updateEmployee(employeeId: string, data: any) {
|
||||
return this.patch(`/employees/${employeeId}`, data);
|
||||
}
|
||||
|
||||
async getEmployeeSchedule(employeeId: string, params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}) {
|
||||
return this.get(`/employees/${employeeId}/schedule`, params);
|
||||
}
|
||||
|
||||
async listEmployeeTimeEntries(employeeId: string, params?: PaginationParams & {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}) {
|
||||
return this.getPaginated(`/employees/${employeeId}/time_entries`, params);
|
||||
}
|
||||
|
||||
// ===== Dispatch Endpoints =====
|
||||
|
||||
async getDispatchBoard(params?: {
|
||||
date?: string;
|
||||
employee_ids?: string[];
|
||||
}) {
|
||||
return this.get('/dispatch/board', params);
|
||||
}
|
||||
|
||||
async assignEmployee(jobId: string, employeeId: string) {
|
||||
return this.post(`/jobs/${jobId}/assign`, { employee_id: employeeId });
|
||||
}
|
||||
|
||||
async getEmployeeAvailability(employeeId: string, params?: {
|
||||
start_date?: string;
|
||||
end_date?: string;
|
||||
}) {
|
||||
return this.get(`/employees/${employeeId}/availability`, params);
|
||||
}
|
||||
|
||||
// ===== Tag Endpoints =====
|
||||
|
||||
async listTags(params?: PaginationParams) {
|
||||
return this.getPaginated('/tags', params);
|
||||
}
|
||||
|
||||
async createTag(data: { name: string; color?: string }) {
|
||||
return this.post('/tags', data);
|
||||
}
|
||||
|
||||
async deleteTag(tagId: string) {
|
||||
return this.delete(`/tags/${tagId}`);
|
||||
}
|
||||
|
||||
async addTagToJob(jobId: string, tagId: string) {
|
||||
return this.post(`/jobs/${jobId}/tags`, { tag_id: tagId });
|
||||
}
|
||||
|
||||
async addTagToCustomer(customerId: string, tagId: string) {
|
||||
return this.post(`/customers/${customerId}/tags`, { tag_id: tagId });
|
||||
}
|
||||
|
||||
// ===== Notification Endpoints =====
|
||||
|
||||
async listNotifications(params?: PaginationParams & {
|
||||
type?: string;
|
||||
status?: string;
|
||||
}) {
|
||||
return this.getPaginated('/notifications', params);
|
||||
}
|
||||
|
||||
async sendNotification(data: {
|
||||
type: 'sms' | 'email';
|
||||
recipient: string;
|
||||
subject?: string;
|
||||
message: string;
|
||||
}) {
|
||||
return this.post('/notifications', data);
|
||||
}
|
||||
|
||||
async markNotificationRead(notificationId: string) {
|
||||
return this.patch(`/notifications/${notificationId}`, { read: true });
|
||||
}
|
||||
|
||||
// ===== Review Endpoints =====
|
||||
|
||||
async listReviews(params?: PaginationParams & {
|
||||
customer_id?: string;
|
||||
job_id?: string;
|
||||
rating?: number;
|
||||
}) {
|
||||
return this.getPaginated('/reviews', params);
|
||||
}
|
||||
|
||||
async getReview(reviewId: string) {
|
||||
return this.get(`/reviews/${reviewId}`);
|
||||
}
|
||||
|
||||
async requestReview(jobId: string, options?: {
|
||||
method?: 'sms' | 'email';
|
||||
message?: string;
|
||||
}) {
|
||||
return this.post(`/jobs/${jobId}/request_review`, options);
|
||||
}
|
||||
|
||||
// ===== Reporting Endpoints =====
|
||||
|
||||
async getRevenueReport(params: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
group_by?: 'day' | 'week' | 'month';
|
||||
}) {
|
||||
return this.get('/reports/revenue', params);
|
||||
}
|
||||
|
||||
async getJobCompletionReport(params: {
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}) {
|
||||
return this.get('/reports/job_completion', params);
|
||||
}
|
||||
|
||||
async getEmployeePerformanceReport(params: {
|
||||
employee_id?: string;
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}) {
|
||||
return this.get('/reports/employee_performance', params);
|
||||
}
|
||||
}
|
||||
7
servers/housecall-pro/src/index.ts
Normal file
7
servers/housecall-pro/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Housecall Pro MCP Server - Main Exports
|
||||
*/
|
||||
|
||||
export { HousecallProServer } from './server.js';
|
||||
export { HousecallProClient } from './clients/housecall-pro.js';
|
||||
export * from './types/index.js';
|
||||
19
servers/housecall-pro/src/main.ts
Normal file
19
servers/housecall-pro/src/main.ts
Normal file
@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Housecall Pro MCP Server Entry Point
|
||||
*/
|
||||
|
||||
import { HousecallProServer } from './server.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new HousecallProServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
console.error('Failed to start server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
136
servers/housecall-pro/src/server.ts
Normal file
136
servers/housecall-pro/src/server.ts
Normal file
@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Housecall Pro MCP Server
|
||||
*/
|
||||
|
||||
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 { HousecallProClient } from './clients/housecall-pro.js';
|
||||
import { registerAllTools } from './tools/index.js';
|
||||
|
||||
export class HousecallProServer {
|
||||
private server: Server;
|
||||
private client: HousecallProClient;
|
||||
private tools: Record<string, any>;
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'housecall-pro',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
resources: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize client (API key will be set from env var)
|
||||
const apiKey = process.env.HOUSECALL_PRO_API_KEY;
|
||||
if (!apiKey) {
|
||||
throw new Error('HOUSECALL_PRO_API_KEY environment variable is required');
|
||||
}
|
||||
|
||||
this.client = new HousecallProClient({
|
||||
apiKey,
|
||||
baseUrl: process.env.HOUSECALL_PRO_BASE_URL,
|
||||
});
|
||||
|
||||
// Register all tools
|
||||
this.tools = registerAllTools(this.client);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
private setupHandlers() {
|
||||
// List available tools
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: Object.entries(this.tools).map(([name, tool]) => ({
|
||||
name,
|
||||
description: tool.description,
|
||||
inputSchema: tool.parameters,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Handle tool execution
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
const tool = this.tools[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) {
|
||||
// Handle API errors
|
||||
if (error.error === 'APIError') {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
error: error.error,
|
||||
message: error.message,
|
||||
status: error.status,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle other errors
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify({
|
||||
error: 'ExecutionError',
|
||||
message: error.message || 'Tool execution failed',
|
||||
details: error,
|
||||
}, null, 2),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// List resources (empty for now, but could include templates, reports, etc.)
|
||||
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
||||
return {
|
||||
resources: [],
|
||||
};
|
||||
});
|
||||
|
||||
// Read resource (empty for now)
|
||||
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
||||
throw new Error(`Resource not found: ${request.params.uri}`);
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('Housecall Pro MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
242
servers/housecall-pro/src/tools/customers-tools.ts
Normal file
242
servers/housecall-pro/src/tools/customers-tools.ts
Normal file
@ -0,0 +1,242 @@
|
||||
/**
|
||||
* Customer Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Customer, CreateCustomerRequest } from '../types/index.js';
|
||||
|
||||
export function registerCustomersTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_customers: {
|
||||
description: 'List customers with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
search: {
|
||||
type: 'string',
|
||||
description: 'Search query (name, email, phone)',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by tags',
|
||||
},
|
||||
lead_source: {
|
||||
type: 'string',
|
||||
description: 'Filter by lead source',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const customers = await client.listCustomers(params);
|
||||
return { customers, count: customers.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_customer: {
|
||||
description: 'Get detailed information about a specific customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: { customer_id: string }) => {
|
||||
const customer = await client.getCustomer(params.customer_id);
|
||||
return customer;
|
||||
},
|
||||
},
|
||||
|
||||
create_customer: {
|
||||
description: 'Create a new customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
first_name: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
mobile_number: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
home_number: {
|
||||
type: 'string',
|
||||
description: 'Home phone number',
|
||||
},
|
||||
work_number: {
|
||||
type: 'string',
|
||||
description: 'Work phone number',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
description: 'Company name',
|
||||
},
|
||||
notifications_enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Enable notifications for this customer',
|
||||
},
|
||||
lead_source: {
|
||||
type: 'string',
|
||||
description: 'Lead source',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tag IDs',
|
||||
},
|
||||
},
|
||||
required: ['first_name', 'last_name'],
|
||||
},
|
||||
handler: async (params: CreateCustomerRequest) => {
|
||||
const customer = await client.createCustomer(params);
|
||||
return customer;
|
||||
},
|
||||
},
|
||||
|
||||
update_customer: {
|
||||
description: 'Update an existing customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
first_name: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
mobile_number: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
home_number: {
|
||||
type: 'string',
|
||||
description: 'Home phone number',
|
||||
},
|
||||
work_number: {
|
||||
type: 'string',
|
||||
description: 'Work phone number',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
description: 'Company name',
|
||||
},
|
||||
notifications_enabled: {
|
||||
type: 'boolean',
|
||||
description: 'Enable notifications for this customer',
|
||||
},
|
||||
lead_source: {
|
||||
type: 'string',
|
||||
description: 'Lead source',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tag IDs',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { customer_id, ...updateData } = params;
|
||||
const customer = await client.updateCustomer(customer_id, updateData);
|
||||
return customer;
|
||||
},
|
||||
},
|
||||
|
||||
delete_customer: {
|
||||
description: 'Delete a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: { customer_id: string }) => {
|
||||
const result = await client.deleteCustomer(params.customer_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
search_customers: {
|
||||
description: 'Search customers by name, email, or phone',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
query: {
|
||||
type: 'string',
|
||||
description: 'Search query',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
required: ['query'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const customers = await client.searchCustomers(params.query, {
|
||||
page: params.page,
|
||||
page_size: params.page_size,
|
||||
});
|
||||
return { customers, count: customers.length };
|
||||
},
|
||||
},
|
||||
|
||||
list_customer_addresses: {
|
||||
description: 'List all addresses for a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: { customer_id: string }) => {
|
||||
const addresses = await client.listCustomerAddresses(params.customer_id);
|
||||
return addresses;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
80
servers/housecall-pro/src/tools/dispatch-tools.ts
Normal file
80
servers/housecall-pro/src/tools/dispatch-tools.ts
Normal file
@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Dispatch and Scheduling Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
|
||||
export function registerDispatchTools(client: HousecallProClient) {
|
||||
return {
|
||||
get_dispatch_board: {
|
||||
description: 'Get the dispatch board showing scheduled jobs and employee assignments',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
date: {
|
||||
type: 'string',
|
||||
description: 'Date for dispatch board (ISO 8601 date, e.g. "2024-01-15")',
|
||||
},
|
||||
employee_ids: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by specific employee IDs',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const board = await client.getDispatchBoard(params);
|
||||
return board;
|
||||
},
|
||||
},
|
||||
|
||||
assign_employee_to_job: {
|
||||
description: 'Assign an employee to a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID to assign',
|
||||
},
|
||||
},
|
||||
required: ['job_id', 'employee_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string; employee_id: string }) => {
|
||||
const result = await client.assignEmployee(params.job_id, params.employee_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
get_employee_availability: {
|
||||
description: 'Get availability slots for an employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date for availability (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date for availability (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['employee_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { employee_id, ...dateParams } = params;
|
||||
const availability = await client.getEmployeeAvailability(employee_id, dateParams);
|
||||
return availability;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
207
servers/housecall-pro/src/tools/employees-tools.ts
Normal file
207
servers/housecall-pro/src/tools/employees-tools.ts
Normal file
@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Employee Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Employee, CreateEmployeeRequest, TimeEntry } from '../types/index.js';
|
||||
|
||||
export function registerEmployeesTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_employees: {
|
||||
description: 'List employees with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
is_active: {
|
||||
type: 'boolean',
|
||||
description: 'Filter by active status',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['admin', 'dispatcher', 'technician', 'sales'],
|
||||
description: 'Filter by role',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const employees = await client.listEmployees(params);
|
||||
return { employees, count: employees.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_employee: {
|
||||
description: 'Get detailed information about a specific employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID',
|
||||
},
|
||||
},
|
||||
required: ['employee_id'],
|
||||
},
|
||||
handler: async (params: { employee_id: string }) => {
|
||||
const employee = await client.getEmployee(params.employee_id);
|
||||
return employee;
|
||||
},
|
||||
},
|
||||
|
||||
create_employee: {
|
||||
description: 'Create a new employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
first_name: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
mobile_number: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['admin', 'dispatcher', 'technician', 'sales'],
|
||||
description: 'Employee role',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Color for calendar display (hex code)',
|
||||
},
|
||||
},
|
||||
required: ['first_name', 'last_name', 'email', 'role'],
|
||||
},
|
||||
handler: async (params: CreateEmployeeRequest) => {
|
||||
const employee = await client.createEmployee(params);
|
||||
return employee;
|
||||
},
|
||||
},
|
||||
|
||||
update_employee: {
|
||||
description: 'Update an existing employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID',
|
||||
},
|
||||
first_name: {
|
||||
type: 'string',
|
||||
description: 'First name',
|
||||
},
|
||||
last_name: {
|
||||
type: 'string',
|
||||
description: 'Last name',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Email address',
|
||||
},
|
||||
mobile_number: {
|
||||
type: 'string',
|
||||
description: 'Mobile phone number',
|
||||
},
|
||||
role: {
|
||||
type: 'string',
|
||||
enum: ['admin', 'dispatcher', 'technician', 'sales'],
|
||||
description: 'Employee role',
|
||||
},
|
||||
is_active: {
|
||||
type: 'boolean',
|
||||
description: 'Active status',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Color for calendar display (hex code)',
|
||||
},
|
||||
},
|
||||
required: ['employee_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { employee_id, ...updateData } = params;
|
||||
const employee = await client.updateEmployee(employee_id, updateData);
|
||||
return employee;
|
||||
},
|
||||
},
|
||||
|
||||
get_employee_schedule: {
|
||||
description: 'Get schedule for an employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date for schedule (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date for schedule (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['employee_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { employee_id, ...dateParams } = params;
|
||||
const schedule = await client.getEmployeeSchedule(employee_id, dateParams);
|
||||
return schedule;
|
||||
},
|
||||
},
|
||||
|
||||
list_employee_time_entries: {
|
||||
description: 'List time entries for an employee',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date for time entries (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date for time entries (ISO 8601)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
required: ['employee_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { employee_id, ...queryParams } = params;
|
||||
const timeEntries = await client.listEmployeeTimeEntries(employee_id, queryParams);
|
||||
return { time_entries: timeEntries, count: timeEntries.length };
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
227
servers/housecall-pro/src/tools/estimates-tools.ts
Normal file
227
servers/housecall-pro/src/tools/estimates-tools.ts
Normal file
@ -0,0 +1,227 @@
|
||||
/**
|
||||
* Estimate Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Estimate, CreateEstimateRequest } from '../types/index.js';
|
||||
|
||||
export function registerEstimatesTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_estimates: {
|
||||
description: 'List estimates with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by customer ID',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['draft', 'sent', 'approved', 'declined', 'expired'],
|
||||
description: 'Filter by status',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const estimates = await client.listEstimates(params);
|
||||
return { estimates, count: estimates.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_estimate: {
|
||||
description: 'Get detailed information about a specific estimate',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: { estimate_id: string }) => {
|
||||
const estimate = await client.getEstimate(params.estimate_id);
|
||||
return estimate;
|
||||
},
|
||||
},
|
||||
|
||||
create_estimate: {
|
||||
description: 'Create a new estimate',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
address_id: {
|
||||
type: 'string',
|
||||
description: 'Service address ID',
|
||||
},
|
||||
line_items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
quantity: { type: 'number' },
|
||||
unit_price: { type: 'number' },
|
||||
},
|
||||
required: ['name', 'quantity', 'unit_price'],
|
||||
},
|
||||
description: 'Array of line items',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Estimate notes',
|
||||
},
|
||||
valid_until: {
|
||||
type: 'string',
|
||||
description: 'Estimate expiration date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: CreateEstimateRequest) => {
|
||||
const estimate = await client.createEstimate(params);
|
||||
return estimate;
|
||||
},
|
||||
},
|
||||
|
||||
update_estimate: {
|
||||
description: 'Update an existing estimate',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
line_items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
quantity: { type: 'number' },
|
||||
unit_price: { type: 'number' },
|
||||
},
|
||||
},
|
||||
description: 'Array of line items',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Estimate notes',
|
||||
},
|
||||
valid_until: {
|
||||
type: 'string',
|
||||
description: 'Estimate expiration date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { estimate_id, ...updateData } = params;
|
||||
const estimate = await client.updateEstimate(estimate_id, updateData);
|
||||
return estimate;
|
||||
},
|
||||
},
|
||||
|
||||
send_estimate: {
|
||||
description: 'Send an estimate to the customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Override customer email',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Custom message to include',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { estimate_id, ...options } = params;
|
||||
const result = await client.sendEstimate(estimate_id, options);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
approve_estimate: {
|
||||
description: 'Mark an estimate as approved',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: { estimate_id: string }) => {
|
||||
const result = await client.approveEstimate(params.estimate_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
decline_estimate: {
|
||||
description: 'Mark an estimate as declined',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Reason for declining',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: { estimate_id: string; reason?: string }) => {
|
||||
const result = await client.declineEstimate(params.estimate_id, params.reason);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
convert_estimate_to_job: {
|
||||
description: 'Convert an approved estimate into a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
estimate_id: {
|
||||
type: 'string',
|
||||
description: 'Estimate ID',
|
||||
},
|
||||
},
|
||||
required: ['estimate_id'],
|
||||
},
|
||||
handler: async (params: { estimate_id: string }) => {
|
||||
const job = await client.convertEstimateToJob(params.estimate_id);
|
||||
return job;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
30
servers/housecall-pro/src/tools/index.ts
Normal file
30
servers/housecall-pro/src/tools/index.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Tools Registry
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { registerJobsTools } from './jobs-tools.js';
|
||||
import { registerCustomersTools } from './customers-tools.js';
|
||||
import { registerEstimatesTools } from './estimates-tools.js';
|
||||
import { registerInvoicesTools } from './invoices-tools.js';
|
||||
import { registerEmployeesTools } from './employees-tools.js';
|
||||
import { registerDispatchTools } from './dispatch-tools.js';
|
||||
import { registerTagsTools } from './tags-tools.js';
|
||||
import { registerNotificationsTools } from './notifications-tools.js';
|
||||
import { registerReviewsTools } from './reviews-tools.js';
|
||||
import { registerReportingTools } from './reporting-tools.js';
|
||||
|
||||
export function registerAllTools(client: HousecallProClient) {
|
||||
return {
|
||||
...registerJobsTools(client),
|
||||
...registerCustomersTools(client),
|
||||
...registerEstimatesTools(client),
|
||||
...registerInvoicesTools(client),
|
||||
...registerEmployeesTools(client),
|
||||
...registerDispatchTools(client),
|
||||
...registerTagsTools(client),
|
||||
...registerNotificationsTools(client),
|
||||
...registerReviewsTools(client),
|
||||
...registerReportingTools(client),
|
||||
};
|
||||
}
|
||||
187
servers/housecall-pro/src/tools/invoices-tools.ts
Normal file
187
servers/housecall-pro/src/tools/invoices-tools.ts
Normal file
@ -0,0 +1,187 @@
|
||||
/**
|
||||
* Invoice Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Invoice, CreateInvoiceRequest, Payment } from '../types/index.js';
|
||||
|
||||
export function registerInvoicesTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_invoices: {
|
||||
description: 'List invoices with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by customer ID',
|
||||
},
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by job ID',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['draft', 'sent', 'viewed', 'partial', 'paid', 'overdue'],
|
||||
description: 'Filter by status',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Filter invoices created on or after this date (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'Filter invoices created on or before this date (ISO 8601)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const invoices = await client.listInvoices(params);
|
||||
return { invoices, count: invoices.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_invoice: {
|
||||
description: 'Get detailed information about a specific invoice',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'string',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (params: { invoice_id: string }) => {
|
||||
const invoice = await client.getInvoice(params.invoice_id);
|
||||
return invoice;
|
||||
},
|
||||
},
|
||||
|
||||
create_invoice: {
|
||||
description: 'Create a new invoice',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
line_items: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
description: { type: 'string' },
|
||||
quantity: { type: 'number' },
|
||||
unit_price: { type: 'number' },
|
||||
},
|
||||
required: ['name', 'quantity', 'unit_price'],
|
||||
},
|
||||
description: 'Array of line items',
|
||||
},
|
||||
due_date: {
|
||||
type: 'string',
|
||||
description: 'Due date (ISO 8601)',
|
||||
},
|
||||
send_immediately: {
|
||||
type: 'boolean',
|
||||
description: 'Send the invoice immediately after creation',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: CreateInvoiceRequest) => {
|
||||
const invoice = await client.createInvoice(params);
|
||||
return invoice;
|
||||
},
|
||||
},
|
||||
|
||||
send_invoice: {
|
||||
description: 'Send an invoice to the customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'string',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
description: 'Override customer email',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Custom message to include',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { invoice_id, ...options } = params;
|
||||
const result = await client.sendInvoice(invoice_id, options);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
mark_invoice_paid: {
|
||||
description: 'Record a payment for an invoice',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'string',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
amount: {
|
||||
type: 'number',
|
||||
description: 'Payment amount',
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
enum: ['cash', 'check', 'card', 'ach', 'other'],
|
||||
description: 'Payment method',
|
||||
},
|
||||
reference: {
|
||||
type: 'string',
|
||||
description: 'Payment reference (check number, transaction ID, etc.)',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id', 'amount', 'method'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { invoice_id, ...paymentData } = params;
|
||||
const payment = await client.markInvoicePaid(invoice_id, paymentData);
|
||||
return payment;
|
||||
},
|
||||
},
|
||||
|
||||
list_invoice_payments: {
|
||||
description: 'List all payments for an invoice',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
invoice_id: {
|
||||
type: 'string',
|
||||
description: 'Invoice ID',
|
||||
},
|
||||
},
|
||||
required: ['invoice_id'],
|
||||
},
|
||||
handler: async (params: { invoice_id: string }) => {
|
||||
const payments = await client.listInvoicePayments(params.invoice_id);
|
||||
return payments;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
346
servers/housecall-pro/src/tools/jobs-tools.ts
Normal file
346
servers/housecall-pro/src/tools/jobs-tools.ts
Normal file
@ -0,0 +1,346 @@
|
||||
/**
|
||||
* Job Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Job, CreateJobRequest } from '../types/index.js';
|
||||
|
||||
export function registerJobsTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_jobs: {
|
||||
description: 'List jobs with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by customer ID',
|
||||
},
|
||||
work_status: {
|
||||
type: 'string',
|
||||
enum: ['scheduled', 'on_my_way', 'working', 'completed', 'cancelled'],
|
||||
description: 'Filter by work status',
|
||||
},
|
||||
invoice_status: {
|
||||
type: 'string',
|
||||
enum: ['not_invoiced', 'invoiced', 'paid', 'partial'],
|
||||
description: 'Filter by invoice status',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Filter jobs scheduled on or after this date (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'Filter jobs scheduled on or before this date (ISO 8601)',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Filter by tags',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const jobs = await client.listJobs(params);
|
||||
return { jobs, count: jobs.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_job: {
|
||||
description: 'Get detailed information about a specific job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string }) => {
|
||||
const job = await client.getJob(params.job_id);
|
||||
return job;
|
||||
},
|
||||
},
|
||||
|
||||
create_job: {
|
||||
description: 'Create a new job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
address_id: {
|
||||
type: 'string',
|
||||
description: 'Service address ID',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Job description',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Internal notes',
|
||||
},
|
||||
schedule_start: {
|
||||
type: 'string',
|
||||
description: 'Scheduled start time (ISO 8601)',
|
||||
},
|
||||
schedule_end: {
|
||||
type: 'string',
|
||||
description: 'Scheduled end time (ISO 8601)',
|
||||
},
|
||||
arrival_window: {
|
||||
type: 'string',
|
||||
description: 'Arrival window (e.g., "8am-12pm")',
|
||||
},
|
||||
assigned_employees: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of employee IDs to assign',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tag IDs',
|
||||
},
|
||||
},
|
||||
required: ['customer_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const jobData: CreateJobRequest = {
|
||||
customer_id: params.customer_id,
|
||||
address_id: params.address_id,
|
||||
description: params.description,
|
||||
notes: params.notes,
|
||||
assigned_employees: params.assigned_employees,
|
||||
tags: params.tags,
|
||||
};
|
||||
|
||||
if (params.schedule_start && params.schedule_end) {
|
||||
jobData.schedule = {
|
||||
start: params.schedule_start,
|
||||
end: params.schedule_end,
|
||||
arrival_window: params.arrival_window,
|
||||
};
|
||||
}
|
||||
|
||||
const job = await client.createJob(jobData);
|
||||
return job;
|
||||
},
|
||||
},
|
||||
|
||||
update_job: {
|
||||
description: 'Update an existing job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Job description',
|
||||
},
|
||||
notes: {
|
||||
type: 'string',
|
||||
description: 'Internal notes',
|
||||
},
|
||||
work_status: {
|
||||
type: 'string',
|
||||
enum: ['scheduled', 'on_my_way', 'working', 'completed', 'cancelled'],
|
||||
description: 'Work status',
|
||||
},
|
||||
assigned_employees: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of employee IDs to assign',
|
||||
},
|
||||
tags: {
|
||||
type: 'array',
|
||||
items: { type: 'string' },
|
||||
description: 'Array of tag IDs',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { job_id, ...updateData } = params;
|
||||
const job = await client.updateJob(job_id, updateData);
|
||||
return job;
|
||||
},
|
||||
},
|
||||
|
||||
complete_job: {
|
||||
description: 'Mark a job as completed',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string }) => {
|
||||
const result = await client.completeJob(params.job_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
cancel_job: {
|
||||
description: 'Cancel a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
reason: {
|
||||
type: 'string',
|
||||
description: 'Cancellation reason',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string; reason?: string }) => {
|
||||
const result = await client.cancelJob(params.job_id, params.reason);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
list_job_line_items: {
|
||||
description: 'List line items for a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string }) => {
|
||||
const lineItems = await client.listJobLineItems(params.job_id);
|
||||
return lineItems;
|
||||
},
|
||||
},
|
||||
|
||||
add_job_line_item: {
|
||||
description: 'Add a line item to a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Line item name',
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: 'Line item description',
|
||||
},
|
||||
quantity: {
|
||||
type: 'number',
|
||||
description: 'Quantity',
|
||||
},
|
||||
unit_price: {
|
||||
type: 'number',
|
||||
description: 'Unit price',
|
||||
},
|
||||
unit: {
|
||||
type: 'string',
|
||||
description: 'Unit of measurement (e.g., "hours", "each")',
|
||||
},
|
||||
},
|
||||
required: ['job_id', 'name', 'quantity', 'unit_price'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { job_id, ...lineItemData } = params;
|
||||
const lineItem = await client.addJobLineItem(job_id, lineItemData);
|
||||
return lineItem;
|
||||
},
|
||||
},
|
||||
|
||||
schedule_job: {
|
||||
description: 'Schedule a job (set initial schedule)',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
description: 'Scheduled start time (ISO 8601)',
|
||||
},
|
||||
end: {
|
||||
type: 'string',
|
||||
description: 'Scheduled end time (ISO 8601)',
|
||||
},
|
||||
arrival_window: {
|
||||
type: 'string',
|
||||
description: 'Arrival window (e.g., "8am-12pm")',
|
||||
},
|
||||
},
|
||||
required: ['job_id', 'start', 'end'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { job_id, ...schedule } = params;
|
||||
const result = await client.scheduleJob(job_id, schedule);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
reschedule_job: {
|
||||
description: 'Reschedule an existing job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
start: {
|
||||
type: 'string',
|
||||
description: 'New scheduled start time (ISO 8601)',
|
||||
},
|
||||
end: {
|
||||
type: 'string',
|
||||
description: 'New scheduled end time (ISO 8601)',
|
||||
},
|
||||
arrival_window: {
|
||||
type: 'string',
|
||||
description: 'New arrival window (e.g., "8am-12pm")',
|
||||
},
|
||||
},
|
||||
required: ['job_id', 'start', 'end'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { job_id, ...schedule } = params;
|
||||
const result = await client.rescheduleJob(job_id, schedule);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
90
servers/housecall-pro/src/tools/notifications-tools.ts
Normal file
90
servers/housecall-pro/src/tools/notifications-tools.ts
Normal file
@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Notification Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Notification } from '../types/index.js';
|
||||
|
||||
export function registerNotificationsTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_notifications: {
|
||||
description: 'List notifications with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['sms', 'email', 'push'],
|
||||
description: 'Filter by notification type',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['queued', 'sent', 'delivered', 'failed'],
|
||||
description: 'Filter by status',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const notifications = await client.listNotifications(params);
|
||||
return { notifications, count: notifications.length };
|
||||
},
|
||||
},
|
||||
|
||||
send_notification: {
|
||||
description: 'Send a notification to a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
enum: ['sms', 'email'],
|
||||
description: 'Notification type',
|
||||
},
|
||||
recipient: {
|
||||
type: 'string',
|
||||
description: 'Recipient (phone number for SMS, email address for email)',
|
||||
},
|
||||
subject: {
|
||||
type: 'string',
|
||||
description: 'Subject (email only)',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Message content',
|
||||
},
|
||||
},
|
||||
required: ['type', 'recipient', 'message'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const notification = await client.sendNotification(params);
|
||||
return notification;
|
||||
},
|
||||
},
|
||||
|
||||
mark_notification_read: {
|
||||
description: 'Mark a notification as read',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
notification_id: {
|
||||
type: 'string',
|
||||
description: 'Notification ID',
|
||||
},
|
||||
},
|
||||
required: ['notification_id'],
|
||||
},
|
||||
handler: async (params: { notification_id: string }) => {
|
||||
const result = await client.markNotificationRead(params.notification_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
85
servers/housecall-pro/src/tools/reporting-tools.ts
Normal file
85
servers/housecall-pro/src/tools/reporting-tools.ts
Normal file
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* Reporting and Analytics Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { RevenueReport, JobCompletionReport, EmployeePerformanceReport } from '../types/index.js';
|
||||
|
||||
export function registerReportingTools(client: HousecallProClient) {
|
||||
return {
|
||||
get_revenue_report: {
|
||||
description: 'Get revenue report for a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
group_by: {
|
||||
type: 'string',
|
||||
enum: ['day', 'week', 'month'],
|
||||
description: 'Group results by time period',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const report = await client.getRevenueReport(params);
|
||||
return report;
|
||||
},
|
||||
},
|
||||
|
||||
get_job_completion_report: {
|
||||
description: 'Get job completion statistics for a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const report = await client.getJobCompletionReport(params);
|
||||
return report;
|
||||
},
|
||||
},
|
||||
|
||||
get_employee_performance_report: {
|
||||
description: 'Get employee performance metrics for a date range',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
employee_id: {
|
||||
type: 'string',
|
||||
description: 'Employee ID (optional - leave blank for all employees)',
|
||||
},
|
||||
start_date: {
|
||||
type: 'string',
|
||||
description: 'Start date (ISO 8601)',
|
||||
},
|
||||
end_date: {
|
||||
type: 'string',
|
||||
description: 'End date (ISO 8601)',
|
||||
},
|
||||
},
|
||||
required: ['start_date', 'end_date'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const report = await client.getEmployeePerformanceReport(params);
|
||||
return report;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
89
servers/housecall-pro/src/tools/reviews-tools.ts
Normal file
89
servers/housecall-pro/src/tools/reviews-tools.ts
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Review Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Review } from '../types/index.js';
|
||||
|
||||
export function registerReviewsTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_reviews: {
|
||||
description: 'List reviews with optional filters',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by customer ID',
|
||||
},
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Filter by job ID',
|
||||
},
|
||||
rating: {
|
||||
type: 'number',
|
||||
description: 'Filter by rating (1-5)',
|
||||
},
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const reviews = await client.listReviews(params);
|
||||
return { reviews, count: reviews.length };
|
||||
},
|
||||
},
|
||||
|
||||
get_review: {
|
||||
description: 'Get detailed information about a specific review',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
review_id: {
|
||||
type: 'string',
|
||||
description: 'Review ID',
|
||||
},
|
||||
},
|
||||
required: ['review_id'],
|
||||
},
|
||||
handler: async (params: { review_id: string }) => {
|
||||
const review = await client.getReview(params.review_id);
|
||||
return review;
|
||||
},
|
||||
},
|
||||
|
||||
request_review: {
|
||||
description: 'Request a review from a customer for a completed job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
method: {
|
||||
type: 'string',
|
||||
enum: ['sms', 'email'],
|
||||
description: 'Review request method',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Custom message to include with the review request',
|
||||
},
|
||||
},
|
||||
required: ['job_id'],
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const { job_id, ...options } = params;
|
||||
const result = await client.requestReview(job_id, options);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
115
servers/housecall-pro/src/tools/tags-tools.ts
Normal file
115
servers/housecall-pro/src/tools/tags-tools.ts
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* Tag Management Tools
|
||||
*/
|
||||
|
||||
import { HousecallProClient } from '../clients/housecall-pro.js';
|
||||
import { Tag } from '../types/index.js';
|
||||
|
||||
export function registerTagsTools(client: HousecallProClient) {
|
||||
return {
|
||||
list_tags: {
|
||||
description: 'List all tags',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
page: {
|
||||
type: 'number',
|
||||
description: 'Page number (default: 1)',
|
||||
},
|
||||
page_size: {
|
||||
type: 'number',
|
||||
description: 'Items per page (default: 50)',
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: async (params: any) => {
|
||||
const tags = await client.listTags(params);
|
||||
return { tags, count: tags.length };
|
||||
},
|
||||
},
|
||||
|
||||
create_tag: {
|
||||
description: 'Create a new tag',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Tag name',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
description: 'Tag color (hex code)',
|
||||
},
|
||||
},
|
||||
required: ['name'],
|
||||
},
|
||||
handler: async (params: { name: string; color?: string }) => {
|
||||
const tag = await client.createTag(params);
|
||||
return tag;
|
||||
},
|
||||
},
|
||||
|
||||
delete_tag: {
|
||||
description: 'Delete a tag',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
tag_id: {
|
||||
type: 'string',
|
||||
description: 'Tag ID',
|
||||
},
|
||||
},
|
||||
required: ['tag_id'],
|
||||
},
|
||||
handler: async (params: { tag_id: string }) => {
|
||||
const result = await client.deleteTag(params.tag_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
add_tag_to_job: {
|
||||
description: 'Add a tag to a job',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
job_id: {
|
||||
type: 'string',
|
||||
description: 'Job ID',
|
||||
},
|
||||
tag_id: {
|
||||
type: 'string',
|
||||
description: 'Tag ID',
|
||||
},
|
||||
},
|
||||
required: ['job_id', 'tag_id'],
|
||||
},
|
||||
handler: async (params: { job_id: string; tag_id: string }) => {
|
||||
const result = await client.addTagToJob(params.job_id, params.tag_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
|
||||
add_tag_to_customer: {
|
||||
description: 'Add a tag to a customer',
|
||||
parameters: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
customer_id: {
|
||||
type: 'string',
|
||||
description: 'Customer ID',
|
||||
},
|
||||
tag_id: {
|
||||
type: 'string',
|
||||
description: 'Tag ID',
|
||||
},
|
||||
},
|
||||
required: ['customer_id', 'tag_id'],
|
||||
},
|
||||
handler: async (params: { customer_id: string; tag_id: string }) => {
|
||||
const result = await client.addTagToCustomer(params.customer_id, params.tag_id);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
105
servers/housecall-pro/src/ui/react-app/customer-detail.tsx
Normal file
105
servers/housecall-pro/src/ui/react-app/customer-detail.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* Customer Detail - Customer profile view
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface CustomerDetailProps {
|
||||
api: any;
|
||||
customerId: string;
|
||||
}
|
||||
|
||||
export function CustomerDetail({ api, customerId }: CustomerDetailProps) {
|
||||
const [customer, setCustomer] = useState<any>(null);
|
||||
const [addresses, setAddresses] = useState<any[]>([]);
|
||||
const [jobs, setJobs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadCustomer();
|
||||
}, [customerId]);
|
||||
|
||||
const loadCustomer = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const customerResult = await api.call('get_customer', { customer_id: customerId });
|
||||
setCustomer(JSON.parse(customerResult));
|
||||
|
||||
const addressesResult = await api.call('list_customer_addresses', { customer_id: customerId });
|
||||
setAddresses(JSON.parse(addressesResult));
|
||||
|
||||
const jobsResult = await api.call('list_jobs', { customer_id: customerId, page_size: 10 });
|
||||
setJobs(JSON.parse(jobsResult).jobs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load customer:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) return <div className="loading">Loading customer...</div>;
|
||||
if (!customer) return <div className="error">Customer not found</div>;
|
||||
|
||||
return (
|
||||
<div className="customer-detail">
|
||||
<h1>{customer.first_name} {customer.last_name}</h1>
|
||||
|
||||
<div className="customer-info">
|
||||
<div className="section">
|
||||
<h2>Contact Information</h2>
|
||||
<p><strong>Email:</strong> {customer.email || 'N/A'}</p>
|
||||
<p><strong>Mobile:</strong> {customer.mobile_number || 'N/A'}</p>
|
||||
<p><strong>Home:</strong> {customer.home_number || 'N/A'}</p>
|
||||
<p><strong>Work:</strong> {customer.work_number || 'N/A'}</p>
|
||||
{customer.company && <p><strong>Company:</strong> {customer.company}</p>}
|
||||
</div>
|
||||
|
||||
<div className="section">
|
||||
<h2>Details</h2>
|
||||
<p><strong>Lead Source:</strong> {customer.lead_source || 'N/A'}</p>
|
||||
<p><strong>Notifications:</strong> {customer.notifications_enabled ? 'Enabled' : 'Disabled'}</p>
|
||||
{customer.tags && customer.tags.length > 0 && (
|
||||
<p><strong>Tags:</strong> {customer.tags.join(', ')}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="addresses">
|
||||
<h2>Addresses</h2>
|
||||
{addresses.map((addr, idx) => (
|
||||
<div key={idx} className="address-card">
|
||||
<p><strong>{addr.type || 'Address'}:</strong></p>
|
||||
<p>{addr.street}</p>
|
||||
{addr.street_line_2 && <p>{addr.street_line_2}</p>}
|
||||
<p>{addr.city}, {addr.state} {addr.zip}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="recent-jobs">
|
||||
<h2>Recent Jobs</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs.map((job) => (
|
||||
<tr key={job.id}>
|
||||
<td>{job.id}</td>
|
||||
<td>{job.description || 'N/A'}</td>
|
||||
<td><span className={`badge ${job.work_status}`}>{job.work_status}</span></td>
|
||||
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleDateString() : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
77
servers/housecall-pro/src/ui/react-app/customer-grid.tsx
Normal file
77
servers/housecall-pro/src/ui/react-app/customer-grid.tsx
Normal file
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Customer Grid - Customer list/grid view
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface CustomerGridProps {
|
||||
api: any;
|
||||
}
|
||||
|
||||
export function CustomerGrid({ api }: CustomerGridProps) {
|
||||
const [customers, setCustomers] = useState<any[]>([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadCustomers();
|
||||
}, [search]);
|
||||
|
||||
const loadCustomers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params = search ? { search } : {};
|
||||
const result = await api.call('list_customers', params);
|
||||
setCustomers(JSON.parse(result).customers);
|
||||
} catch (error) {
|
||||
console.error('Failed to load customers:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="customer-grid">
|
||||
<h1>Customers</h1>
|
||||
|
||||
<div className="search-bar">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search by name, email, or phone..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">Loading customers...</div>
|
||||
) : (
|
||||
<table className="data-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Email</th>
|
||||
<th>Phone</th>
|
||||
<th>Company</th>
|
||||
<th>Lead Source</th>
|
||||
<th>Tags</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{customers.map((customer) => (
|
||||
<tr key={customer.id}>
|
||||
<td>{customer.first_name} {customer.last_name}</td>
|
||||
<td>{customer.email || '-'}</td>
|
||||
<td>{customer.mobile_number || customer.home_number || '-'}</td>
|
||||
<td>{customer.company || '-'}</td>
|
||||
<td>{customer.lead_source || '-'}</td>
|
||||
<td>{customer.tags?.join(', ') || '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
34
servers/housecall-pro/src/ui/react-app/dispatch-board.tsx
Normal file
34
servers/housecall-pro/src/ui/react-app/dispatch-board.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Dispatch Board - Visual dispatch/scheduling board
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function DispatchBoard({ api }: { api: any }) {
|
||||
const [board, setBoard] = useState<any>(null);
|
||||
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
|
||||
|
||||
useEffect(() => {
|
||||
loadBoard();
|
||||
}, [date]);
|
||||
|
||||
const loadBoard = async () => {
|
||||
const result = await api.call('get_dispatch_board', { date });
|
||||
setBoard(JSON.parse(result));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dispatch-board">
|
||||
<h1>Dispatch Board</h1>
|
||||
<input type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
{board ? (
|
||||
<div className="board-view">
|
||||
{/* Board visualization would go here */}
|
||||
<pre>{JSON.stringify(board, null, 2)}</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div>Loading...</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Employee Performance - Performance metrics dashboard
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function EmployeePerformance({ api }: { api: any }) {
|
||||
const [report, setReport] = useState<any>(null);
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) loadReport();
|
||||
}, [startDate, endDate]);
|
||||
|
||||
const loadReport = async () => {
|
||||
const result = await api.call('get_employee_performance_report', {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
setReport(JSON.parse(result));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="employee-performance">
|
||||
<h1>Employee Performance</h1>
|
||||
<div className="date-range">
|
||||
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
||||
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
||||
</div>
|
||||
{report && (
|
||||
<div className="metrics">
|
||||
<div className="metric">
|
||||
<h3>Jobs Completed</h3>
|
||||
<p>{report.jobs_completed}</p>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<h3>Total Revenue</h3>
|
||||
<p>${report.total_revenue?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<h3>Hours Worked</h3>
|
||||
<p>{report.hours_worked}</p>
|
||||
</div>
|
||||
<div className="metric">
|
||||
<h3>Average Rating</h3>
|
||||
<p>{report.average_rating?.toFixed(1)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
35
servers/housecall-pro/src/ui/react-app/employee-schedule.tsx
Normal file
35
servers/housecall-pro/src/ui/react-app/employee-schedule.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Employee Schedule - Calendar view of employee schedules
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function EmployeeSchedule({ api, employeeId }: { api: any; employeeId: string }) {
|
||||
const [schedule, setSchedule] = useState<any>(null);
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) loadSchedule();
|
||||
}, [employeeId, startDate, endDate]);
|
||||
|
||||
const loadSchedule = async () => {
|
||||
const result = await api.call('get_employee_schedule', {
|
||||
employee_id: employeeId,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
});
|
||||
setSchedule(JSON.parse(result));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="employee-schedule">
|
||||
<h1>Employee Schedule</h1>
|
||||
<div className="date-range">
|
||||
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
||||
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
||||
</div>
|
||||
{schedule && <pre>{JSON.stringify(schedule, null, 2)}</pre>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
124
servers/housecall-pro/src/ui/react-app/estimate-builder.tsx
Normal file
124
servers/housecall-pro/src/ui/react-app/estimate-builder.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
/**
|
||||
* Estimate Builder - Interactive estimate creation
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface EstimateBuilderProps {
|
||||
api: any;
|
||||
}
|
||||
|
||||
export function EstimateBuilder({ api }: EstimateBuilderProps) {
|
||||
const [customerId, setCustomerId] = useState('');
|
||||
const [lineItems, setLineItems] = useState<any[]>([]);
|
||||
const [notes, setNotes] = useState('');
|
||||
const [validUntil, setValidUntil] = useState('');
|
||||
|
||||
const addLineItem = () => {
|
||||
setLineItems([...lineItems, { name: '', description: '', quantity: 1, unit_price: 0 }]);
|
||||
};
|
||||
|
||||
const updateLineItem = (index: number, field: string, value: any) => {
|
||||
const updated = [...lineItems];
|
||||
updated[index][field] = value;
|
||||
setLineItems(updated);
|
||||
};
|
||||
|
||||
const removeLineItem = (index: number) => {
|
||||
setLineItems(lineItems.filter((_, i) => i !== index));
|
||||
};
|
||||
|
||||
const calculateTotal = () => {
|
||||
return lineItems.reduce((sum, item) => sum + (item.quantity * item.unit_price), 0);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await api.call('create_estimate', {
|
||||
customer_id: customerId,
|
||||
line_items: lineItems,
|
||||
notes,
|
||||
valid_until: validUntil,
|
||||
});
|
||||
alert('Estimate created successfully!');
|
||||
} catch (error) {
|
||||
alert('Failed to create estimate');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="estimate-builder">
|
||||
<h1>Create Estimate</h1>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Customer ID:</label>
|
||||
<input
|
||||
type="text"
|
||||
value={customerId}
|
||||
onChange={(e) => setCustomerId(e.target.value)}
|
||||
placeholder="Enter customer ID"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="line-items">
|
||||
<h2>Line Items</h2>
|
||||
{lineItems.map((item, index) => (
|
||||
<div key={index} className="line-item">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Name"
|
||||
value={item.name}
|
||||
onChange={(e) => updateLineItem(index, 'name', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Description"
|
||||
value={item.description}
|
||||
onChange={(e) => updateLineItem(index, 'description', e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Qty"
|
||||
value={item.quantity}
|
||||
onChange={(e) => updateLineItem(index, 'quantity', parseFloat(e.target.value))}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Unit Price"
|
||||
value={item.unit_price}
|
||||
onChange={(e) => updateLineItem(index, 'unit_price', parseFloat(e.target.value))}
|
||||
/>
|
||||
<span className="item-total">${(item.quantity * item.unit_price).toFixed(2)}</span>
|
||||
<button onClick={() => removeLineItem(index)}>Remove</button>
|
||||
</div>
|
||||
))}
|
||||
<button onClick={addLineItem}>+ Add Line Item</button>
|
||||
</div>
|
||||
|
||||
<div className="total">
|
||||
<h3>Total: ${calculateTotal().toFixed(2)}</h3>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Notes:</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={4}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Valid Until:</label>
|
||||
<input
|
||||
type="date"
|
||||
value={validUntil}
|
||||
onChange={(e) => setValidUntil(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button onClick={handleSubmit} className="btn-primary">Create Estimate</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
servers/housecall-pro/src/ui/react-app/estimate-grid.tsx
Normal file
55
servers/housecall-pro/src/ui/react-app/estimate-grid.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* Estimate Grid - List/grid view of estimates
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function EstimateGrid({ api }: { api: any }) {
|
||||
const [estimates, setEstimates] = useState<any[]>([]);
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadEstimates();
|
||||
}, [status]);
|
||||
|
||||
const loadEstimates = async () => {
|
||||
const params = status ? { status } : {};
|
||||
const result = await api.call('list_estimates', params);
|
||||
setEstimates(JSON.parse(result).estimates);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="estimate-grid">
|
||||
<h1>Estimates</h1>
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<option value="">All</option>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="approved">Approved</option>
|
||||
<option value="declined">Declined</option>
|
||||
</select>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Valid Until</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{estimates.map((est) => (
|
||||
<tr key={est.id}>
|
||||
<td>{est.id}</td>
|
||||
<td>{est.customer_id}</td>
|
||||
<td><span className={`badge ${est.status}`}>{est.status}</span></td>
|
||||
<td>${est.total.toFixed(2)}</td>
|
||||
<td>{est.valid_until ? new Date(est.valid_until).toLocaleDateString() : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
134
servers/housecall-pro/src/ui/react-app/index.tsx
Normal file
134
servers/housecall-pro/src/ui/react-app/index.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
/**
|
||||
* Housecall Pro MCP Apps - Index
|
||||
*
|
||||
* 16 React-based MCP apps for rich UI interactions
|
||||
*/
|
||||
|
||||
export { JobDashboard } from './job-dashboard';
|
||||
export { JobDetail } from './job-detail';
|
||||
export { JobGrid } from './job-grid';
|
||||
export { CustomerDetail } from './customer-detail';
|
||||
export { CustomerGrid } from './customer-grid';
|
||||
export { EstimateBuilder } from './estimate-builder';
|
||||
export { EstimateGrid } from './estimate-grid';
|
||||
export { InvoiceDashboard } from './invoice-dashboard';
|
||||
export { InvoiceDetail } from './invoice-detail';
|
||||
export { DispatchBoard } from './dispatch-board';
|
||||
export { EmployeeSchedule } from './employee-schedule';
|
||||
export { EmployeePerformance } from './employee-performance';
|
||||
export { ReviewDashboard } from './review-dashboard';
|
||||
export { RevenueDashboard } from './revenue-dashboard';
|
||||
export { TagManager } from './tag-manager';
|
||||
export { NotificationCenter } from './notification-center';
|
||||
|
||||
/**
|
||||
* App Registry - Maps app names to components
|
||||
*/
|
||||
export const MCP_APPS = {
|
||||
'job-dashboard': JobDashboard,
|
||||
'job-detail': JobDetail,
|
||||
'job-grid': JobGrid,
|
||||
'customer-detail': CustomerDetail,
|
||||
'customer-grid': CustomerGrid,
|
||||
'estimate-builder': EstimateBuilder,
|
||||
'estimate-grid': EstimateGrid,
|
||||
'invoice-dashboard': InvoiceDashboard,
|
||||
'invoice-detail': InvoiceDetail,
|
||||
'dispatch-board': DispatchBoard,
|
||||
'employee-schedule': EmployeeSchedule,
|
||||
'employee-performance': EmployeePerformance,
|
||||
'review-dashboard': ReviewDashboard,
|
||||
'revenue-dashboard': RevenueDashboard,
|
||||
'tag-manager': TagManager,
|
||||
'notification-center': NotificationCenter,
|
||||
};
|
||||
|
||||
/**
|
||||
* App Metadata
|
||||
*/
|
||||
export const APP_METADATA = {
|
||||
'job-dashboard': {
|
||||
name: 'Job Dashboard',
|
||||
description: 'Overview of all jobs with stats',
|
||||
category: 'Jobs',
|
||||
},
|
||||
'job-detail': {
|
||||
name: 'Job Detail',
|
||||
description: 'Detailed view of a single job',
|
||||
category: 'Jobs',
|
||||
params: ['jobId'],
|
||||
},
|
||||
'job-grid': {
|
||||
name: 'Job Grid',
|
||||
description: 'Filterable grid view of jobs',
|
||||
category: 'Jobs',
|
||||
},
|
||||
'customer-detail': {
|
||||
name: 'Customer Detail',
|
||||
description: 'Customer profile and history',
|
||||
category: 'Customers',
|
||||
params: ['customerId'],
|
||||
},
|
||||
'customer-grid': {
|
||||
name: 'Customer Grid',
|
||||
description: 'Searchable customer list',
|
||||
category: 'Customers',
|
||||
},
|
||||
'estimate-builder': {
|
||||
name: 'Estimate Builder',
|
||||
description: 'Interactive estimate creation',
|
||||
category: 'Estimates',
|
||||
},
|
||||
'estimate-grid': {
|
||||
name: 'Estimate Grid',
|
||||
description: 'List and manage estimates',
|
||||
category: 'Estimates',
|
||||
},
|
||||
'invoice-dashboard': {
|
||||
name: 'Invoice Dashboard',
|
||||
description: 'Invoice overview and metrics',
|
||||
category: 'Invoices',
|
||||
},
|
||||
'invoice-detail': {
|
||||
name: 'Invoice Detail',
|
||||
description: 'Detailed invoice view with payments',
|
||||
category: 'Invoices',
|
||||
params: ['invoiceId'],
|
||||
},
|
||||
'dispatch-board': {
|
||||
name: 'Dispatch Board',
|
||||
description: 'Visual dispatch and scheduling',
|
||||
category: 'Dispatch',
|
||||
},
|
||||
'employee-schedule': {
|
||||
name: 'Employee Schedule',
|
||||
description: 'Calendar view of employee schedules',
|
||||
category: 'Employees',
|
||||
params: ['employeeId'],
|
||||
},
|
||||
'employee-performance': {
|
||||
name: 'Employee Performance',
|
||||
description: 'Performance metrics dashboard',
|
||||
category: 'Employees',
|
||||
},
|
||||
'review-dashboard': {
|
||||
name: 'Review Dashboard',
|
||||
description: 'Customer reviews management',
|
||||
category: 'Reviews',
|
||||
},
|
||||
'revenue-dashboard': {
|
||||
name: 'Revenue Dashboard',
|
||||
description: 'Revenue analytics and reporting',
|
||||
category: 'Reporting',
|
||||
},
|
||||
'tag-manager': {
|
||||
name: 'Tag Manager',
|
||||
description: 'Tag organization and management',
|
||||
category: 'Settings',
|
||||
},
|
||||
'notification-center': {
|
||||
name: 'Notification Center',
|
||||
description: 'Notification history and management',
|
||||
category: 'Communications',
|
||||
},
|
||||
};
|
||||
72
servers/housecall-pro/src/ui/react-app/invoice-dashboard.tsx
Normal file
72
servers/housecall-pro/src/ui/react-app/invoice-dashboard.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* Invoice Dashboard - Overview of invoices
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function InvoiceDashboard({ api }: { api: any }) {
|
||||
const [stats, setStats] = useState<any>(null);
|
||||
const [invoices, setInvoices] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
const loadDashboard = async () => {
|
||||
const result = await api.call('list_invoices', { page_size: 100 });
|
||||
const data = JSON.parse(result).invoices;
|
||||
|
||||
const stats = {
|
||||
total: data.length,
|
||||
draft: data.filter((i: any) => i.status === 'draft').length,
|
||||
sent: data.filter((i: any) => i.status === 'sent').length,
|
||||
paid: data.filter((i: any) => i.status === 'paid').length,
|
||||
overdue: data.filter((i: any) => i.status === 'overdue').length,
|
||||
totalRevenue: data.reduce((sum: number, i: any) => sum + i.total, 0),
|
||||
totalPaid: data.reduce((sum: number, i: any) => sum + i.amount_paid, 0),
|
||||
};
|
||||
|
||||
setStats(stats);
|
||||
setInvoices(data.slice(0, 10));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="invoice-dashboard">
|
||||
<h1>Invoice Dashboard</h1>
|
||||
{stats && (
|
||||
<div className="stats">
|
||||
<div className="stat"><h3>Total Invoices</h3><p>{stats.total}</p></div>
|
||||
<div className="stat"><h3>Paid</h3><p>{stats.paid}</p></div>
|
||||
<div className="stat"><h3>Overdue</h3><p className="danger">{stats.overdue}</p></div>
|
||||
<div className="stat"><h3>Total Revenue</h3><p>${stats.totalRevenue.toFixed(2)}</p></div>
|
||||
<div className="stat"><h3>Collected</h3><p>${stats.totalPaid.toFixed(2)}</p></div>
|
||||
</div>
|
||||
)}
|
||||
<h2>Recent Invoices</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Customer</th>
|
||||
<th>Status</th>
|
||||
<th>Total</th>
|
||||
<th>Balance</th>
|
||||
<th>Due Date</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoices.map((inv) => (
|
||||
<tr key={inv.id}>
|
||||
<td>{inv.invoice_number}</td>
|
||||
<td>{inv.customer_id}</td>
|
||||
<td><span className={`badge ${inv.status}`}>{inv.status}</span></td>
|
||||
<td>${inv.total.toFixed(2)}</td>
|
||||
<td>${inv.balance.toFixed(2)}</td>
|
||||
<td>{inv.due_date ? new Date(inv.due_date).toLocaleDateString() : '-'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
89
servers/housecall-pro/src/ui/react-app/invoice-detail.tsx
Normal file
89
servers/housecall-pro/src/ui/react-app/invoice-detail.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Invoice Detail - Detailed invoice view
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function InvoiceDetail({ api, invoiceId }: { api: any; invoiceId: string }) {
|
||||
const [invoice, setInvoice] = useState<any>(null);
|
||||
const [payments, setPayments] = useState<any[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
loadInvoice();
|
||||
}, [invoiceId]);
|
||||
|
||||
const loadInvoice = async () => {
|
||||
const invResult = await api.call('get_invoice', { invoice_id: invoiceId });
|
||||
setInvoice(JSON.parse(invResult));
|
||||
|
||||
const payResult = await api.call('list_invoice_payments', { invoice_id: invoiceId });
|
||||
setPayments(JSON.parse(payResult));
|
||||
};
|
||||
|
||||
if (!invoice) return <div>Loading...</div>;
|
||||
|
||||
return (
|
||||
<div className="invoice-detail">
|
||||
<h1>Invoice #{invoice.invoice_number}</h1>
|
||||
<div className="info">
|
||||
<p><strong>Customer:</strong> {invoice.customer_id}</p>
|
||||
<p><strong>Job:</strong> {invoice.job_id}</p>
|
||||
<p><strong>Status:</strong> <span className={`badge ${invoice.status}`}>{invoice.status}</span></p>
|
||||
<p><strong>Due Date:</strong> {invoice.due_date ? new Date(invoice.due_date).toLocaleDateString() : '-'}</p>
|
||||
</div>
|
||||
<h2>Line Items</h2>
|
||||
<table>
|
||||
<tbody>
|
||||
{invoice.line_items?.map((item: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.quantity} × ${item.unit_price.toFixed(2)}</td>
|
||||
<td>${item.total.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
<tr className="total-row">
|
||||
<td colSpan={2}><strong>Subtotal</strong></td>
|
||||
<td>${invoice.subtotal.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="total-row">
|
||||
<td colSpan={2}><strong>Tax</strong></td>
|
||||
<td>${invoice.tax.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="total-row">
|
||||
<td colSpan={2}><strong>Total</strong></td>
|
||||
<td><strong>${invoice.total.toFixed(2)}</strong></td>
|
||||
</tr>
|
||||
<tr className="total-row">
|
||||
<td colSpan={2}><strong>Paid</strong></td>
|
||||
<td>${invoice.amount_paid.toFixed(2)}</td>
|
||||
</tr>
|
||||
<tr className="total-row">
|
||||
<td colSpan={2}><strong>Balance</strong></td>
|
||||
<td className="balance">${invoice.balance.toFixed(2)}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Payments</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Method</th>
|
||||
<th>Reference</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{payments.map((payment) => (
|
||||
<tr key={payment.id}>
|
||||
<td>{new Date(payment.created_at).toLocaleDateString()}</td>
|
||||
<td>{payment.method}</td>
|
||||
<td>{payment.reference || '-'}</td>
|
||||
<td>${payment.amount.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
122
servers/housecall-pro/src/ui/react-app/job-dashboard.tsx
Normal file
122
servers/housecall-pro/src/ui/react-app/job-dashboard.tsx
Normal file
@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Job Dashboard - Overview of all jobs
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface JobDashboardProps {
|
||||
api: any; // MCP API interface
|
||||
}
|
||||
|
||||
interface JobStats {
|
||||
total: number;
|
||||
scheduled: number;
|
||||
inProgress: number;
|
||||
completed: number;
|
||||
cancelled: number;
|
||||
}
|
||||
|
||||
export function JobDashboard({ api }: JobDashboardProps) {
|
||||
const [stats, setStats] = useState<JobStats | null>(null);
|
||||
const [recentJobs, setRecentJobs] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadDashboard();
|
||||
}, []);
|
||||
|
||||
const loadDashboard = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Get jobs from last 30 days
|
||||
const endDate = new Date().toISOString();
|
||||
const startDate = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const result = await api.call('list_jobs', {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
page_size: 100,
|
||||
});
|
||||
|
||||
const jobs = JSON.parse(result).jobs;
|
||||
|
||||
// Calculate stats
|
||||
const stats: JobStats = {
|
||||
total: jobs.length,
|
||||
scheduled: jobs.filter((j: any) => j.work_status === 'scheduled').length,
|
||||
inProgress: jobs.filter((j: any) => ['on_my_way', 'working'].includes(j.work_status)).length,
|
||||
completed: jobs.filter((j: any) => j.work_status === 'completed').length,
|
||||
cancelled: jobs.filter((j: any) => j.work_status === 'cancelled').length,
|
||||
};
|
||||
|
||||
setStats(stats);
|
||||
setRecentJobs(jobs.slice(0, 10));
|
||||
} catch (error) {
|
||||
console.error('Failed to load dashboard:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading dashboard...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="job-dashboard">
|
||||
<h1>Job Dashboard</h1>
|
||||
|
||||
{stats && (
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<h3>Total Jobs</h3>
|
||||
<p className="stat-value">{stats.total}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Scheduled</h3>
|
||||
<p className="stat-value scheduled">{stats.scheduled}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>In Progress</h3>
|
||||
<p className="stat-value in-progress">{stats.inProgress}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Completed</h3>
|
||||
<p className="stat-value completed">{stats.completed}</p>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<h3>Cancelled</h3>
|
||||
<p className="stat-value cancelled">{stats.cancelled}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="recent-jobs">
|
||||
<h2>Recent Jobs</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Description</th>
|
||||
<th>Status</th>
|
||||
<th>Scheduled</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{recentJobs.map((job) => (
|
||||
<tr key={job.id}>
|
||||
<td>{job.id}</td>
|
||||
<td>{job.customer_id}</td>
|
||||
<td>{job.description || 'N/A'}</td>
|
||||
<td className={`status-${job.work_status}`}>{job.work_status}</td>
|
||||
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleString() : 'Unscheduled'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
154
servers/housecall-pro/src/ui/react-app/job-detail.tsx
Normal file
154
servers/housecall-pro/src/ui/react-app/job-detail.tsx
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Job Detail - Detailed view of a single job
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface JobDetailProps {
|
||||
api: any;
|
||||
jobId: string;
|
||||
}
|
||||
|
||||
export function JobDetail({ api, jobId }: JobDetailProps) {
|
||||
const [job, setJob] = useState<any>(null);
|
||||
const [lineItems, setLineItems] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadJob();
|
||||
}, [jobId]);
|
||||
|
||||
const loadJob = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const jobResult = await api.call('get_job', { job_id: jobId });
|
||||
const jobData = JSON.parse(jobResult);
|
||||
setJob(jobData);
|
||||
|
||||
const lineItemsResult = await api.call('list_job_line_items', { job_id: jobId });
|
||||
const lineItemsData = JSON.parse(lineItemsResult);
|
||||
setLineItems(lineItemsData);
|
||||
} catch (error) {
|
||||
console.error('Failed to load job:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleComplete = async () => {
|
||||
if (confirm('Mark this job as completed?')) {
|
||||
await api.call('complete_job', { job_id: jobId });
|
||||
loadJob();
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = async () => {
|
||||
const reason = prompt('Reason for cancellation:');
|
||||
if (reason) {
|
||||
await api.call('cancel_job', { job_id: jobId, reason });
|
||||
loadJob();
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return <div className="loading">Loading job...</div>;
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return <div className="error">Job not found</div>;
|
||||
}
|
||||
|
||||
const total = lineItems.reduce((sum, item) => sum + item.total, 0);
|
||||
|
||||
return (
|
||||
<div className="job-detail">
|
||||
<div className="job-header">
|
||||
<h1>Job #{job.id}</h1>
|
||||
<span className={`status-badge ${job.work_status}`}>{job.work_status}</span>
|
||||
</div>
|
||||
|
||||
<div className="job-info">
|
||||
<div className="info-section">
|
||||
<h2>Customer Information</h2>
|
||||
<p><strong>Customer ID:</strong> {job.customer_id}</p>
|
||||
<p><strong>Address ID:</strong> {job.address_id || 'N/A'}</p>
|
||||
</div>
|
||||
|
||||
<div className="info-section">
|
||||
<h2>Schedule</h2>
|
||||
{job.schedule ? (
|
||||
<>
|
||||
<p><strong>Start:</strong> {new Date(job.schedule.start).toLocaleString()}</p>
|
||||
<p><strong>End:</strong> {new Date(job.schedule.end).toLocaleString()}</p>
|
||||
{job.schedule.arrival_window && (
|
||||
<p><strong>Arrival Window:</strong> {job.schedule.arrival_window}</p>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<p>Not scheduled</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="info-section">
|
||||
<h2>Details</h2>
|
||||
<p><strong>Description:</strong> {job.description || 'N/A'}</p>
|
||||
<p><strong>Notes:</strong> {job.notes || 'N/A'}</p>
|
||||
<p><strong>Invoice Status:</strong> {job.invoice_status}</p>
|
||||
</div>
|
||||
|
||||
{job.assigned_employees && job.assigned_employees.length > 0 && (
|
||||
<div className="info-section">
|
||||
<h2>Assigned Employees</h2>
|
||||
<ul>
|
||||
{job.assigned_employees.map((empId: string) => (
|
||||
<li key={empId}>{empId}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="line-items">
|
||||
<h2>Line Items</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Quantity</th>
|
||||
<th>Unit Price</th>
|
||||
<th>Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{lineItems.map((item, index) => (
|
||||
<tr key={index}>
|
||||
<td>{item.name}</td>
|
||||
<td>{item.description || '-'}</td>
|
||||
<td>{item.quantity}</td>
|
||||
<td>${item.unit_price.toFixed(2)}</td>
|
||||
<td>${item.total.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colSpan={4}><strong>Total</strong></td>
|
||||
<td><strong>${total.toFixed(2)}</strong></td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
{job.work_status !== 'completed' && job.work_status !== 'cancelled' && (
|
||||
<>
|
||||
<button onClick={handleComplete} className="btn-primary">Complete Job</button>
|
||||
<button onClick={handleCancel} className="btn-danger">Cancel Job</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
servers/housecall-pro/src/ui/react-app/job-grid.tsx
Normal file
120
servers/housecall-pro/src/ui/react-app/job-grid.tsx
Normal file
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Job Grid - Filterable grid view of jobs
|
||||
* MCP App for Housecall Pro
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
interface JobGridProps {
|
||||
api: any;
|
||||
}
|
||||
|
||||
export function JobGrid({ api }: JobGridProps) {
|
||||
const [jobs, setJobs] = useState<any[]>([]);
|
||||
const [filters, setFilters] = useState({
|
||||
work_status: '',
|
||||
invoice_status: '',
|
||||
start_date: '',
|
||||
end_date: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadJobs();
|
||||
}, [filters]);
|
||||
|
||||
const loadJobs = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (filters.work_status) params.work_status = filters.work_status;
|
||||
if (filters.invoice_status) params.invoice_status = filters.invoice_status;
|
||||
if (filters.start_date) params.start_date = filters.start_date;
|
||||
if (filters.end_date) params.end_date = filters.end_date;
|
||||
|
||||
const result = await api.call('list_jobs', params);
|
||||
const data = JSON.parse(result);
|
||||
setJobs(data.jobs);
|
||||
} catch (error) {
|
||||
console.error('Failed to load jobs:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="job-grid">
|
||||
<h1>Jobs</h1>
|
||||
|
||||
<div className="filters">
|
||||
<select
|
||||
value={filters.work_status}
|
||||
onChange={(e) => setFilters({...filters, work_status: e.target.value})}
|
||||
>
|
||||
<option value="">All Statuses</option>
|
||||
<option value="scheduled">Scheduled</option>
|
||||
<option value="on_my_way">On My Way</option>
|
||||
<option value="working">Working</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
|
||||
<select
|
||||
value={filters.invoice_status}
|
||||
onChange={(e) => setFilters({...filters, invoice_status: e.target.value})}
|
||||
>
|
||||
<option value="">All Invoice Status</option>
|
||||
<option value="not_invoiced">Not Invoiced</option>
|
||||
<option value="invoiced">Invoiced</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="partial">Partial</option>
|
||||
</select>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
value={filters.start_date}
|
||||
onChange={(e) => setFilters({...filters, start_date: e.target.value})}
|
||||
placeholder="Start Date"
|
||||
/>
|
||||
|
||||
<input
|
||||
type="date"
|
||||
value={filters.end_date}
|
||||
onChange={(e) => setFilters({...filters, end_date: e.target.value})}
|
||||
placeholder="End Date"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="loading">Loading jobs...</div>
|
||||
) : (
|
||||
<table className="data-grid">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Description</th>
|
||||
<th>Work Status</th>
|
||||
<th>Invoice Status</th>
|
||||
<th>Scheduled</th>
|
||||
<th>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{jobs.map((job) => (
|
||||
<tr key={job.id}>
|
||||
<td>{job.id}</td>
|
||||
<td>{job.customer_id}</td>
|
||||
<td>{job.description || 'N/A'}</td>
|
||||
<td><span className={`badge ${job.work_status}`}>{job.work_status}</span></td>
|
||||
<td><span className={`badge ${job.invoice_status}`}>{job.invoice_status}</span></td>
|
||||
<td>{job.schedule?.start ? new Date(job.schedule.start).toLocaleDateString() : '-'}</td>
|
||||
<td>${job.total_amount?.toFixed(2) || '0.00'}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
/**
|
||||
* Notification Center - Notification management
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function NotificationCenter({ api }: { api: any }) {
|
||||
const [notifications, setNotifications] = useState<any[]>([]);
|
||||
const [type, setType] = useState('');
|
||||
const [status, setStatus] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
loadNotifications();
|
||||
}, [type, status]);
|
||||
|
||||
const loadNotifications = async () => {
|
||||
const params: any = {};
|
||||
if (type) params.type = type;
|
||||
if (status) params.status = status;
|
||||
const result = await api.call('list_notifications', params);
|
||||
setNotifications(JSON.parse(result).notifications);
|
||||
};
|
||||
|
||||
const handleMarkRead = async (notificationId: string) => {
|
||||
await api.call('mark_notification_read', { notification_id: notificationId });
|
||||
loadNotifications();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notification-center">
|
||||
<h1>Notifications</h1>
|
||||
|
||||
<div className="filters">
|
||||
<select value={type} onChange={(e) => setType(e.target.value)}>
|
||||
<option value="">All Types</option>
|
||||
<option value="sms">SMS</option>
|
||||
<option value="email">Email</option>
|
||||
<option value="push">Push</option>
|
||||
</select>
|
||||
<select value={status} onChange={(e) => setStatus(e.target.value)}>
|
||||
<option value="">All Status</option>
|
||||
<option value="queued">Queued</option>
|
||||
<option value="sent">Sent</option>
|
||||
<option value="delivered">Delivered</option>
|
||||
<option value="failed">Failed</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="notifications-list">
|
||||
{notifications.map((notif) => (
|
||||
<div key={notif.id} className={`notification-card ${notif.status}`}>
|
||||
<div className="notif-header">
|
||||
<span className="type">{notif.type}</span>
|
||||
<span className="status">{notif.status}</span>
|
||||
</div>
|
||||
<div className="notif-body">
|
||||
<p><strong>To:</strong> {notif.recipient}</p>
|
||||
{notif.subject && <p><strong>Subject:</strong> {notif.subject}</p>}
|
||||
<p>{notif.message}</p>
|
||||
</div>
|
||||
<div className="notif-footer">
|
||||
<small>{new Date(notif.created_at).toLocaleString()}</small>
|
||||
<button onClick={() => handleMarkRead(notif.id)}>Mark Read</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
75
servers/housecall-pro/src/ui/react-app/revenue-dashboard.tsx
Normal file
75
servers/housecall-pro/src/ui/react-app/revenue-dashboard.tsx
Normal file
@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Revenue Dashboard - Revenue analytics and reporting
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function RevenueDashboard({ api }: { api: any }) {
|
||||
const [report, setReport] = useState<any>(null);
|
||||
const [startDate, setStartDate] = useState('');
|
||||
const [endDate, setEndDate] = useState('');
|
||||
const [groupBy, setGroupBy] = useState<'day' | 'week' | 'month'>('day');
|
||||
|
||||
useEffect(() => {
|
||||
if (startDate && endDate) loadReport();
|
||||
}, [startDate, endDate, groupBy]);
|
||||
|
||||
const loadReport = async () => {
|
||||
const result = await api.call('get_revenue_report', {
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
group_by: groupBy,
|
||||
});
|
||||
setReport(JSON.parse(result));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="revenue-dashboard">
|
||||
<h1>Revenue Dashboard</h1>
|
||||
<div className="controls">
|
||||
<input type="date" value={startDate} onChange={(e) => setStartDate(e.target.value)} />
|
||||
<input type="date" value={endDate} onChange={(e) => setEndDate(e.target.value)} />
|
||||
<select value={groupBy} onChange={(e) => setGroupBy(e.target.value as any)}>
|
||||
<option value="day">Daily</option>
|
||||
<option value="week">Weekly</option>
|
||||
<option value="month">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
{report && (
|
||||
<>
|
||||
<div className="revenue-stats">
|
||||
<div className="stat">
|
||||
<h3>Total Revenue</h3>
|
||||
<p>${report.total_revenue?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<h3>Invoiced</h3>
|
||||
<p>${report.invoiced?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<h3>Paid</h3>
|
||||
<p>${report.paid?.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<h3>Outstanding</h3>
|
||||
<p>${report.outstanding?.toFixed(2)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{report.by_day && (
|
||||
<div className="chart">
|
||||
<h3>Revenue by {groupBy}</h3>
|
||||
{/* Chart visualization would go here */}
|
||||
<ul>
|
||||
{report.by_day.map((day: any, i: number) => (
|
||||
<li key={i}>
|
||||
{day.date}: ${day.revenue.toFixed(2)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
43
servers/housecall-pro/src/ui/react-app/review-dashboard.tsx
Normal file
43
servers/housecall-pro/src/ui/react-app/review-dashboard.tsx
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Review Dashboard - Customer reviews management
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function ReviewDashboard({ api }: { api: any }) {
|
||||
const [reviews, setReviews] = useState<any[]>([]);
|
||||
const [rating, setRating] = useState<number | undefined>();
|
||||
|
||||
useEffect(() => {
|
||||
loadReviews();
|
||||
}, [rating]);
|
||||
|
||||
const loadReviews = async () => {
|
||||
const params = rating ? { rating } : {};
|
||||
const result = await api.call('list_reviews', params);
|
||||
setReviews(JSON.parse(result).reviews);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="review-dashboard">
|
||||
<h1>Customer Reviews</h1>
|
||||
<select value={rating || ''} onChange={(e) => setRating(e.target.value ? parseInt(e.target.value) : undefined)}>
|
||||
<option value="">All Ratings</option>
|
||||
<option value="5">5 Stars</option>
|
||||
<option value="4">4 Stars</option>
|
||||
<option value="3">3 Stars</option>
|
||||
<option value="2">2 Stars</option>
|
||||
<option value="1">1 Star</option>
|
||||
</select>
|
||||
<div className="reviews">
|
||||
{reviews.map((review) => (
|
||||
<div key={review.id} className="review-card">
|
||||
<div className="rating">{'★'.repeat(review.rating || 0)}</div>
|
||||
<p>{review.comment}</p>
|
||||
<small>Job: {review.job_id} | Customer: {review.customer_id}</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
69
servers/housecall-pro/src/ui/react-app/tag-manager.tsx
Normal file
69
servers/housecall-pro/src/ui/react-app/tag-manager.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Tag Manager - Tag organization and management
|
||||
*/
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export function TagManager({ api }: { api: any }) {
|
||||
const [tags, setTags] = useState<any[]>([]);
|
||||
const [newTagName, setNewTagName] = useState('');
|
||||
const [newTagColor, setNewTagColor] = useState('#3498db');
|
||||
|
||||
useEffect(() => {
|
||||
loadTags();
|
||||
}, []);
|
||||
|
||||
const loadTags = async () => {
|
||||
const result = await api.call('list_tags', {});
|
||||
setTags(JSON.parse(result).tags);
|
||||
};
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!newTagName) return;
|
||||
await api.call('create_tag', { name: newTagName, color: newTagColor });
|
||||
setNewTagName('');
|
||||
loadTags();
|
||||
};
|
||||
|
||||
const handleDelete = async (tagId: string) => {
|
||||
if (confirm('Delete this tag?')) {
|
||||
await api.call('delete_tag', { tag_id: tagId });
|
||||
loadTags();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="tag-manager">
|
||||
<h1>Tag Manager</h1>
|
||||
|
||||
<div className="create-tag">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Tag name"
|
||||
value={newTagName}
|
||||
onChange={(e) => setNewTagName(e.target.value)}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={newTagColor}
|
||||
onChange={(e) => setNewTagColor(e.target.value)}
|
||||
/>
|
||||
<button onClick={handleCreate}>Create Tag</button>
|
||||
</div>
|
||||
|
||||
<div className="tags-list">
|
||||
{tags.map((tag) => (
|
||||
<div key={tag.id} className="tag-item">
|
||||
<span
|
||||
className="tag-badge"
|
||||
style={{ backgroundColor: tag.color || '#ccc' }}
|
||||
>
|
||||
{tag.name}
|
||||
</span>
|
||||
<button onClick={() => handleDelete(tag.id)}>Delete</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
20
servers/housecall-pro/tsconfig.json
Normal file
20
servers/housecall-pro/tsconfig.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/ui"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user