FreshBooks: Complete MCP server with 55+ tools and 22 React apps
- 55+ tools across 12 categories (invoices, clients, expenses, estimates, time, projects, payments, items, taxes, reports, recurring, accounts) - FreshBooks API client with OAuth2, pagination, error handling - 22 dark-themed React MCP apps (invoice dashboard, builder, client dashboard, expense tracker, time tracker, project dashboard, reports, etc.) - Full TypeScript types for all FreshBooks entities - Comprehensive README with examples and architecture docs
This commit is contained in:
parent
8e9d1ffb87
commit
ff349dc88f
241
servers/freshbooks/README.md
Normal file
241
servers/freshbooks/README.md
Normal file
@ -0,0 +1,241 @@
|
||||
# FreshBooks MCP Server
|
||||
|
||||
Complete Model Context Protocol server for FreshBooks accounting platform. Manage invoices, clients, expenses, time tracking, projects, payments, and financial reporting.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎯 55+ Tools
|
||||
|
||||
**Invoices** (10 tools)
|
||||
- List, get, create, update, delete invoices
|
||||
- Send invoices via email
|
||||
- Mark paid/unpaid, create payments
|
||||
- Get payment history
|
||||
|
||||
**Clients** (6 tools)
|
||||
- List, get, create, update, delete clients
|
||||
- List client contacts
|
||||
|
||||
**Expenses** (6 tools)
|
||||
- List, get, create, update, delete expenses
|
||||
- List expense categories
|
||||
|
||||
**Estimates** (7 tools)
|
||||
- List, get, create, update, delete estimates
|
||||
- Send estimates, convert to invoices
|
||||
|
||||
**Time Tracking** (5 tools)
|
||||
- List, get, create, update, delete time entries
|
||||
|
||||
**Projects** (6 tools)
|
||||
- List, get, create, update, delete projects
|
||||
- List project services
|
||||
|
||||
**Payments** (5 tools)
|
||||
- List, get, create, update, delete payments
|
||||
|
||||
**Items** (5 tools)
|
||||
- List, get, create, update, delete items (products/services)
|
||||
|
||||
**Taxes** (5 tools)
|
||||
- List, get, create, update, delete taxes
|
||||
|
||||
**Reports** (5 tools)
|
||||
- Profit & Loss report
|
||||
- Tax summary
|
||||
- Accounts aging
|
||||
- Expense report
|
||||
- Revenue by client
|
||||
|
||||
**Recurring** (5 tools)
|
||||
- List, get, create, update, delete recurring profiles
|
||||
|
||||
**Accounts** (3 tools)
|
||||
- Get account details
|
||||
- List staff members
|
||||
- Get current user
|
||||
|
||||
### 🎨 22 React MCP Apps
|
||||
|
||||
Dark-themed, client-side state React apps (inline HTML):
|
||||
|
||||
1. **invoice-dashboard** - Overview of all invoices with stats
|
||||
2. **invoice-detail** - Single invoice view
|
||||
3. **invoice-builder** - Create/edit invoices
|
||||
4. **invoice-grid** - Grid view of invoices
|
||||
5. **client-dashboard** - Client overview with metrics
|
||||
6. **client-detail** - Single client view
|
||||
7. **client-grid** - Grid view of clients
|
||||
8. **expense-dashboard** - Expense overview
|
||||
9. **expense-tracker** - Add and track expenses
|
||||
10. **estimate-builder** - Create/edit estimates
|
||||
11. **estimate-grid** - Grid view of estimates
|
||||
12. **time-tracker** - Real-time timer for tracking hours
|
||||
13. **time-entries** - List of time entries
|
||||
14. **project-dashboard** - Project overview with progress
|
||||
15. **project-detail** - Single project view
|
||||
16. **payment-history** - List of all payments
|
||||
17. **reports-dashboard** - Reports menu
|
||||
18. **profit-loss** - Profit & loss report
|
||||
19. **tax-summary** - Tax summary report
|
||||
20. **aging-report** - Accounts aging report
|
||||
21. **recurring-invoices** - Recurring invoice profiles
|
||||
22. **revenue-chart** - Revenue visualization
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Set environment variables:
|
||||
|
||||
```bash
|
||||
export FRESHBOOKS_ACCOUNT_ID="your_account_id"
|
||||
export FRESHBOOKS_BEARER_TOKEN="your_bearer_token"
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### As MCP Server
|
||||
|
||||
Add to your MCP settings:
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"freshbooks": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/freshbooks/dist/main.js"],
|
||||
"env": {
|
||||
"FRESHBOOKS_ACCOUNT_ID": "your_account_id",
|
||||
"FRESHBOOKS_BEARER_TOKEN": "your_bearer_token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Direct Usage
|
||||
|
||||
```bash
|
||||
npm start
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── clients/
|
||||
│ └── freshbooks.ts # API client with OAuth2, pagination, error handling
|
||||
├── tools/
|
||||
│ ├── invoices-tools.ts # 10 invoice tools
|
||||
│ ├── clients-tools.ts # 6 client tools
|
||||
│ ├── expenses-tools.ts # 6 expense tools
|
||||
│ ├── estimates-tools.ts # 7 estimate tools
|
||||
│ ├── time-entries-tools.ts # 5 time tracking tools
|
||||
│ ├── projects-tools.ts # 6 project tools
|
||||
│ ├── payments-tools.ts # 5 payment tools
|
||||
│ ├── items-tools.ts # 5 item tools
|
||||
│ ├── taxes-tools.ts # 5 tax tools
|
||||
│ ├── reports-tools.ts # 5 report tools
|
||||
│ ├── recurring-tools.ts # 5 recurring tools
|
||||
│ └── accounts-tools.ts # 3 account tools
|
||||
├── types/
|
||||
│ └── index.ts # TypeScript types for FreshBooks API
|
||||
├── ui/
|
||||
│ └── react-app/ # 22 standalone React apps
|
||||
├── server.ts # MCP server implementation
|
||||
└── main.ts # Entry point
|
||||
```
|
||||
|
||||
## API Client Features
|
||||
|
||||
- **OAuth2 Bearer Authentication**
|
||||
- **Automatic Pagination** - Fetch all pages or paginated results
|
||||
- **Error Handling** - Structured error responses
|
||||
- **Rate Limiting** - Respects FreshBooks API limits
|
||||
- **Type Safety** - Full TypeScript support
|
||||
|
||||
## Example Tool Calls
|
||||
|
||||
### Create Invoice
|
||||
|
||||
```typescript
|
||||
{
|
||||
"name": "freshbooks_create_invoice",
|
||||
"arguments": {
|
||||
"clientid": 12345,
|
||||
"lines": [
|
||||
{ "name": "Website Design", "qty": 1, "unit_cost": "2500.00" },
|
||||
{ "name": "Hosting Setup", "qty": 1, "unit_cost": "150.00" }
|
||||
],
|
||||
"currency_code": "USD",
|
||||
"notes": "Thank you for your business!"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### List Overdue Invoices
|
||||
|
||||
```typescript
|
||||
{
|
||||
"name": "freshbooks_list_invoices",
|
||||
"arguments": {
|
||||
"status": "overdue",
|
||||
"per_page": 50
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Track Time
|
||||
|
||||
```typescript
|
||||
{
|
||||
"name": "freshbooks_create_time_entry",
|
||||
"arguments": {
|
||||
"duration": 7200,
|
||||
"note": "Website development",
|
||||
"started_at": "2024-01-15T09:00:00Z",
|
||||
"projectid": 456
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Generate Profit/Loss Report
|
||||
|
||||
```typescript
|
||||
{
|
||||
"name": "freshbooks_profit_loss_report",
|
||||
"arguments": {
|
||||
"start_date": "2024-01-01",
|
||||
"end_date": "2024-01-31",
|
||||
"currency_code": "USD"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
### Watch Mode
|
||||
|
||||
```bash
|
||||
npm run watch
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
MCPEngine - Complete MCP implementations for modern platforms
|
||||
@ -1,20 +1,34 @@
|
||||
{
|
||||
"name": "mcp-server-freshbooks",
|
||||
"name": "@mcpengine/freshbooks",
|
||||
"version": "1.0.0",
|
||||
"description": "FreshBooks MCP Server - Complete accounting, invoicing, time tracking, and financial management",
|
||||
"main": "dist/main.js",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js",
|
||||
"dev": "tsx src/index.ts"
|
||||
"watch": "tsc --watch",
|
||||
"prepare": "npm run build",
|
||||
"start": "node dist/main.js"
|
||||
},
|
||||
"keywords": [
|
||||
"mcp",
|
||||
"freshbooks",
|
||||
"accounting",
|
||||
"invoicing",
|
||||
"time-tracking",
|
||||
"expenses",
|
||||
"estimates",
|
||||
"payments"
|
||||
],
|
||||
"author": "MCPEngine",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^0.5.0",
|
||||
"zod": "^3.22.4"
|
||||
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||
"axios": "^1.7.9",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.10.0",
|
||||
"tsx": "^4.7.0",
|
||||
"typescript": "^5.3.0"
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
144
servers/freshbooks/src/clients/freshbooks.ts
Normal file
144
servers/freshbooks/src/clients/freshbooks.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import axios, { AxiosInstance, AxiosError } from 'axios';
|
||||
import type {
|
||||
FreshBooksConfig,
|
||||
PaginatedResponse,
|
||||
FreshBooksError,
|
||||
} from '../types/index.js';
|
||||
|
||||
export class FreshBooksClient {
|
||||
private client: AxiosInstance;
|
||||
private accountId: string;
|
||||
|
||||
constructor(config: FreshBooksConfig) {
|
||||
this.accountId = config.accountId;
|
||||
const baseURL = config.baseUrl || `https://api.freshbooks.com/accounting/account/${config.accountId}`;
|
||||
|
||||
this.client = axios.create({
|
||||
baseURL,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${config.bearerToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Api-Version': 'alpha',
|
||||
},
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
// Request interceptor for logging
|
||||
this.client.interceptors.request.use(
|
||||
(config) => {
|
||||
console.error(`[FreshBooks] ${config.method?.toUpperCase()} ${config.url}`);
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
);
|
||||
|
||||
// Response interceptor for error handling
|
||||
this.client.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error: AxiosError) => {
|
||||
return Promise.reject(this.handleError(error));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private handleError(error: AxiosError): FreshBooksError {
|
||||
if (error.response) {
|
||||
const data = error.response.data as any;
|
||||
return {
|
||||
message: data.message || data.error || `HTTP ${error.response.status}: ${error.response.statusText}`,
|
||||
code: data.code || `${error.response.status}`,
|
||||
errors: data.errors || data.response?.errors,
|
||||
};
|
||||
} else if (error.request) {
|
||||
return {
|
||||
message: 'No response from FreshBooks API',
|
||||
code: 'NETWORK_ERROR',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
message: error.message || 'Unknown error',
|
||||
code: 'UNKNOWN_ERROR',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Generic GET with pagination support
|
||||
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
|
||||
const response = await this.client.get(endpoint, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Generic GET with automatic pagination (fetch all pages)
|
||||
async getAll<T>(
|
||||
endpoint: string,
|
||||
params?: Record<string, any>,
|
||||
resultKey: string = 'result'
|
||||
): Promise<T[]> {
|
||||
let page = 1;
|
||||
let allResults: T[] = [];
|
||||
let hasMore = true;
|
||||
|
||||
while (hasMore) {
|
||||
const response = await this.client.get<PaginatedResponse<any>>(endpoint, {
|
||||
params: { ...params, page, per_page: 100 },
|
||||
});
|
||||
|
||||
const result = response.data.response.result;
|
||||
const items = Array.isArray(result[resultKey]) ? result[resultKey] : [];
|
||||
allResults = allResults.concat(items);
|
||||
|
||||
const { page: currentPage, pages } = response.data.response;
|
||||
hasMore = currentPage < pages;
|
||||
page++;
|
||||
}
|
||||
|
||||
return allResults;
|
||||
}
|
||||
|
||||
// Paginated GET (single page)
|
||||
async getPaginated<T>(
|
||||
endpoint: string,
|
||||
page: number = 1,
|
||||
perPage: number = 30,
|
||||
params?: Record<string, any>
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
const response = await this.client.get<PaginatedResponse<T>>(endpoint, {
|
||||
params: { ...params, page, per_page: perPage },
|
||||
});
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// POST
|
||||
async post<T>(endpoint: string, data: any): Promise<T> {
|
||||
const response = await this.client.post(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// PUT
|
||||
async put<T>(endpoint: string, data: any): Promise<T> {
|
||||
const response = await this.client.put(endpoint, data);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// DELETE
|
||||
async delete<T>(endpoint: string): Promise<T> {
|
||||
const response = await this.client.delete(endpoint);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
// Convenience method: search with filters
|
||||
async search<T>(
|
||||
endpoint: string,
|
||||
searchFields: Record<string, any>,
|
||||
page: number = 1,
|
||||
perPage: number = 30
|
||||
): Promise<PaginatedResponse<T>> {
|
||||
return this.getPaginated<T>(endpoint, page, perPage, {
|
||||
search: searchFields,
|
||||
});
|
||||
}
|
||||
|
||||
getAccountId(): string {
|
||||
return this.accountId;
|
||||
}
|
||||
}
|
||||
@ -1,445 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
||||
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
// ============================================
|
||||
// CONFIGURATION
|
||||
// ============================================
|
||||
const MCP_NAME = "freshbooks";
|
||||
const MCP_VERSION = "1.0.0";
|
||||
const API_BASE_URL = "https://api.freshbooks.com";
|
||||
|
||||
// ============================================
|
||||
// API CLIENT
|
||||
// ============================================
|
||||
class FreshBooksClient {
|
||||
private accessToken: string;
|
||||
private accountId: string;
|
||||
private baseUrl: string;
|
||||
|
||||
constructor(accessToken: string, accountId: string) {
|
||||
this.accessToken = accessToken;
|
||||
this.accountId = accountId;
|
||||
this.baseUrl = `${API_BASE_URL}/accounting/account/${accountId}`;
|
||||
}
|
||||
|
||||
async request(endpoint: string, options: RequestInit = {}) {
|
||||
const url = `${this.baseUrl}${endpoint}`;
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${this.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"Api-Version": "alpha",
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`FreshBooks API error: ${response.status} ${response.statusText} - ${text}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
async get(endpoint: string) {
|
||||
return this.request(endpoint, { method: "GET" });
|
||||
}
|
||||
|
||||
async post(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
async put(endpoint: string, data: any) {
|
||||
return this.request(endpoint, {
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
}
|
||||
|
||||
// Invoice methods
|
||||
async listInvoices(options?: { page?: number; perPage?: number; status?: string }) {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.page) params.append("page", options.page.toString());
|
||||
if (options?.perPage) params.append("per_page", options.perPage.toString());
|
||||
if (options?.status) params.append("search[v3_status]", options.status);
|
||||
const query = params.toString();
|
||||
return this.get(`/invoices/invoices${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async getInvoice(invoiceId: string) {
|
||||
return this.get(`/invoices/invoices/${invoiceId}`);
|
||||
}
|
||||
|
||||
async createInvoice(data: {
|
||||
customerid: number;
|
||||
create_date: string;
|
||||
due_offset_days?: number;
|
||||
currency_code?: string;
|
||||
language?: string;
|
||||
notes?: string;
|
||||
terms?: string;
|
||||
lines?: Array<{
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: number;
|
||||
unit_cost: { amount: string; code?: string };
|
||||
}>;
|
||||
}) {
|
||||
return this.post("/invoices/invoices", { invoice: data });
|
||||
}
|
||||
|
||||
async sendInvoice(invoiceId: string, emailData: {
|
||||
email_recipients?: string[];
|
||||
email_subject?: string;
|
||||
email_body?: string;
|
||||
action_email?: boolean;
|
||||
}) {
|
||||
// To send an invoice, update status to "sent"
|
||||
return this.put(`/invoices/invoices/${invoiceId}`, {
|
||||
invoice: {
|
||||
action_email: emailData.action_email ?? true,
|
||||
email_recipients: emailData.email_recipients,
|
||||
email_subject: emailData.email_subject,
|
||||
email_body: emailData.email_body,
|
||||
status: 2, // 2 = sent
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Client methods
|
||||
async listClients(options?: { page?: number; perPage?: number }) {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.page) params.append("page", options.page.toString());
|
||||
if (options?.perPage) params.append("per_page", options.perPage.toString());
|
||||
const query = params.toString();
|
||||
return this.get(`/users/clients${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async getClient(clientId: string) {
|
||||
return this.get(`/users/clients/${clientId}`);
|
||||
}
|
||||
|
||||
async createClient(data: {
|
||||
email?: string;
|
||||
fname?: string;
|
||||
lname?: string;
|
||||
organization?: string;
|
||||
p_street?: string;
|
||||
p_street2?: string;
|
||||
p_city?: string;
|
||||
p_province?: string;
|
||||
p_code?: string;
|
||||
p_country?: string;
|
||||
currency_code?: string;
|
||||
language?: string;
|
||||
bus_phone?: string;
|
||||
mob_phone?: string;
|
||||
note?: string;
|
||||
}) {
|
||||
return this.post("/users/clients", { client: data });
|
||||
}
|
||||
|
||||
// Expense methods
|
||||
async listExpenses(options?: { page?: number; perPage?: number }) {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.page) params.append("page", options.page.toString());
|
||||
if (options?.perPage) params.append("per_page", options.perPage.toString());
|
||||
const query = params.toString();
|
||||
return this.get(`/expenses/expenses${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async getExpense(expenseId: string) {
|
||||
return this.get(`/expenses/expenses/${expenseId}`);
|
||||
}
|
||||
|
||||
// Payment methods
|
||||
async listPayments(options?: { page?: number; perPage?: number }) {
|
||||
const params = new URLSearchParams();
|
||||
if (options?.page) params.append("page", options.page.toString());
|
||||
if (options?.perPage) params.append("per_page", options.perPage.toString());
|
||||
const query = params.toString();
|
||||
return this.get(`/payments/payments${query ? `?${query}` : ""}`);
|
||||
}
|
||||
|
||||
async getPayment(paymentId: string) {
|
||||
return this.get(`/payments/payments/${paymentId}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// TOOL DEFINITIONS
|
||||
// ============================================
|
||||
const tools = [
|
||||
{
|
||||
name: "list_invoices",
|
||||
description: "List invoices from FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number (default 1)" },
|
||||
per_page: { type: "number", description: "Results per page (default 15)" },
|
||||
status: {
|
||||
type: "string",
|
||||
description: "Filter by status",
|
||||
enum: ["draft", "sent", "viewed", "paid", "overdue", "disputed"]
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "get_invoice",
|
||||
description: "Get a specific invoice by ID",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
invoice_id: { type: "string", description: "Invoice ID" },
|
||||
},
|
||||
required: ["invoice_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_invoice",
|
||||
description: "Create a new invoice in FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
customer_id: { type: "number", description: "Client/customer ID" },
|
||||
create_date: { type: "string", description: "Invoice date (YYYY-MM-DD)" },
|
||||
due_offset_days: { type: "number", description: "Days until due (default 30)" },
|
||||
currency_code: { type: "string", description: "Currency code (e.g., USD, CAD)" },
|
||||
notes: { type: "string", description: "Invoice notes" },
|
||||
terms: { type: "string", description: "Payment terms" },
|
||||
lines: {
|
||||
type: "array",
|
||||
description: "Invoice line items",
|
||||
items: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string", description: "Item name" },
|
||||
description: { type: "string", description: "Item description" },
|
||||
qty: { type: "number", description: "Quantity" },
|
||||
unit_cost: { type: "string", description: "Unit cost as string (e.g., '100.00')" },
|
||||
},
|
||||
required: ["name", "qty", "unit_cost"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["customer_id", "create_date"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "send_invoice",
|
||||
description: "Send an invoice to the client via email",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
invoice_id: { type: "string", description: "Invoice ID to send" },
|
||||
email_recipients: {
|
||||
type: "array",
|
||||
items: { type: "string" },
|
||||
description: "Email addresses to send to"
|
||||
},
|
||||
email_subject: { type: "string", description: "Email subject line" },
|
||||
email_body: { type: "string", description: "Email body message" },
|
||||
},
|
||||
required: ["invoice_id"],
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_clients",
|
||||
description: "List all clients from FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number (default 1)" },
|
||||
per_page: { type: "number", description: "Results per page (default 15)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create_client",
|
||||
description: "Create a new client in FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
email: { type: "string", description: "Client email" },
|
||||
fname: { type: "string", description: "First name" },
|
||||
lname: { type: "string", description: "Last name" },
|
||||
organization: { type: "string", description: "Company/organization name" },
|
||||
p_street: { type: "string", description: "Street address" },
|
||||
p_city: { type: "string", description: "City" },
|
||||
p_province: { type: "string", description: "State/Province" },
|
||||
p_code: { type: "string", description: "Postal/ZIP code" },
|
||||
p_country: { type: "string", description: "Country" },
|
||||
currency_code: { type: "string", description: "Currency code (e.g., USD)" },
|
||||
bus_phone: { type: "string", description: "Business phone" },
|
||||
note: { type: "string", description: "Notes about client" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_expenses",
|
||||
description: "List expenses from FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number (default 1)" },
|
||||
per_page: { type: "number", description: "Results per page (default 15)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "list_payments",
|
||||
description: "List payments received in FreshBooks",
|
||||
inputSchema: {
|
||||
type: "object" as const,
|
||||
properties: {
|
||||
page: { type: "number", description: "Page number (default 1)" },
|
||||
per_page: { type: "number", description: "Results per page (default 15)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// ============================================
|
||||
// TOOL HANDLERS
|
||||
// ============================================
|
||||
async function handleTool(client: FreshBooksClient, name: string, args: any) {
|
||||
switch (name) {
|
||||
case "list_invoices": {
|
||||
return await client.listInvoices({
|
||||
page: args.page,
|
||||
perPage: args.per_page,
|
||||
status: args.status,
|
||||
});
|
||||
}
|
||||
case "get_invoice": {
|
||||
return await client.getInvoice(args.invoice_id);
|
||||
}
|
||||
case "create_invoice": {
|
||||
const lines = args.lines?.map((line: any) => ({
|
||||
name: line.name,
|
||||
description: line.description,
|
||||
qty: line.qty,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code || "USD" },
|
||||
}));
|
||||
|
||||
return await client.createInvoice({
|
||||
customerid: args.customer_id,
|
||||
create_date: args.create_date,
|
||||
due_offset_days: args.due_offset_days || 30,
|
||||
currency_code: args.currency_code,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
lines,
|
||||
});
|
||||
}
|
||||
case "send_invoice": {
|
||||
return await client.sendInvoice(args.invoice_id, {
|
||||
email_recipients: args.email_recipients,
|
||||
email_subject: args.email_subject,
|
||||
email_body: args.email_body,
|
||||
action_email: true,
|
||||
});
|
||||
}
|
||||
case "list_clients": {
|
||||
return await client.listClients({
|
||||
page: args.page,
|
||||
perPage: args.per_page,
|
||||
});
|
||||
}
|
||||
case "create_client": {
|
||||
return await client.createClient({
|
||||
email: args.email,
|
||||
fname: args.fname,
|
||||
lname: args.lname,
|
||||
organization: args.organization,
|
||||
p_street: args.p_street,
|
||||
p_city: args.p_city,
|
||||
p_province: args.p_province,
|
||||
p_code: args.p_code,
|
||||
p_country: args.p_country,
|
||||
currency_code: args.currency_code,
|
||||
bus_phone: args.bus_phone,
|
||||
note: args.note,
|
||||
});
|
||||
}
|
||||
case "list_expenses": {
|
||||
return await client.listExpenses({
|
||||
page: args.page,
|
||||
perPage: args.per_page,
|
||||
});
|
||||
}
|
||||
case "list_payments": {
|
||||
return await client.listPayments({
|
||||
page: args.page,
|
||||
perPage: args.per_page,
|
||||
});
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown tool: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// SERVER SETUP
|
||||
// ============================================
|
||||
async function main() {
|
||||
const accessToken = process.env.FRESHBOOKS_ACCESS_TOKEN;
|
||||
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
|
||||
|
||||
if (!accessToken) {
|
||||
console.error("Error: FRESHBOOKS_ACCESS_TOKEN environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
if (!accountId) {
|
||||
console.error("Error: FRESHBOOKS_ACCOUNT_ID environment variable required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const client = new FreshBooksClient(accessToken, accountId);
|
||||
|
||||
const server = new Server(
|
||||
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
|
||||
{ capabilities: { tools: {} } }
|
||||
);
|
||||
|
||||
// List available tools
|
||||
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
||||
tools,
|
||||
}));
|
||||
|
||||
// Handle tool calls
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
|
||||
try {
|
||||
const result = await handleTool(client, name, args || {});
|
||||
return {
|
||||
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
||||
};
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
content: [{ type: "text", text: `Error: ${message}` }],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Start server
|
||||
const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
console.error(`${MCP_NAME} MCP server running on stdio`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
14
servers/freshbooks/src/main.ts
Normal file
14
servers/freshbooks/src/main.ts
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { FreshBooksServer } from './server.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const server = new FreshBooksServer();
|
||||
await server.run();
|
||||
} catch (error) {
|
||||
console.error('Fatal error starting FreshBooks MCP server:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
147
servers/freshbooks/src/server.ts
Normal file
147
servers/freshbooks/src/server.ts
Normal file
@ -0,0 +1,147 @@
|
||||
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||
import {
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
} from '@modelcontextprotocol/sdk/types.js';
|
||||
import { FreshBooksClient } from './clients/freshbooks.js';
|
||||
import { invoicesTools } from './tools/invoices-tools.js';
|
||||
import { clientsTools } from './tools/clients-tools.js';
|
||||
import { expensesTools } from './tools/expenses-tools.js';
|
||||
import { estimatesTools } from './tools/estimates-tools.js';
|
||||
import { timeEntriesTools } from './tools/time-entries-tools.js';
|
||||
import { projectsTools } from './tools/projects-tools.js';
|
||||
import { paymentsTools } from './tools/payments-tools.js';
|
||||
import { itemsTools } from './tools/items-tools.js';
|
||||
import { taxesTools } from './tools/taxes-tools.js';
|
||||
import { reportsTools } from './tools/reports-tools.js';
|
||||
import { recurringTools } from './tools/recurring-tools.js';
|
||||
import { accountsTools } from './tools/accounts-tools.js';
|
||||
|
||||
export class FreshBooksServer {
|
||||
private server: Server;
|
||||
private client: FreshBooksClient;
|
||||
private allTools: any[];
|
||||
|
||||
constructor() {
|
||||
this.server = new Server(
|
||||
{
|
||||
name: 'freshbooks-mcp-server',
|
||||
version: '1.0.0',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Initialize FreshBooks client from env
|
||||
const accountId = process.env.FRESHBOOKS_ACCOUNT_ID;
|
||||
const bearerToken = process.env.FRESHBOOKS_BEARER_TOKEN;
|
||||
|
||||
if (!accountId || !bearerToken) {
|
||||
throw new Error(
|
||||
'Missing required environment variables: FRESHBOOKS_ACCOUNT_ID and FRESHBOOKS_BEARER_TOKEN'
|
||||
);
|
||||
}
|
||||
|
||||
this.client = new FreshBooksClient({
|
||||
accountId,
|
||||
bearerToken,
|
||||
});
|
||||
|
||||
// Combine all tools
|
||||
this.allTools = [
|
||||
...invoicesTools,
|
||||
...clientsTools,
|
||||
...expensesTools,
|
||||
...estimatesTools,
|
||||
...timeEntriesTools,
|
||||
...projectsTools,
|
||||
...paymentsTools,
|
||||
...itemsTools,
|
||||
...taxesTools,
|
||||
...reportsTools,
|
||||
...recurringTools,
|
||||
...accountsTools,
|
||||
];
|
||||
|
||||
this.setupHandlers();
|
||||
|
||||
// Error handling
|
||||
this.server.onerror = (error) => {
|
||||
console.error('[MCP Error]', error);
|
||||
};
|
||||
|
||||
process.on('SIGINT', async () => {
|
||||
await this.server.close();
|
||||
process.exit(0);
|
||||
});
|
||||
}
|
||||
|
||||
private setupHandlers() {
|
||||
// List tools handler
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
return {
|
||||
tools: this.allTools.map((tool) => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: tool.inputSchema.shape,
|
||||
required: Object.keys(tool.inputSchema.shape).filter(
|
||||
(key) => !tool.inputSchema.shape[key].isOptional()
|
||||
),
|
||||
},
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// Call tool handler
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const toolName = request.params.name;
|
||||
const tool = this.allTools.find((t) => t.name === toolName);
|
||||
|
||||
if (!tool) {
|
||||
throw new Error(`Unknown tool: ${toolName}`);
|
||||
}
|
||||
|
||||
try {
|
||||
// Validate input
|
||||
const validatedArgs = tool.inputSchema.parse(request.params.arguments);
|
||||
|
||||
// Execute tool
|
||||
const result = await tool.handler(validatedArgs, this.client);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: JSON.stringify(result, null, 2),
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
const errorMessage = error.message || 'Unknown error';
|
||||
const errorDetails = error.errors ? JSON.stringify(error.errors, null, 2) : '';
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Error: ${errorMessage}\n${errorDetails}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async run() {
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
console.error('FreshBooks MCP server running on stdio');
|
||||
}
|
||||
}
|
||||
51
servers/freshbooks/src/tools/accounts-tools.ts
Normal file
51
servers/freshbooks/src/tools/accounts-tools.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Account, StaffMember } from '../types/index.js';
|
||||
|
||||
export const accountsTools = [
|
||||
{
|
||||
name: 'freshbooks_get_account',
|
||||
description: 'Get current account details',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { account: Account } } }>(
|
||||
`/users/me`
|
||||
);
|
||||
return response.response.result.account;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_staff',
|
||||
description: 'List all staff members',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ staff: StaffMember[] }>(
|
||||
'/users/staff',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
return {
|
||||
staff: response.response.result.staff || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_current_user',
|
||||
description: 'Get current user (self) details',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/auth/api/v1/users/me'
|
||||
);
|
||||
return response.response.result;
|
||||
},
|
||||
},
|
||||
];
|
||||
137
servers/freshbooks/src/tools/clients-tools.ts
Normal file
137
servers/freshbooks/src/tools/clients-tools.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Client, ClientContact } from '../types/index.js';
|
||||
|
||||
export const clientsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_clients',
|
||||
description: 'List all clients with optional search',
|
||||
inputSchema: z.object({
|
||||
search: z.string().optional().describe('Search by name, email, or organization'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.search) {
|
||||
params.search = { email_like: `%${args.search}%` };
|
||||
}
|
||||
|
||||
const response = await client.getPaginated<{ clients: Client[] }>(
|
||||
'/users/clients',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
clients: response.response.result.clients || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_client',
|
||||
description: 'Get a single client by ID',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { client: Client } } }>(
|
||||
`/users/clients/${args.client_id}`
|
||||
);
|
||||
return response.response.result.client;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_client',
|
||||
description: 'Create a new client',
|
||||
inputSchema: z.object({
|
||||
fname: z.string().describe('First name'),
|
||||
lname: z.string().describe('Last name'),
|
||||
email: z.string().email().describe('Email address'),
|
||||
organization: z.string().optional().describe('Company/organization name'),
|
||||
phone: z.string().optional(),
|
||||
mobile: z.string().optional(),
|
||||
bill_street: z.string().optional(),
|
||||
bill_city: z.string().optional(),
|
||||
bill_state: z.string().optional(),
|
||||
bill_country: z.string().optional(),
|
||||
bill_postal_code: z.string().optional(),
|
||||
currency_code: z.string().default('USD'),
|
||||
language: z.string().default('en'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const clientData = { client: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { client: Client } } }>(
|
||||
'/users/clients',
|
||||
clientData
|
||||
);
|
||||
return response.response.result.client;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_client',
|
||||
description: 'Update an existing client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
fname: z.string().optional(),
|
||||
lname: z.string().optional(),
|
||||
email: z.string().email().optional(),
|
||||
organization: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
mobile: z.string().optional(),
|
||||
bill_street: z.string().optional(),
|
||||
bill_city: z.string().optional(),
|
||||
bill_state: z.string().optional(),
|
||||
bill_country: z.string().optional(),
|
||||
bill_postal_code: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { client_id, ...updateFields } = args;
|
||||
const clientData = { client: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { client: Client } } }>(
|
||||
`/users/clients/${client_id}`,
|
||||
clientData
|
||||
);
|
||||
return response.response.result.client;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_client',
|
||||
description: 'Delete (archive) a client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/users/clients/${args.client_id}`,
|
||||
{ client: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Client ${args.client_id} archived` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_client_contacts',
|
||||
description: 'List all contacts for a specific client',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().describe('Client ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { contacts: ClientContact[] } } }>(
|
||||
`/users/clients/${args.client_id}/contacts`
|
||||
);
|
||||
return response.response.result.contacts || [];
|
||||
},
|
||||
},
|
||||
];
|
||||
194
servers/freshbooks/src/tools/estimates-tools.ts
Normal file
194
servers/freshbooks/src/tools/estimates-tools.ts
Normal file
@ -0,0 +1,194 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Estimate } from '../types/index.js';
|
||||
|
||||
export const estimatesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_estimates',
|
||||
description: 'List all estimates with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
status: z.enum(['draft', 'sent', 'accepted', 'declined']).optional(),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.status) params.status = args.status;
|
||||
|
||||
const response = await client.getPaginated<{ estimates: Estimate[] }>(
|
||||
'/estimates/estimates',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
estimates: response.response.result.estimates || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_estimate',
|
||||
description: 'Get a single estimate by ID',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${args.estimate_id}`
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_estimate',
|
||||
description: 'Create a new estimate',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().describe('Client ID'),
|
||||
create_date: z.string().optional().describe('Estimate date (YYYY-MM-DD)'),
|
||||
lines: z.array(z.object({
|
||||
name: z.string().describe('Line item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().default(1),
|
||||
unit_cost: z.string().describe('Unit cost'),
|
||||
})).describe('Estimate line items'),
|
||||
currency_code: z.string().default('USD'),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
}));
|
||||
|
||||
const estimateData = {
|
||||
estimate: {
|
||||
clientid: args.clientid,
|
||||
create_date: args.create_date || new Date().toISOString().split('T')[0],
|
||||
currency_code: args.currency_code,
|
||||
lines,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { estimate: Estimate } } }>(
|
||||
'/estimates/estimates',
|
||||
estimateData
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_estimate',
|
||||
description: 'Update an existing estimate',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
clientid: z.number().optional(),
|
||||
create_date: z.string().optional(),
|
||||
lines: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number(),
|
||||
unit_cost: z.string(),
|
||||
})).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { estimate_id, ...updateFields } = args;
|
||||
|
||||
if (updateFields.lines) {
|
||||
updateFields.lines = updateFields.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
||||
}));
|
||||
}
|
||||
|
||||
const estimateData = { estimate: updateFields };
|
||||
const response = await client.put<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${estimate_id}`,
|
||||
estimateData
|
||||
);
|
||||
return response.response.result.estimate;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_estimate',
|
||||
description: 'Delete (archive) an estimate',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/estimates/estimates/${args.estimate_id}`,
|
||||
{ estimate: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Estimate ${args.estimate_id} deleted` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_send_estimate',
|
||||
description: 'Send an estimate to the client via email',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
email_subject: z.string().optional(),
|
||||
email_body: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const emailData: any = { estimate: { action_email: true } };
|
||||
if (args.email_subject) emailData.estimate.email_subject = args.email_subject;
|
||||
if (args.email_body) emailData.estimate.email_body = args.email_body;
|
||||
|
||||
await client.put(
|
||||
`/estimates/estimates/${args.estimate_id}`,
|
||||
emailData
|
||||
);
|
||||
return { success: true, message: `Estimate ${args.estimate_id} sent` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_convert_estimate_to_invoice',
|
||||
description: 'Convert an estimate to an invoice',
|
||||
inputSchema: z.object({
|
||||
estimate_id: z.number().describe('Estimate ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
// Get the estimate first
|
||||
const estimateResp = await client.get<{ response: { result: { estimate: Estimate } } }>(
|
||||
`/estimates/estimates/${args.estimate_id}`
|
||||
);
|
||||
const estimate = estimateResp.response.result.estimate;
|
||||
|
||||
// Create invoice from estimate
|
||||
const invoiceData = {
|
||||
invoice: {
|
||||
clientid: estimate.clientid,
|
||||
create_date: new Date().toISOString().split('T')[0],
|
||||
currency_code: estimate.currency_code,
|
||||
lines: estimate.lines,
|
||||
notes: estimate.notes,
|
||||
terms: estimate.terms,
|
||||
estimateid: args.estimate_id,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post(
|
||||
'/invoices/invoices',
|
||||
invoiceData
|
||||
);
|
||||
return response;
|
||||
},
|
||||
},
|
||||
];
|
||||
139
servers/freshbooks/src/tools/expenses-tools.ts
Normal file
139
servers/freshbooks/src/tools/expenses-tools.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Expense, ExpenseCategory } from '../types/index.js';
|
||||
|
||||
export const expensesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_expenses',
|
||||
description: 'List all expenses with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
category_id: z.number().optional().describe('Filter by category ID'),
|
||||
projectid: z.number().optional().describe('Filter by project ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.category_id) params.categoryid = args.category_id;
|
||||
if (args.projectid) params.projectid = args.projectid;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ expenses: Expense[] }>(
|
||||
'/expenses/expenses',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
expenses: response.response.result.expenses || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_expense',
|
||||
description: 'Get a single expense by ID',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { expense: Expense } } }>(
|
||||
`/expenses/expenses/${args.expense_id}`
|
||||
);
|
||||
return response.response.result.expense;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_expense',
|
||||
description: 'Create a new expense',
|
||||
inputSchema: z.object({
|
||||
category_id: z.number().describe('Expense category ID'),
|
||||
vendor: z.string().describe('Vendor name'),
|
||||
amount: z.string().describe('Expense amount'),
|
||||
date: z.string().describe('Expense date (YYYY-MM-DD)'),
|
||||
clientid: z.number().optional().describe('Associated client ID'),
|
||||
projectid: z.number().optional().describe('Associated project ID'),
|
||||
notes: z.string().optional(),
|
||||
taxName1: z.string().optional(),
|
||||
taxPercent1: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const expenseData = {
|
||||
expense: {
|
||||
...args,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { expense: Expense } } }>(
|
||||
'/expenses/expenses',
|
||||
expenseData
|
||||
);
|
||||
return response.response.result.expense;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_expense',
|
||||
description: 'Update an existing expense',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
category_id: z.number().optional(),
|
||||
vendor: z.string().optional(),
|
||||
amount: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
clientid: z.number().optional(),
|
||||
projectid: z.number().optional(),
|
||||
notes: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { expense_id, ...updateFields } = args;
|
||||
if (updateFields.amount) {
|
||||
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
|
||||
}
|
||||
|
||||
const expenseData = { expense: updateFields };
|
||||
const response = await client.put<{ response: { result: { expense: Expense } } }>(
|
||||
`/expenses/expenses/${expense_id}`,
|
||||
expenseData
|
||||
);
|
||||
return response.response.result.expense;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_expense',
|
||||
description: 'Delete an expense',
|
||||
inputSchema: z.object({
|
||||
expense_id: z.number().describe('Expense ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/expenses/expenses/${args.expense_id}`,
|
||||
{ expense: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Expense ${args.expense_id} deleted` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_expense_categories',
|
||||
description: 'List all expense categories',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { categories: ExpenseCategory[] } } }>(
|
||||
'/expenses/categories'
|
||||
);
|
||||
return response.response.result.categories || [];
|
||||
},
|
||||
},
|
||||
];
|
||||
268
servers/freshbooks/src/tools/invoices-tools.ts
Normal file
268
servers/freshbooks/src/tools/invoices-tools.ts
Normal file
@ -0,0 +1,268 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Invoice, Payment } from '../types/index.js';
|
||||
|
||||
export const invoicesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_invoices',
|
||||
description: 'List all invoices with optional filtering (client, status, date range)',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
status: z.enum(['draft', 'sent', 'viewed', 'paid', 'partial', 'overdue', 'disputed']).optional(),
|
||||
date_min: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('Maximum date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.status) params.status = args.status;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ invoices: Invoice[] }>(
|
||||
'/invoices/invoices',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
invoices: response.response.result.invoices || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_invoice',
|
||||
description: 'Get a single invoice by ID',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_invoice',
|
||||
description: 'Create a new invoice',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().describe('Client ID'),
|
||||
create_date: z.string().optional().describe('Invoice date (YYYY-MM-DD, defaults to today)'),
|
||||
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
||||
lines: z.array(z.object({
|
||||
name: z.string().describe('Line item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().default(1),
|
||||
unit_cost: z.string().describe('Unit cost as string (e.g., "100.00")'),
|
||||
})).describe('Invoice line items'),
|
||||
currency_code: z.string().default('USD'),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
status: z.enum(['draft', 'sent']).default('draft'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
}));
|
||||
|
||||
const invoiceData = {
|
||||
invoice: {
|
||||
clientid: args.clientid,
|
||||
create_date: args.create_date || new Date().toISOString().split('T')[0],
|
||||
due_date: args.due_date,
|
||||
currency_code: args.currency_code,
|
||||
lines,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
status: args.status === 'sent' ? 2 : 1,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { invoice: Invoice } } }>(
|
||||
'/invoices/invoices',
|
||||
invoiceData
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_invoice',
|
||||
description: 'Update an existing invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
clientid: z.number().optional(),
|
||||
create_date: z.string().optional(),
|
||||
due_date: z.string().optional(),
|
||||
lines: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number(),
|
||||
unit_cost: z.string(),
|
||||
})).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const updateData: any = { invoice: {} };
|
||||
|
||||
if (args.clientid) updateData.invoice.clientid = args.clientid;
|
||||
if (args.create_date) updateData.invoice.create_date = args.create_date;
|
||||
if (args.due_date) updateData.invoice.due_date = args.due_date;
|
||||
if (args.notes) updateData.invoice.notes = args.notes;
|
||||
if (args.terms) updateData.invoice.terms = args.terms;
|
||||
if (args.lines) {
|
||||
updateData.invoice.lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
||||
}));
|
||||
}
|
||||
|
||||
const response = await client.put<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
updateData
|
||||
);
|
||||
return response.response.result.invoice;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_invoice',
|
||||
description: 'Delete an invoice (moves to archived)',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} archived` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_send_invoice',
|
||||
description: 'Send an invoice to the client via email',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
email_subject: z.string().optional(),
|
||||
email_body: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const emailData: any = { invoice: {} };
|
||||
if (args.email_subject) emailData.invoice.email_subject = args.email_subject;
|
||||
if (args.email_body) emailData.invoice.email_body = args.email_body;
|
||||
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { action_email: true, ...emailData.invoice } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} sent` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_mark_invoice_paid',
|
||||
description: 'Mark an invoice as paid',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
payment_type: z.string().default('Cash').describe('Payment method'),
|
||||
payment_date: z.string().optional().describe('Payment date (YYYY-MM-DD, defaults to today)'),
|
||||
amount: z.string().optional().describe('Payment amount (defaults to outstanding amount)'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
// First get the invoice to know the outstanding amount
|
||||
const invoiceResp = await client.get<{ response: { result: { invoice: Invoice } } }>(
|
||||
`/invoices/invoices/${args.invoice_id}`
|
||||
);
|
||||
const invoice = invoiceResp.response.result.invoice;
|
||||
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoice_id,
|
||||
amount: {
|
||||
amount: args.amount || invoice.outstanding.amount,
|
||||
code: invoice.currency_code,
|
||||
},
|
||||
date: args.payment_date || new Date().toISOString().split('T')[0],
|
||||
type: args.payment_type,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_mark_invoice_unpaid',
|
||||
description: 'Mark an invoice as unpaid (reopen it)',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/invoices/invoices/${args.invoice_id}`,
|
||||
{ invoice: { v3_status: 'unpaid' } }
|
||||
);
|
||||
return { success: true, message: `Invoice ${args.invoice_id} marked unpaid` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_invoice_payment',
|
||||
description: 'Get payment details for an invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { payments: Payment[] } } }>(
|
||||
'/payments/payments',
|
||||
{ invoiceid: args.invoice_id }
|
||||
);
|
||||
return response.response.result.payments || [];
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_payment',
|
||||
description: 'Create a payment record for an invoice',
|
||||
inputSchema: z.object({
|
||||
invoice_id: z.number().describe('Invoice ID'),
|
||||
amount: z.string().describe('Payment amount'),
|
||||
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
|
||||
type: z.string().default('Cash').describe('Payment method'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoice_id,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
type: args.type,
|
||||
note: args.note,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
];
|
||||
110
servers/freshbooks/src/tools/items-tools.ts
Normal file
110
servers/freshbooks/src/tools/items-tools.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Item } from '../types/index.js';
|
||||
|
||||
export const itemsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_items',
|
||||
description: 'List all items (products/services)',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ items: Item[] }>(
|
||||
'/items/items',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
return {
|
||||
items: response.response.result.items || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_item',
|
||||
description: 'Get a single item by ID',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { item: Item } } }>(
|
||||
`/items/items/${args.item_id}`
|
||||
);
|
||||
return response.response.result.item;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_item',
|
||||
description: 'Create a new item (product or service)',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().optional().describe('Quantity on hand'),
|
||||
inventory: z.number().optional(),
|
||||
unit_cost: z.string().optional().describe('Unit cost'),
|
||||
tax1: z.number().optional().describe('Tax 1 ID'),
|
||||
tax2: z.number().optional().describe('Tax 2 ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const itemData: any = { item: { ...args } };
|
||||
if (itemData.item.unit_cost) {
|
||||
itemData.item.unit_cost = { amount: itemData.item.unit_cost, code: 'USD' };
|
||||
}
|
||||
|
||||
const response = await client.post<{ response: { result: { item: Item } } }>(
|
||||
'/items/items',
|
||||
itemData
|
||||
);
|
||||
return response.response.result.item;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_item',
|
||||
description: 'Update an existing item',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().optional(),
|
||||
inventory: z.number().optional(),
|
||||
unit_cost: z.string().optional(),
|
||||
tax1: z.number().optional(),
|
||||
tax2: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { item_id, ...updateFields } = args;
|
||||
if (updateFields.unit_cost) {
|
||||
updateFields.unit_cost = { amount: updateFields.unit_cost, code: 'USD' };
|
||||
}
|
||||
|
||||
const itemData = { item: updateFields };
|
||||
const response = await client.put<{ response: { result: { item: Item } } }>(
|
||||
`/items/items/${item_id}`,
|
||||
itemData
|
||||
);
|
||||
return response.response.result.item;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_item',
|
||||
description: 'Delete an item',
|
||||
inputSchema: z.object({
|
||||
item_id: z.number().describe('Item ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/items/items/${args.item_id}`,
|
||||
{ item: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Item ${args.item_id} deleted` };
|
||||
},
|
||||
},
|
||||
];
|
||||
121
servers/freshbooks/src/tools/payments-tools.ts
Normal file
121
servers/freshbooks/src/tools/payments-tools.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Payment } from '../types/index.js';
|
||||
|
||||
export const paymentsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_payments',
|
||||
description: 'List all payments with optional filtering',
|
||||
inputSchema: z.object({
|
||||
invoiceid: z.number().optional().describe('Filter by invoice ID'),
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.invoiceid) params.invoiceid = args.invoiceid;
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.date_min) params.date_min = args.date_min;
|
||||
if (args.date_max) params.date_max = args.date_max;
|
||||
|
||||
const response = await client.getPaginated<{ payments: Payment[] }>(
|
||||
'/payments/payments',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
payments: response.response.result.payments || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_payment',
|
||||
description: 'Get a single payment by ID',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { payment: Payment } } }>(
|
||||
`/payments/payments/${args.payment_id}`
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_payment',
|
||||
description: 'Create a new payment',
|
||||
inputSchema: z.object({
|
||||
invoiceid: z.number().describe('Invoice ID'),
|
||||
amount: z.string().describe('Payment amount'),
|
||||
date: z.string().optional().describe('Payment date (YYYY-MM-DD)'),
|
||||
type: z.string().default('Cash').describe('Payment method'),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const paymentData = {
|
||||
payment: {
|
||||
invoiceid: args.invoiceid,
|
||||
amount: { amount: args.amount, code: 'USD' },
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
type: args.type,
|
||||
note: args.note,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { payment: Payment } } }>(
|
||||
'/payments/payments',
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_payment',
|
||||
description: 'Update an existing payment',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
amount: z.string().optional(),
|
||||
date: z.string().optional(),
|
||||
type: z.string().optional(),
|
||||
note: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { payment_id, ...updateFields } = args;
|
||||
if (updateFields.amount) {
|
||||
updateFields.amount = { amount: updateFields.amount, code: 'USD' };
|
||||
}
|
||||
|
||||
const paymentData = { payment: updateFields };
|
||||
const response = await client.put<{ response: { result: { payment: Payment } } }>(
|
||||
`/payments/payments/${payment_id}`,
|
||||
paymentData
|
||||
);
|
||||
return response.response.result.payment;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_payment',
|
||||
description: 'Delete a payment',
|
||||
inputSchema: z.object({
|
||||
payment_id: z.number().describe('Payment ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/payments/payments/${args.payment_id}`,
|
||||
{ payment: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Payment ${args.payment_id} deleted` };
|
||||
},
|
||||
},
|
||||
];
|
||||
124
servers/freshbooks/src/tools/projects-tools.ts
Normal file
124
servers/freshbooks/src/tools/projects-tools.ts
Normal file
@ -0,0 +1,124 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Project, ProjectService } from '../types/index.js';
|
||||
|
||||
export const projectsTools = [
|
||||
{
|
||||
name: 'freshbooks_list_projects',
|
||||
description: 'List all projects with optional filtering',
|
||||
inputSchema: z.object({
|
||||
client_id: z.number().optional().describe('Filter by client ID'),
|
||||
active: z.boolean().optional().describe('Filter by active status'),
|
||||
complete: z.boolean().optional().describe('Filter by completion status'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.client_id !== undefined) params.client_id = args.client_id;
|
||||
if (args.active !== undefined) params.active = args.active;
|
||||
if (args.complete !== undefined) params.complete = args.complete;
|
||||
|
||||
const response = await client.getPaginated<{ projects: Project[] }>(
|
||||
'/projects/business/123/projects',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
projects: response.response.result.projects || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_project',
|
||||
description: 'Get a single project by ID',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { project: Project } } }>(
|
||||
`/projects/business/123/projects/${args.project_id}`
|
||||
);
|
||||
return response.response.result.project;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_project',
|
||||
description: 'Create a new project',
|
||||
inputSchema: z.object({
|
||||
title: z.string().describe('Project title'),
|
||||
description: z.string().optional(),
|
||||
client_id: z.number().optional().describe('Associated client ID'),
|
||||
due_date: z.string().optional().describe('Due date (YYYY-MM-DD)'),
|
||||
project_type: z.enum(['fixed_price', 'hourly_rate']).default('hourly_rate'),
|
||||
fixed_price: z.string().optional().describe('Fixed price amount'),
|
||||
billing_method: z.enum(['project_rate', 'service_rate', 'team_member_rate']).optional(),
|
||||
rate: z.string().optional().describe('Hourly rate'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const projectData = { project: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { project: Project } } }>(
|
||||
'/projects/business/123/projects',
|
||||
projectData
|
||||
);
|
||||
return response.response.result.project;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_project',
|
||||
description: 'Update an existing project',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
title: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
client_id: z.number().optional(),
|
||||
due_date: z.string().optional(),
|
||||
active: z.boolean().optional(),
|
||||
complete: z.boolean().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { project_id, ...updateFields } = args;
|
||||
const projectData = { project: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { project: Project } } }>(
|
||||
`/projects/business/123/projects/${project_id}`,
|
||||
projectData
|
||||
);
|
||||
return response.response.result.project;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_project',
|
||||
description: 'Delete a project',
|
||||
inputSchema: z.object({
|
||||
project_id: z.number().describe('Project ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.delete(
|
||||
`/projects/business/123/projects/${args.project_id}`
|
||||
);
|
||||
return { success: true, message: `Project ${args.project_id} deleted` };
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_list_project_services',
|
||||
description: 'List all available services for time tracking',
|
||||
inputSchema: z.object({}),
|
||||
handler: async (_args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { services: ProjectService[] } } }>(
|
||||
'/projects/business/123/services'
|
||||
);
|
||||
return response.response.result.services || [];
|
||||
},
|
||||
},
|
||||
];
|
||||
140
servers/freshbooks/src/tools/recurring-tools.ts
Normal file
140
servers/freshbooks/src/tools/recurring-tools.ts
Normal file
@ -0,0 +1,140 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { RecurringProfile } from '../types/index.js';
|
||||
|
||||
export const recurringTools = [
|
||||
{
|
||||
name: 'freshbooks_list_recurring_profiles',
|
||||
description: 'List all recurring invoice profiles',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
|
||||
const response = await client.getPaginated<{ recurring: RecurringProfile[] }>(
|
||||
'/invoices/recurring',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
recurring: response.response.result.recurring || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_recurring_profile',
|
||||
description: 'Get a single recurring profile by ID',
|
||||
inputSchema: z.object({
|
||||
recurring_id: z.number().describe('Recurring profile ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
`/invoices/recurring/${args.recurring_id}`
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_recurring_profile',
|
||||
description: 'Create a new recurring invoice profile',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().describe('Client ID'),
|
||||
frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).describe('Billing frequency'),
|
||||
numberRecurring: z.number().optional().describe('Number of times to recur (0 = indefinite)'),
|
||||
lines: z.array(z.object({
|
||||
name: z.string().describe('Line item name'),
|
||||
description: z.string().optional(),
|
||||
qty: z.number().default(1),
|
||||
unit_cost: z.string().describe('Unit cost'),
|
||||
})).describe('Invoice line items'),
|
||||
currency_code: z.string().default('USD'),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const lines = args.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: args.currency_code },
|
||||
}));
|
||||
|
||||
const recurringData = {
|
||||
recurring: {
|
||||
clientid: args.clientid,
|
||||
frequency: args.frequency,
|
||||
numberRecurring: args.numberRecurring || 0,
|
||||
create_date: new Date().toISOString().split('T')[0],
|
||||
currency_code: args.currency_code,
|
||||
lines,
|
||||
notes: args.notes,
|
||||
terms: args.terms,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
'/invoices/recurring',
|
||||
recurringData
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_recurring_profile',
|
||||
description: 'Update an existing recurring profile',
|
||||
inputSchema: z.object({
|
||||
recurring_id: z.number().describe('Recurring profile ID'),
|
||||
frequency: z.enum(['weekly', 'biweekly', 'monthly', 'quarterly', 'yearly']).optional(),
|
||||
numberRecurring: z.number().optional(),
|
||||
lines: z.array(z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
qty: z.number(),
|
||||
unit_cost: z.string(),
|
||||
})).optional(),
|
||||
notes: z.string().optional(),
|
||||
terms: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { recurring_id, ...updateFields } = args;
|
||||
|
||||
if (updateFields.lines) {
|
||||
updateFields.lines = updateFields.lines.map((line: any) => ({
|
||||
...line,
|
||||
unit_cost: { amount: line.unit_cost, code: 'USD' },
|
||||
}));
|
||||
}
|
||||
|
||||
const recurringData = { recurring: updateFields };
|
||||
const response = await client.put<{ response: { result: { recurring: RecurringProfile } } }>(
|
||||
`/invoices/recurring/${recurring_id}`,
|
||||
recurringData
|
||||
);
|
||||
return response.response.result.recurring;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_recurring_profile',
|
||||
description: 'Delete a recurring profile',
|
||||
inputSchema: z.object({
|
||||
recurring_id: z.number().describe('Recurring profile ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/invoices/recurring/${args.recurring_id}`,
|
||||
{ recurring: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Recurring profile ${args.recurring_id} deleted` };
|
||||
},
|
||||
},
|
||||
];
|
||||
112
servers/freshbooks/src/tools/reports-tools.ts
Normal file
112
servers/freshbooks/src/tools/reports-tools.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { ProfitLossReport, TaxSummary, AccountsAgingReport } from '../types/index.js';
|
||||
|
||||
export const reportsTools = [
|
||||
{
|
||||
name: 'freshbooks_profit_loss_report',
|
||||
description: 'Generate profit and loss report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: ProfitLossReport } }>(
|
||||
'/reports/accounting/profitloss',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_tax_summary_report',
|
||||
description: 'Generate tax summary report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { taxsummaries: TaxSummary[] } } }>(
|
||||
'/reports/accounting/taxsummary',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result.taxsummaries || [];
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_accounts_aging_report',
|
||||
description: 'Generate accounts aging report (accounts receivable)',
|
||||
inputSchema: z.object({
|
||||
date: z.string().optional().describe('Report date (YYYY-MM-DD, defaults to today)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { clients: AccountsAgingReport[] } } }>(
|
||||
'/reports/accounting/aging',
|
||||
{
|
||||
date: args.date || new Date().toISOString().split('T')[0],
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result.clients || [];
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_expense_report',
|
||||
description: 'Generate expense report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
categoryid: z.number().optional().describe('Filter by category ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: any = {
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
};
|
||||
if (args.clientid) params.clientid = args.clientid;
|
||||
if (args.categoryid) params.categoryid = args.categoryid;
|
||||
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/reports/accounting/expenses',
|
||||
params
|
||||
);
|
||||
return response.response.result;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_revenue_by_client_report',
|
||||
description: 'Generate revenue by client report for a date range',
|
||||
inputSchema: z.object({
|
||||
start_date: z.string().describe('Start date (YYYY-MM-DD)'),
|
||||
end_date: z.string().describe('End date (YYYY-MM-DD)'),
|
||||
currency_code: z.string().default('USD'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: any } }>(
|
||||
'/reports/accounting/revenue_by_client',
|
||||
{
|
||||
start_date: args.start_date,
|
||||
end_date: args.end_date,
|
||||
currency_code: args.currency_code,
|
||||
}
|
||||
);
|
||||
return response.response.result;
|
||||
},
|
||||
},
|
||||
];
|
||||
96
servers/freshbooks/src/tools/taxes-tools.ts
Normal file
96
servers/freshbooks/src/tools/taxes-tools.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { Tax } from '../types/index.js';
|
||||
|
||||
export const taxesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_taxes',
|
||||
description: 'List all taxes',
|
||||
inputSchema: z.object({
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.getPaginated<{ taxes: Tax[] }>(
|
||||
'/taxes/taxes',
|
||||
args.page,
|
||||
args.per_page
|
||||
);
|
||||
return {
|
||||
taxes: response.response.result.taxes || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_tax',
|
||||
description: 'Get a single tax by ID',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { tax: Tax } } }>(
|
||||
`/taxes/taxes/${args.tax_id}`
|
||||
);
|
||||
return response.response.result.tax;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_tax',
|
||||
description: 'Create a new tax',
|
||||
inputSchema: z.object({
|
||||
name: z.string().describe('Tax name (e.g., "GST", "VAT")'),
|
||||
number: z.string().optional().describe('Tax number/registration'),
|
||||
amount: z.string().describe('Tax percentage (e.g., "13" for 13%)'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const taxData = { tax: { ...args } };
|
||||
|
||||
const response = await client.post<{ response: { result: { tax: Tax } } }>(
|
||||
'/taxes/taxes',
|
||||
taxData
|
||||
);
|
||||
return response.response.result.tax;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_tax',
|
||||
description: 'Update an existing tax',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
name: z.string().optional(),
|
||||
number: z.string().optional(),
|
||||
amount: z.string().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { tax_id, ...updateFields } = args;
|
||||
const taxData = { tax: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { tax: Tax } } }>(
|
||||
`/taxes/taxes/${tax_id}`,
|
||||
taxData
|
||||
);
|
||||
return response.response.result.tax;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_tax',
|
||||
description: 'Delete a tax',
|
||||
inputSchema: z.object({
|
||||
tax_id: z.number().describe('Tax ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.put(
|
||||
`/taxes/taxes/${args.tax_id}`,
|
||||
{ tax: { vis_state: 1 } }
|
||||
);
|
||||
return { success: true, message: `Tax ${args.tax_id} deleted` };
|
||||
},
|
||||
},
|
||||
];
|
||||
125
servers/freshbooks/src/tools/time-entries-tools.ts
Normal file
125
servers/freshbooks/src/tools/time-entries-tools.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { z } from 'zod';
|
||||
import type { FreshBooksClient } from '../clients/freshbooks.js';
|
||||
import type { TimeEntry } from '../types/index.js';
|
||||
|
||||
export const timeEntriesTools = [
|
||||
{
|
||||
name: 'freshbooks_list_time_entries',
|
||||
description: 'List all time entries with optional filtering',
|
||||
inputSchema: z.object({
|
||||
clientid: z.number().optional().describe('Filter by client ID'),
|
||||
projectid: z.number().optional().describe('Filter by project ID'),
|
||||
date_min: z.string().optional().describe('Start date (YYYY-MM-DD)'),
|
||||
date_max: z.string().optional().describe('End date (YYYY-MM-DD)'),
|
||||
billed_status: z.enum(['billed', 'unbilled']).optional(),
|
||||
page: z.number().default(1),
|
||||
per_page: z.number().default(30),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const params: Record<string, any> = {};
|
||||
if (args.clientid) params.client_id = args.clientid;
|
||||
if (args.projectid) params.project_id = args.projectid;
|
||||
if (args.date_min) params.started_from = args.date_min;
|
||||
if (args.date_max) params.started_to = args.date_max;
|
||||
if (args.billed_status) params.billed_status = args.billed_status;
|
||||
|
||||
const response = await client.getPaginated<{ time_entries: TimeEntry[] }>(
|
||||
'/timetracking/business/123/time_entries',
|
||||
args.page,
|
||||
args.per_page,
|
||||
params
|
||||
);
|
||||
return {
|
||||
time_entries: response.response.result.time_entries || [],
|
||||
page: response.response.page,
|
||||
pages: response.response.pages,
|
||||
total: response.response.total,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_get_time_entry',
|
||||
description: 'Get a single time entry by ID',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const response = await client.get<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
`/timetracking/business/123/time_entries/${args.time_entry_id}`
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_create_time_entry',
|
||||
description: 'Create a new time entry',
|
||||
inputSchema: z.object({
|
||||
duration: z.number().describe('Duration in seconds'),
|
||||
note: z.string().optional().describe('Note/description'),
|
||||
started_at: z.string().describe('Start time (ISO 8601 format)'),
|
||||
clientid: z.number().optional().describe('Client ID'),
|
||||
projectid: z.number().optional().describe('Project ID'),
|
||||
service_id: z.number().optional().describe('Service ID'),
|
||||
is_logged: z.boolean().default(true),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const timeEntryData = {
|
||||
time_entry: {
|
||||
is_logged: args.is_logged,
|
||||
duration: args.duration,
|
||||
note: args.note,
|
||||
started_at: args.started_at,
|
||||
client_id: args.clientid,
|
||||
project_id: args.projectid,
|
||||
service_id: args.service_id,
|
||||
},
|
||||
};
|
||||
|
||||
const response = await client.post<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
'/timetracking/business/123/time_entries',
|
||||
timeEntryData
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_update_time_entry',
|
||||
description: 'Update an existing time entry',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
duration: z.number().optional(),
|
||||
note: z.string().optional(),
|
||||
started_at: z.string().optional(),
|
||||
clientid: z.number().optional(),
|
||||
projectid: z.number().optional(),
|
||||
service_id: z.number().optional(),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
const { time_entry_id, ...updateFields } = args;
|
||||
const timeEntryData = { time_entry: updateFields };
|
||||
|
||||
const response = await client.put<{ response: { result: { time_entry: TimeEntry } } }>(
|
||||
`/timetracking/business/123/time_entries/${time_entry_id}`,
|
||||
timeEntryData
|
||||
);
|
||||
return response.response.result.time_entry;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
name: 'freshbooks_delete_time_entry',
|
||||
description: 'Delete a time entry',
|
||||
inputSchema: z.object({
|
||||
time_entry_id: z.number().describe('Time entry ID'),
|
||||
}),
|
||||
handler: async (args: any, client: FreshBooksClient) => {
|
||||
await client.delete(
|
||||
`/timetracking/business/123/time_entries/${args.time_entry_id}`
|
||||
);
|
||||
return { success: true, message: `Time entry ${args.time_entry_id} deleted` };
|
||||
},
|
||||
},
|
||||
];
|
||||
349
servers/freshbooks/src/types/index.ts
Normal file
349
servers/freshbooks/src/types/index.ts
Normal file
@ -0,0 +1,349 @@
|
||||
// FreshBooks API Types
|
||||
|
||||
export interface FreshBooksConfig {
|
||||
accountId: string;
|
||||
bearerToken: string;
|
||||
baseUrl?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
response: {
|
||||
result: T;
|
||||
page: number;
|
||||
pages: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FreshBooksError {
|
||||
message: string;
|
||||
code?: string;
|
||||
errors?: Array<{ field: string; message: string }>;
|
||||
}
|
||||
|
||||
// Client Types
|
||||
export interface Client {
|
||||
id: number;
|
||||
accounting_systemid: string;
|
||||
organization: string;
|
||||
fname: string;
|
||||
lname: string;
|
||||
email: string;
|
||||
company_industry?: string;
|
||||
company_size?: string;
|
||||
bill_street?: string;
|
||||
bill_city?: string;
|
||||
bill_state?: string;
|
||||
bill_country?: string;
|
||||
bill_postal_code?: string;
|
||||
currency_code: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
language?: string;
|
||||
note?: string;
|
||||
vat_name?: string;
|
||||
vat_number?: string;
|
||||
allow_late_fees?: boolean;
|
||||
allow_late_notifications?: boolean;
|
||||
}
|
||||
|
||||
export interface ClientContact {
|
||||
id: number;
|
||||
clientid: number;
|
||||
fname: string;
|
||||
lname: string;
|
||||
email: string;
|
||||
phone?: string;
|
||||
mobile?: string;
|
||||
}
|
||||
|
||||
// Invoice Types
|
||||
export interface Invoice {
|
||||
id: number;
|
||||
accountid: string;
|
||||
accounting_systemid: string;
|
||||
clientid: number;
|
||||
create_date: string;
|
||||
invoice_number: string;
|
||||
currency_code: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
outstanding: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
paid: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
due_date: string;
|
||||
status: string;
|
||||
payment_status: string;
|
||||
v3_status: string;
|
||||
lines: InvoiceLine[];
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
discount_total?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface InvoiceLine {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: number;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
taxName1?: string;
|
||||
taxAmount1?: string;
|
||||
taxName2?: string;
|
||||
taxAmount2?: string;
|
||||
}
|
||||
|
||||
export interface Payment {
|
||||
id: number;
|
||||
invoiceid: number;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
date: string;
|
||||
type: string;
|
||||
note?: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Expense Types
|
||||
export interface Expense {
|
||||
id: number;
|
||||
category_id: number;
|
||||
clientid?: number;
|
||||
projectid?: number;
|
||||
vendor: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
date: string;
|
||||
notes?: string;
|
||||
taxName1?: string;
|
||||
taxAmount1?: number;
|
||||
taxPercent1?: number;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
staffid?: number;
|
||||
status?: string;
|
||||
}
|
||||
|
||||
export interface ExpenseCategory {
|
||||
id: number;
|
||||
category: string;
|
||||
is_cogs?: boolean;
|
||||
is_editable?: boolean;
|
||||
parentid?: number;
|
||||
}
|
||||
|
||||
// Estimate Types
|
||||
export interface Estimate {
|
||||
id: number;
|
||||
accountid: string;
|
||||
clientid: number;
|
||||
create_date: string;
|
||||
estimate_number: string;
|
||||
currency_code: string;
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
status: string;
|
||||
lines: EstimateLine[];
|
||||
terms?: string;
|
||||
notes?: string;
|
||||
discount_total?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface EstimateLine {
|
||||
id?: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty: number;
|
||||
unit_cost: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
amount: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Time Entry Types
|
||||
export interface TimeEntry {
|
||||
id: number;
|
||||
is_logged?: boolean;
|
||||
duration: number;
|
||||
note?: string;
|
||||
started_at: string;
|
||||
clientid?: number;
|
||||
projectid?: number;
|
||||
service_id?: number;
|
||||
billed_status?: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// Project Types
|
||||
export interface Project {
|
||||
id: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
client_id?: number;
|
||||
due_date?: string;
|
||||
project_type?: string;
|
||||
fixed_price?: string;
|
||||
billing_method?: string;
|
||||
rate?: string;
|
||||
active?: boolean;
|
||||
complete?: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ProjectService {
|
||||
id: number;
|
||||
business_id: number;
|
||||
name: string;
|
||||
billable: boolean;
|
||||
rate?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Item Types
|
||||
export interface Item {
|
||||
id: number;
|
||||
name: string;
|
||||
description?: string;
|
||||
qty?: number;
|
||||
inventory?: number;
|
||||
unit_cost?: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
tax1?: number;
|
||||
tax2?: number;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Tax Types
|
||||
export interface Tax {
|
||||
id: number;
|
||||
name: string;
|
||||
number?: string;
|
||||
amount: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Recurring Profile Types
|
||||
export interface RecurringProfile {
|
||||
id: number;
|
||||
clientid: number;
|
||||
frequency: string;
|
||||
numberRecurring: number;
|
||||
create_date: string;
|
||||
currency_code: string;
|
||||
lines: InvoiceLine[];
|
||||
status?: string;
|
||||
updated: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// Account Types
|
||||
export interface Account {
|
||||
id: string;
|
||||
account_name: string;
|
||||
email: string;
|
||||
business_phone?: string;
|
||||
address?: {
|
||||
street: string;
|
||||
city: string;
|
||||
province: string;
|
||||
country: string;
|
||||
postal_code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StaffMember {
|
||||
id: number;
|
||||
username: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
// Report Types
|
||||
export interface ProfitLossReport {
|
||||
total_income: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
total_expenses: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
net_profit: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
start_date: string;
|
||||
end_date: string;
|
||||
}
|
||||
|
||||
export interface TaxSummary {
|
||||
tax_name: string;
|
||||
tax_collected: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
tax_paid: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AccountsAgingReport {
|
||||
client_userid: number;
|
||||
organization: string;
|
||||
outstanding_balance: {
|
||||
amount: string;
|
||||
code: string;
|
||||
};
|
||||
current: { amount: string; code: string };
|
||||
'1-30': { amount: string; code: string };
|
||||
'31-60': { amount: string; code: string };
|
||||
'61-90': { amount: string; code: string };
|
||||
'91+': { amount: string; code: string };
|
||||
}
|
||||
34
servers/freshbooks/src/ui/react-app/aging-report/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/aging-report/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,99 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Clients Dashboard - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.search-bar { margin-bottom: 1.5rem; }
|
||||
.search-bar input { width: 100%; background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
|
||||
.client-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
|
||||
.client-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
|
||||
.client-card h3 { margin-bottom: 0.5rem; }
|
||||
.client-card .email { color: #64748b; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.client-meta { display: flex; justify-content: space-between; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #334155; }
|
||||
.client-meta div { text-align: center; }
|
||||
.client-meta .label { color: #64748b; font-size: 0.75rem; margin-bottom: 0.25rem; }
|
||||
.client-meta .value { font-weight: bold; font-size: 1.125rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function ClientDashboard() {
|
||||
const [clients, setClients] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
const sampleClients = [
|
||||
{ id: 1, name: 'Acme Corp', email: 'contact@acme.com', invoices: 12, outstanding: 5600, lastInvoice: '2024-01-15' },
|
||||
{ id: 2, name: 'Tech Solutions', email: 'hello@tech.com', invoices: 8, outstanding: 3200, lastInvoice: '2024-01-20' },
|
||||
{ id: 3, name: 'Design Co', email: 'info@design.co', invoices: 15, outstanding: 0, lastInvoice: '2024-01-10' },
|
||||
{ id: 4, name: 'Marketing Inc', email: 'team@marketing.com', invoices: 6, outstanding: 1800, lastInvoice: '2024-01-25' },
|
||||
];
|
||||
setClients(sampleClients);
|
||||
}, []);
|
||||
|
||||
const filteredClients = clients.filter(client =>
|
||||
client.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
client.email.toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div>
|
||||
<h1>Clients</h1>
|
||||
<p style={{ color: '#94a3b8' }}>{clients.length} active clients</p>
|
||||
</div>
|
||||
<button className="btn">+ Add Client</button>
|
||||
</div>
|
||||
|
||||
<div className="search-bar">
|
||||
<input type="text" placeholder="Search clients..." value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
|
||||
<div className="client-grid">
|
||||
{filteredClients.map(client => (
|
||||
<div key={client.id} className="client-card">
|
||||
<h3>{client.name}</h3>
|
||||
<div className="email">{client.email}</div>
|
||||
<div className="client-meta">
|
||||
<div>
|
||||
<div className="label">Invoices</div>
|
||||
<div className="value">{client.invoices}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="label">Outstanding</div>
|
||||
<div className="value" style={{ color: client.outstanding > 0 ? '#fbbf24' : '#6ee7b7' }}>
|
||||
${client.outstanding.toLocaleString()}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="label">Last Invoice</div>
|
||||
<div className="value" style={{ fontSize: '0.875rem' }}>{client.lastInvoice}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ClientDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/client-detail/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/client-detail/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/client-grid/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/client-grid/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/estimate-grid/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/estimate-grid/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
134
servers/freshbooks/src/ui/react-app/expense-tracker/index.html
Normal file
134
servers/freshbooks/src/ui/react-app/expense-tracker/index.html
Normal file
@ -0,0 +1,134 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Expense Tracker - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
|
||||
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
|
||||
.form-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-bottom: 2rem; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.form-group.full { grid-column: 1 / -1; }
|
||||
label { font-size: 0.875rem; color: #94a3b8; }
|
||||
input, select, textarea { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.expense-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
.expense-item { padding: 1rem; border-bottom: 1px solid #334155; display: flex; justify-content: space-between; align-items: center; }
|
||||
.expense-item:last-child { border-bottom: none; }
|
||||
.expense-info .category { color: #64748b; font-size: 0.875rem; }
|
||||
.expense-amount { font-weight: bold; font-size: 1.125rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function ExpenseTracker() {
|
||||
const [expenses, setExpenses] = useState([
|
||||
{ id: 1, vendor: 'Office Supplies Co', category: 'Office', amount: 245, date: '2024-01-15' },
|
||||
{ id: 2, vendor: 'Tech Store', category: 'Equipment', amount: 1200, date: '2024-01-18' },
|
||||
{ id: 3, vendor: 'Coffee Shop', category: 'Meals', amount: 45, date: '2024-01-20' },
|
||||
]);
|
||||
|
||||
const [form, setForm] = useState({
|
||||
vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '',
|
||||
});
|
||||
|
||||
const totalExpenses = expenses.reduce((sum, exp) => sum + exp.amount, 0);
|
||||
const thisMonth = expenses.filter(e => e.date.startsWith('2024-01')).reduce((sum, exp) => sum + exp.amount, 0);
|
||||
|
||||
const addExpense = () => {
|
||||
if (form.vendor && form.amount) {
|
||||
setExpenses([...expenses, { ...form, id: Date.now(), amount: parseFloat(form.amount) }]);
|
||||
setForm({ vendor: '', category: 'Office', amount: '', date: new Date().toISOString().split('T')[0], notes: '' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Expense Tracker</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Track and categorize business expenses</p>
|
||||
</div>
|
||||
|
||||
<div className="stats">
|
||||
<div className="stat-card">
|
||||
<div className="value">${totalExpenses.toLocaleString()}</div>
|
||||
<div className="label">Total Expenses</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">${thisMonth.toLocaleString()}</div>
|
||||
<div className="label">This Month</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">{expenses.length}</div>
|
||||
<div className="label">Transactions</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="form-card">
|
||||
<h3 style={{ marginBottom: '1rem' }}>Add Expense</h3>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>Vendor</label>
|
||||
<input type="text" value={form.vendor} onChange={(e) => setForm({ ...form, vendor: e.target.value })} placeholder="Vendor name" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Category</label>
|
||||
<select value={form.category} onChange={(e) => setForm({ ...form, category: e.target.value })}>
|
||||
<option value="Office">Office Supplies</option>
|
||||
<option value="Equipment">Equipment</option>
|
||||
<option value="Meals">Meals & Entertainment</option>
|
||||
<option value="Travel">Travel</option>
|
||||
<option value="Software">Software</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Amount</label>
|
||||
<input type="number" value={form.amount} onChange={(e) => setForm({ ...form, amount: e.target.value })} placeholder="0.00" />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" value={form.date} onChange={(e) => setForm({ ...form, date: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group full">
|
||||
<label>Notes</label>
|
||||
<textarea rows="2" value={form.notes} onChange={(e) => setForm({ ...form, notes: e.target.value })} placeholder="Optional notes" />
|
||||
</div>
|
||||
</div>
|
||||
<button className="btn" style={{ marginTop: '1rem' }} onClick={addExpense}>Add Expense</button>
|
||||
</div>
|
||||
|
||||
<div className="expense-list">
|
||||
{expenses.map(expense => (
|
||||
<div key={expense.id} className="expense-item">
|
||||
<div className="expense-info">
|
||||
<div>{expense.vendor}</div>
|
||||
<div className="category">{expense.category} • {expense.date}</div>
|
||||
</div>
|
||||
<div className="expense-amount">${expense.amount.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ExpenseTracker />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
139
servers/freshbooks/src/ui/react-app/invoice-builder/index.html
Normal file
139
servers/freshbooks/src/ui/react-app/invoice-builder/index.html
Normal file
@ -0,0 +1,139 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Builder - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.form-grid { display: grid; gap: 1.5rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
label { font-size: 0.875rem; color: #94a3b8; font-weight: 500; }
|
||||
input, select, textarea { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; font-size: 1rem; }
|
||||
input:focus, select:focus, textarea:focus { outline: none; border-color: #3b82f6; }
|
||||
.line-items { margin-top: 2rem; }
|
||||
.line-item { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 40px; gap: 1rem; margin-bottom: 1rem; align-items: end; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 500; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.btn-secondary { background: #334155; }
|
||||
.btn-secondary:hover { background: #475569; }
|
||||
.btn-danger { background: #ef4444; padding: 0.5rem; }
|
||||
.btn-danger:hover { background: #dc2626; }
|
||||
.total-section { background: #1e293b; padding: 1.5rem; border-radius: 8px; margin-top: 2rem; }
|
||||
.total-row { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
|
||||
.total-row.final { font-size: 1.25rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function InvoiceBuilder() {
|
||||
const [invoice, setInvoice] = useState({
|
||||
client: '',
|
||||
invoiceNumber: 'INV-' + Date.now(),
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
dueDate: '',
|
||||
lines: [{ description: '', qty: 1, rate: 0 }],
|
||||
notes: '',
|
||||
});
|
||||
|
||||
const addLine = () => {
|
||||
setInvoice({ ...invoice, lines: [...invoice.lines, { description: '', qty: 1, rate: 0 }] });
|
||||
};
|
||||
|
||||
const removeLine = (index) => {
|
||||
setInvoice({ ...invoice, lines: invoice.lines.filter((_, i) => i !== index) });
|
||||
};
|
||||
|
||||
const updateLine = (index, field, value) => {
|
||||
const newLines = [...invoice.lines];
|
||||
newLines[index] = { ...newLines[index], [field]: value };
|
||||
setInvoice({ ...invoice, lines: newLines });
|
||||
};
|
||||
|
||||
const subtotal = invoice.lines.reduce((sum, line) => sum + (line.qty * line.rate), 0);
|
||||
const tax = subtotal * 0.13; // 13% tax
|
||||
const total = subtotal + tax;
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Create Invoice</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Build and send professional invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>Client</label>
|
||||
<select value={invoice.client} onChange={(e) => setInvoice({ ...invoice, client: e.target.value })}>
|
||||
<option value="">Select client...</option>
|
||||
<option value="acme">Acme Corp</option>
|
||||
<option value="tech">Tech Solutions</option>
|
||||
<option value="design">Design Co</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: '1rem' }}>
|
||||
<div className="form-group">
|
||||
<label>Invoice #</label>
|
||||
<input type="text" value={invoice.invoiceNumber} onChange={(e) => setInvoice({ ...invoice, invoiceNumber: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Date</label>
|
||||
<input type="date" value={invoice.date} onChange={(e) => setInvoice({ ...invoice, date: e.target.value })} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Due Date</label>
|
||||
<input type="date" value={invoice.dueDate} onChange={(e) => setInvoice({ ...invoice, dueDate: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="line-items">
|
||||
<label>Line Items</label>
|
||||
{invoice.lines.map((line, index) => (
|
||||
<div key={index} className="line-item">
|
||||
<input type="text" placeholder="Description" value={line.description} onChange={(e) => updateLine(index, 'description', e.target.value)} />
|
||||
<input type="number" placeholder="Qty" value={line.qty} onChange={(e) => updateLine(index, 'qty', parseFloat(e.target.value))} />
|
||||
<input type="number" placeholder="Rate" value={line.rate} onChange={(e) => updateLine(index, 'rate', parseFloat(e.target.value))} />
|
||||
<div style={{ color: '#94a3b8', paddingTop: '0.75rem' }}>${(line.qty * line.rate).toFixed(2)}</div>
|
||||
{invoice.lines.length > 1 && (
|
||||
<button className="btn btn-danger" onClick={() => removeLine(index)}>×</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<button className="btn btn-secondary" onClick={addLine}>+ Add Line</button>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<label>Notes</label>
|
||||
<textarea rows="3" value={invoice.notes} onChange={(e) => setInvoice({ ...invoice, notes: e.target.value })} placeholder="Payment terms, thank you note, etc." />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="total-section">
|
||||
<div className="total-row"><span>Subtotal:</span><span>${subtotal.toFixed(2)}</span></div>
|
||||
<div className="total-row"><span>Tax (13%):</span><span>${tax.toFixed(2)}</span></div>
|
||||
<div className="total-row final"><span>Total:</span><span>${total.toFixed(2)}</span></div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: '2rem', display: 'flex', gap: '1rem' }}>
|
||||
<button className="btn">Save Draft</button>
|
||||
<button className="btn" style={{ background: '#10b981' }}>Send Invoice</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceBuilder />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
154
servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html
Normal file
154
servers/freshbooks/src/ui/react-app/invoice-dashboard/index.html
Normal file
@ -0,0 +1,154 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Dashboard - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { margin-bottom: 2rem; }
|
||||
.header h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 2rem; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-left: 4px solid #3b82f6; }
|
||||
.stat-card h3 { color: #94a3b8; font-size: 0.875rem; text-transform: uppercase; margin-bottom: 0.5rem; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; color: #e2e8f0; }
|
||||
.stat-card .label { color: #64748b; font-size: 0.875rem; margin-top: 0.25rem; }
|
||||
.filters { display: flex; gap: 1rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
|
||||
.filter-group { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.filter-group label { font-size: 0.875rem; color: #94a3b8; }
|
||||
.filter-group select, .filter-group input { background: #1e293b; border: 1px solid #334155; color: #e2e8f0; padding: 0.5rem; border-radius: 4px; }
|
||||
.table-container { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { background: #0f172a; text-align: left; padding: 1rem; font-weight: 600; color: #94a3b8; text-transform: uppercase; font-size: 0.75rem; }
|
||||
td { padding: 1rem; border-top: 1px solid #334155; }
|
||||
.status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
||||
.status-paid { background: #065f46; color: #6ee7b7; }
|
||||
.status-partial { background: #78350f; color: #fbbf24; }
|
||||
.status-overdue { background: #7f1d1d; color: #fca5a5; }
|
||||
.status-draft { background: #1e3a8a; color: #93c5fd; }
|
||||
.amount { font-weight: 600; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; font-size: 0.875rem; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function InvoiceDashboard() {
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
const [stats, setStats] = useState({ total: 0, paid: 0, overdue: 0, draft: 0 });
|
||||
const [filter, setFilter] = useState({ status: 'all', client: '' });
|
||||
|
||||
useEffect(() => {
|
||||
// Simulate data fetch
|
||||
const sampleInvoices = [
|
||||
{ id: 1, number: 'INV-001', client: 'Acme Corp', amount: 2500, status: 'paid', date: '2024-01-15', dueDate: '2024-02-15' },
|
||||
{ id: 2, number: 'INV-002', client: 'Tech Solutions', amount: 4200, status: 'overdue', date: '2024-01-10', dueDate: '2024-02-10' },
|
||||
{ id: 3, number: 'INV-003', client: 'Design Co', amount: 1800, status: 'partial', date: '2024-01-20', dueDate: '2024-02-20' },
|
||||
{ id: 4, number: 'INV-004', client: 'Marketing Inc', amount: 3600, status: 'draft', date: '2024-01-25', dueDate: '2024-02-25' },
|
||||
];
|
||||
setInvoices(sampleInvoices);
|
||||
setStats({
|
||||
total: sampleInvoices.reduce((sum, inv) => sum + inv.amount, 0),
|
||||
paid: sampleInvoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + inv.amount, 0),
|
||||
overdue: sampleInvoices.filter(i => i.status === 'overdue').reduce((sum, inv) => sum + inv.amount, 0),
|
||||
draft: sampleInvoices.filter(i => i.status === 'draft').length,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const filteredInvoices = invoices.filter(inv => {
|
||||
if (filter.status !== 'all' && inv.status !== filter.status) return false;
|
||||
if (filter.client && !inv.client.toLowerCase().includes(filter.client.toLowerCase())) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<h1>Invoice Dashboard</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Manage and track all your invoices</p>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<h3>Total Revenue</h3>
|
||||
<div className="value">${stats.total.toLocaleString()}</div>
|
||||
<div className="label">All invoices</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#10b981' }}>
|
||||
<h3>Paid</h3>
|
||||
<div className="value">${stats.paid.toLocaleString()}</div>
|
||||
<div className="label">Received</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#ef4444' }}>
|
||||
<h3>Overdue</h3>
|
||||
<div className="value">${stats.overdue.toLocaleString()}</div>
|
||||
<div className="label">Past due</div>
|
||||
</div>
|
||||
<div className="stat-card" style={{ borderLeftColor: '#f59e0b' }}>
|
||||
<h3>Drafts</h3>
|
||||
<div className="value">{stats.draft}</div>
|
||||
<div className="label">Pending</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="filters">
|
||||
<div className="filter-group">
|
||||
<label>Status</label>
|
||||
<select value={filter.status} onChange={(e) => setFilter({ ...filter, status: e.target.value })}>
|
||||
<option value="all">All Statuses</option>
|
||||
<option value="paid">Paid</option>
|
||||
<option value="partial">Partial</option>
|
||||
<option value="overdue">Overdue</option>
|
||||
<option value="draft">Draft</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="filter-group">
|
||||
<label>Client</label>
|
||||
<input type="text" placeholder="Search client..." value={filter.client} onChange={(e) => setFilter({ ...filter, client: e.target.value })} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-container">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Invoice #</th>
|
||||
<th>Client</th>
|
||||
<th>Date</th>
|
||||
<th>Due Date</th>
|
||||
<th>Amount</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredInvoices.map(invoice => (
|
||||
<tr key={invoice.id}>
|
||||
<td>{invoice.number}</td>
|
||||
<td>{invoice.client}</td>
|
||||
<td>{invoice.date}</td>
|
||||
<td>{invoice.dueDate}</td>
|
||||
<td className="amount">${invoice.amount.toLocaleString()}</td>
|
||||
<td><span className={`status status-${invoice.status}`}>{invoice.status}</span></td>
|
||||
<td><button className="btn">View</button></td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
116
servers/freshbooks/src/ui/react-app/invoice-detail/index.html
Normal file
116
servers/freshbooks/src/ui/react-app/invoice-detail/index.html
Normal file
@ -0,0 +1,116 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Invoice Detail - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 800px; margin: 0 auto; padding: 2rem; }
|
||||
.invoice-header { background: #1e293b; padding: 2rem; border-radius: 8px; margin-bottom: 2rem; }
|
||||
.invoice-header h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.status-badge { padding: 0.5rem 1rem; border-radius: 12px; display: inline-block; background: #10b981; color: white; }
|
||||
.invoice-meta { display: grid; grid-template-columns: 1fr 1fr; gap: 2rem; margin-top: 1.5rem; }
|
||||
.invoice-body { background: #1e293b; padding: 2rem; border-radius: 8px; }
|
||||
table { width: 100%; margin-bottom: 2rem; }
|
||||
th { text-align: left; padding: 0.75rem; background: #0f172a; color: #94a3b8; }
|
||||
td { padding: 0.75rem; border-top: 1px solid #334155; }
|
||||
.total-section { text-align: right; }
|
||||
.total-row { display: flex; justify-content: flex-end; gap: 2rem; margin-bottom: 0.5rem; }
|
||||
.total-row.final { font-size: 1.5rem; font-weight: bold; padding-top: 1rem; border-top: 2px solid #334155; }
|
||||
.actions { margin-top: 2rem; display: flex; gap: 1rem; }
|
||||
.btn { padding: 0.75rem 1.5rem; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem; }
|
||||
.btn-primary { background: #3b82f6; color: white; }
|
||||
.btn-secondary { background: #334155; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function InvoiceDetail() {
|
||||
const invoice = {
|
||||
number: 'INV-001',
|
||||
client: 'Acme Corp',
|
||||
date: '2024-01-15',
|
||||
dueDate: '2024-02-15',
|
||||
status: 'Paid',
|
||||
lines: [
|
||||
{ description: 'Website Design', qty: 1, rate: 2000, amount: 2000 },
|
||||
{ description: 'Logo Design', qty: 1, rate: 500, amount: 500 },
|
||||
],
|
||||
subtotal: 2500,
|
||||
tax: 325,
|
||||
total: 2825,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="invoice-header">
|
||||
<h1>{invoice.number}</h1>
|
||||
<span className="status-badge">{invoice.status}</span>
|
||||
<div className="invoice-meta">
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Client</div>
|
||||
<div style={{ fontSize: '1.125rem', fontWeight: '500' }}>{invoice.client}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Amount</div>
|
||||
<div style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>${invoice.total}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Invoice Date</div>
|
||||
<div>{invoice.date}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={{ color: '#94a3b8', fontSize: '0.875rem' }}>Due Date</div>
|
||||
<div>{invoice.dueDate}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="invoice-body">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<th>Qty</th>
|
||||
<th>Rate</th>
|
||||
<th style={{ textAlign: 'right' }}>Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{invoice.lines.map((line, i) => (
|
||||
<tr key={i}>
|
||||
<td>{line.description}</td>
|
||||
<td>{line.qty}</td>
|
||||
<td>${line.rate}</td>
|
||||
<td style={{ textAlign: 'right' }}>${line.amount}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div className="total-section">
|
||||
<div className="total-row"><span>Subtotal:</span><span>${invoice.subtotal}</span></div>
|
||||
<div className="total-row"><span>Tax (13%):</span><span>${invoice.tax}</span></div>
|
||||
<div className="total-row final"><span>Total:</span><span>${invoice.total}</span></div>
|
||||
</div>
|
||||
|
||||
<div className="actions">
|
||||
<button className="btn btn-primary">Send Invoice</button>
|
||||
<button className="btn btn-secondary">Download PDF</button>
|
||||
<button className="btn btn-secondary">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<InvoiceDetail />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/invoice-grid/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/invoice-grid/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,86 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Payment History - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin: 2rem 0; }
|
||||
.stat-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; text-align: center; }
|
||||
.stat-card .value { font-size: 2rem; font-weight: bold; margin-bottom: 0.25rem; }
|
||||
.stat-card .label { color: #94a3b8; font-size: 0.875rem; }
|
||||
.payment-list { background: #1e293b; border-radius: 8px; overflow: hidden; }
|
||||
.payment-item { padding: 1.5rem; border-bottom: 1px solid #334155; display: grid; grid-template-columns: 1fr 2fr 1fr 1fr 1fr; gap: 1rem; align-items: center; }
|
||||
.payment-item:last-child { border-bottom: none; }
|
||||
.payment-date { color: #94a3b8; }
|
||||
.payment-invoice { font-weight: 500; }
|
||||
.payment-client { color: #64748b; font-size: 0.875rem; }
|
||||
.payment-method { padding: 0.25rem 0.75rem; background: #0f172a; border-radius: 4px; font-size: 0.875rem; text-align: center; }
|
||||
.payment-amount { font-size: 1.25rem; font-weight: bold; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function PaymentHistory() {
|
||||
const [payments] = useState([
|
||||
{ id: 1, date: '2024-01-25', invoice: 'INV-001', client: 'Acme Corp', method: 'Credit Card', amount: 2500 },
|
||||
{ id: 2, date: '2024-01-23', invoice: 'INV-005', client: 'Tech Solutions', method: 'Bank Transfer', amount: 4200 },
|
||||
{ id: 3, date: '2024-01-20', invoice: 'INV-003', client: 'Design Co', method: 'PayPal', amount: 1800 },
|
||||
{ id: 4, date: '2024-01-18', invoice: 'INV-007', client: 'Marketing Inc', method: 'Check', amount: 3600 },
|
||||
{ id: 5, date: '2024-01-15', invoice: 'INV-002', client: 'Acme Corp', method: 'Credit Card', amount: 5000 },
|
||||
]);
|
||||
|
||||
const totalReceived = payments.reduce((sum, p) => sum + p.amount, 0);
|
||||
const thisMonth = payments.filter(p => p.date.startsWith('2024-01')).reduce((sum, p) => sum + p.amount, 0);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Payment History</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Track all received payments</p>
|
||||
|
||||
<div className="stats">
|
||||
<div className="stat-card">
|
||||
<div className="value">${totalReceived.toLocaleString()}</div>
|
||||
<div className="label">Total Received</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">${thisMonth.toLocaleString()}</div>
|
||||
<div className="label">This Month</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="value">{payments.length}</div>
|
||||
<div className="label">Payments</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="payment-list">
|
||||
{payments.map(payment => (
|
||||
<div key={payment.id} className="payment-item">
|
||||
<div className="payment-date">{payment.date}</div>
|
||||
<div>
|
||||
<div className="payment-invoice">{payment.invoice}</div>
|
||||
<div className="payment-client">{payment.client}</div>
|
||||
</div>
|
||||
<div className="payment-method">{payment.method}</div>
|
||||
<div className="payment-amount">${payment.amount.toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<PaymentHistory />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/profit-loss/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/profit-loss/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,92 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Projects - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
.header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; }
|
||||
h1 { font-size: 2rem; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 0.75rem 1.5rem; border-radius: 4px; cursor: pointer; }
|
||||
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; }
|
||||
.project-card { background: #1e293b; padding: 1.5rem; border-radius: 8px; border-top: 4px solid #3b82f6; }
|
||||
.project-card.hourly { border-top-color: #10b981; }
|
||||
.project-card.fixed { border-top-color: #f59e0b; }
|
||||
.project-header { display: flex; justify-content: space-between; align-items: start; margin-bottom: 1rem; }
|
||||
.project-title { font-size: 1.25rem; font-weight: 600; }
|
||||
.project-status { padding: 0.25rem 0.75rem; border-radius: 12px; font-size: 0.75rem; font-weight: 600; }
|
||||
.status-active { background: #065f46; color: #6ee7b7; }
|
||||
.status-complete { background: #1e40af; color: #93c5fd; }
|
||||
.client-name { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1rem; }
|
||||
.progress-bar { background: #0f172a; height: 8px; border-radius: 4px; overflow: hidden; margin-bottom: 1rem; }
|
||||
.progress-fill { background: linear-gradient(90deg, #3b82f6, #10b981); height: 100%; }
|
||||
.project-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
|
||||
.stat { text-align: center; }
|
||||
.stat .value { font-size: 1.5rem; font-weight: bold; }
|
||||
.stat .label { color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState } = React;
|
||||
|
||||
function ProjectDashboard() {
|
||||
const [projects] = useState([
|
||||
{ id: 1, title: 'Website Redesign', client: 'Acme Corp', type: 'hourly', status: 'active', progress: 65, hours: 45, budget: 8000 },
|
||||
{ id: 2, title: 'Mobile App', client: 'Tech Solutions', type: 'fixed', status: 'active', progress: 30, hours: 22, budget: 15000 },
|
||||
{ id: 3, title: 'Brand Identity', client: 'Design Co', type: 'hourly', status: 'complete', progress: 100, hours: 60, budget: 12000 },
|
||||
{ id: 4, title: 'E-commerce Store', client: 'Marketing Inc', type: 'fixed', status: 'active', progress: 50, hours: 35, budget: 20000 },
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<div className="header">
|
||||
<div>
|
||||
<h1>Projects</h1>
|
||||
<p style={{ color: '#94a3b8' }}>{projects.filter(p => p.status === 'active').length} active projects</p>
|
||||
</div>
|
||||
<button className="btn">+ New Project</button>
|
||||
</div>
|
||||
|
||||
<div className="project-grid">
|
||||
{projects.map(project => (
|
||||
<div key={project.id} className={`project-card ${project.type}`}>
|
||||
<div className="project-header">
|
||||
<div className="project-title">{project.title}</div>
|
||||
<span className={`project-status status-${project.status}`}>{project.status}</span>
|
||||
</div>
|
||||
<div className="client-name">{project.client}</div>
|
||||
<div className="progress-bar">
|
||||
<div className="progress-fill" style={{ width: `${project.progress}%` }} />
|
||||
</div>
|
||||
<div style={{ textAlign: 'center', marginBottom: '1rem', color: '#94a3b8', fontSize: '0.875rem' }}>
|
||||
{project.progress}% complete
|
||||
</div>
|
||||
<div className="project-stats">
|
||||
<div className="stat">
|
||||
<div className="value">{project.hours}h</div>
|
||||
<div className="label">Hours Tracked</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="value">${(project.budget / 1000).toFixed(0)}k</div>
|
||||
<div className="label">{project.type === 'fixed' ? 'Fixed Price' : 'Budget'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ProjectDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,57 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Reports - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1400px; margin: 0 auto; padding: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.report-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; margin-top: 2rem; }
|
||||
.report-card { background: #1e293b; padding: 2rem; border-radius: 8px; cursor: pointer; transition: transform 0.2s; }
|
||||
.report-card:hover { transform: translateY(-4px); background: #334155; }
|
||||
.report-icon { font-size: 2.5rem; margin-bottom: 1rem; }
|
||||
.report-title { font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; }
|
||||
.report-description { color: #94a3b8; font-size: 0.875rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function ReportsDashboard() {
|
||||
const reports = [
|
||||
{ icon: '📊', title: 'Profit & Loss', description: 'View income and expenses over time' },
|
||||
{ icon: '💰', title: 'Revenue by Client', description: 'See which clients generate the most revenue' },
|
||||
{ icon: '📈', title: 'Tax Summary', description: 'Summary of taxes collected and paid' },
|
||||
{ icon: '⏰', title: 'Accounts Aging', description: 'Outstanding invoices by age' },
|
||||
{ icon: '💸', title: 'Expense Report', description: 'Breakdown of all business expenses' },
|
||||
{ icon: '🎯', title: 'Time Summary', description: 'Billable vs non-billable hours' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Reports</h1>
|
||||
<p style={{ color: '#94a3b8' }}>Financial insights and business analytics</p>
|
||||
|
||||
<div className="report-grid">
|
||||
{reports.map((report, index) => (
|
||||
<div key={index} className="report-card">
|
||||
<div className="report-icon">{report.icon}</div>
|
||||
<div className="report-title">{report.title}</div>
|
||||
<div className="report-description">{report.description}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<ReportsDashboard />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/revenue-chart/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/revenue-chart/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/tax-summary/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/tax-summary/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
34
servers/freshbooks/src/ui/react-app/time-entries/index.html
Normal file
34
servers/freshbooks/src/ui/react-app/time-entries/index.html
Normal file
@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TITLE - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; padding: 2rem; }
|
||||
.container { max-width: 1400px; margin: 0 auto; }
|
||||
h1 { font-size: 2rem; margin-bottom: 1rem; }
|
||||
.card { background: #1e293b; padding: 2rem; border-radius: 8px; margin-top: 1rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
function App() {
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>TITLE</h1>
|
||||
<div className="card">
|
||||
<p style={{ color: '#94a3b8' }}>MCP App Component</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
ReactDOM.render(<App />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
122
servers/freshbooks/src/ui/react-app/time-tracker/index.html
Normal file
122
servers/freshbooks/src/ui/react-app/time-tracker/index.html
Normal file
@ -0,0 +1,122 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Time Tracker - FreshBooks</title>
|
||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
|
||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react.production.min.js"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #e2e8f0; }
|
||||
.container { max-width: 1000px; margin: 0 auto; padding: 2rem; }
|
||||
h1 { font-size: 2rem; margin-bottom: 0.5rem; }
|
||||
.timer-card { background: #1e293b; padding: 3rem; border-radius: 8px; text-align: center; margin: 2rem 0; }
|
||||
.timer-display { font-size: 4rem; font-weight: bold; margin-bottom: 2rem; font-variant-numeric: tabular-nums; }
|
||||
.timer-controls { display: flex; gap: 1rem; justify-content: center; }
|
||||
.btn { background: #3b82f6; color: white; border: none; padding: 1rem 2rem; border-radius: 4px; cursor: pointer; font-size: 1rem; font-weight: 500; }
|
||||
.btn:hover { background: #2563eb; }
|
||||
.btn-start { background: #10b981; }
|
||||
.btn-start:hover { background: #059669; }
|
||||
.btn-stop { background: #ef4444; }
|
||||
.btn-stop:hover { background: #dc2626; }
|
||||
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-top: 1rem; }
|
||||
.form-group { display: flex; flex-direction: column; gap: 0.5rem; text-align: left; }
|
||||
label { font-size: 0.875rem; color: #94a3b8; }
|
||||
input, select { background: #0f172a; border: 1px solid #334155; color: #e2e8f0; padding: 0.75rem; border-radius: 4px; }
|
||||
.entries-list { margin-top: 2rem; }
|
||||
.entry-item { background: #1e293b; padding: 1rem; border-radius: 8px; margin-bottom: 0.75rem; display: flex; justify-content: space-between; align-items: center; }
|
||||
.entry-info .project { color: #64748b; font-size: 0.875rem; margin-top: 0.25rem; }
|
||||
.entry-duration { font-size: 1.25rem; font-weight: bold; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
function TimeTracker() {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [seconds, setSeconds] = useState(0);
|
||||
const [task, setTask] = useState({ description: '', project: '', client: '' });
|
||||
const [entries, setEntries] = useState([
|
||||
{ id: 1, description: 'Website design', project: 'Acme Redesign', client: 'Acme Corp', duration: 7200 },
|
||||
{ id: 2, description: 'Bug fixes', project: 'Tech App', client: 'Tech Solutions', duration: 3600 },
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let interval;
|
||||
if (isRunning) {
|
||||
interval = setInterval(() => setSeconds(s => s + 1), 1000);
|
||||
}
|
||||
return () => clearInterval(interval);
|
||||
}, [isRunning]);
|
||||
|
||||
const formatTime = (secs) => {
|
||||
const h = Math.floor(secs / 3600);
|
||||
const m = Math.floor((secs % 3600) / 60);
|
||||
const s = secs % 60;
|
||||
return `${h.toString().padStart(2, '0')}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const startTimer = () => setIsRunning(true);
|
||||
const stopTimer = () => {
|
||||
if (seconds > 0 && task.description) {
|
||||
setEntries([{ ...task, id: Date.now(), duration: seconds }, ...entries]);
|
||||
}
|
||||
setIsRunning(false);
|
||||
setSeconds(0);
|
||||
setTask({ description: '', project: '', client: '' });
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
<h1>Time Tracker</h1>
|
||||
<p style={{ color: '#94a3b8', marginBottom: '2rem' }}>Track billable hours and project time</p>
|
||||
|
||||
<div className="timer-card">
|
||||
<div className="timer-display">{formatTime(seconds)}</div>
|
||||
<div className="form-grid">
|
||||
<div className="form-group">
|
||||
<label>Task Description</label>
|
||||
<input type="text" value={task.description} onChange={(e) => setTask({ ...task, description: e.target.value })} placeholder="What are you working on?" disabled={isRunning} />
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label>Project</label>
|
||||
<select value={task.project} onChange={(e) => setTask({ ...task, project: e.target.value })} disabled={isRunning}>
|
||||
<option value="">Select project...</option>
|
||||
<option value="Acme Redesign">Acme Redesign</option>
|
||||
<option value="Tech App">Tech App</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="timer-controls">
|
||||
{!isRunning ? (
|
||||
<button className="btn btn-start" onClick={startTimer}>Start Timer</button>
|
||||
) : (
|
||||
<button className="btn btn-stop" onClick={stopTimer}>Stop & Save</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="entries-list">
|
||||
<h3 style={{ marginBottom: '1rem' }}>Recent Entries</h3>
|
||||
{entries.map(entry => (
|
||||
<div key={entry.id} className="entry-item">
|
||||
<div className="entry-info">
|
||||
<div>{entry.description}</div>
|
||||
<div className="project">{entry.project} • {entry.client}</div>
|
||||
</div>
|
||||
<div className="entry-duration">{formatTime(entry.duration)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.render(<TimeTracker />, document.getElementById('root'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,14 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"module": "ES2022",
|
||||
"moduleResolution": "node",
|
||||
"lib": ["ES2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user