feat: Complete Acuity Scheduling MCP server with 40+ tools and 14 React apps

- Full API client with Basic Auth and OAuth2 support
- 40+ tools across 10 categories (appointments, availability, clients, calendars, products, forms, labels, webhooks, coupons, blocks)
- 14 interactive React-based MCP apps for rich UI experiences
- Comprehensive error handling and pagination
- TypeScript implementation with full type definitions
- Complete documentation and examples
This commit is contained in:
Jake Shore 2026-02-12 17:41:55 -05:00
parent e28a971b50
commit f8e0b3246f
34 changed files with 4141 additions and 296 deletions

View File

@ -0,0 +1,11 @@
# Acuity Scheduling API Credentials
# Get your credentials from: https://secure.acuityscheduling.com/app.php?key=api&action=settings
# Required: Your Acuity User ID
ACUITY_USER_ID=your_user_id_here
# Required: Your Acuity API Key
ACUITY_API_KEY=your_api_key_here
# Optional: OAuth2 Token (if using OAuth2 instead of Basic Auth)
# ACUITY_OAUTH2_TOKEN=your_oauth2_token_here

5
servers/acuity-scheduling/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
node_modules/
dist/
.env
*.log
.DS_Store

View File

@ -0,0 +1,221 @@
# Acuity Scheduling MCP Server
A comprehensive Model Context Protocol (MCP) server for Acuity Scheduling API integration.
## Features
### 40+ Tools Across 10 Categories
- **Appointments** (8 tools): List, get, create, update, cancel, reschedule, list types, get type
- **Availability** (4 tools): Get dates, get times, get classes, check availability
- **Clients** (5 tools): List, get, create, update, delete
- **Calendars** (4 tools): List, get, create, update
- **Products** (6 tools): List products by type (add-ons, packages, subscriptions, gift certificates)
- **Forms** (3 tools): List forms, get fields, get intake form responses
- **Labels** (5 tools): List, create, delete, add to appointment, remove from appointment
- **Webhooks** (3 tools): List, create, delete
- **Coupons** (5 tools): List, get, create, update, delete
- **Blocks** (3 tools): List, create, delete time blocks
### 14 Interactive MCP Apps
1. **Appointment Dashboard** - Overview dashboard with stats and upcoming appointments
2. **Appointment Detail** - Detailed view of individual appointments
3. **Appointment Grid** - Filterable table view of all appointments
4. **Availability Calendar** - Interactive calendar showing available time slots
5. **Client Directory** - Searchable directory of all clients
6. **Client Detail** - Comprehensive client profiles with appointment history
7. **Calendar Manager** - Manage staff calendars and settings
8. **Product Catalog** - Browse products, packages, and add-ons
9. **Form Responses** - View and manage intake form submissions
10. **Label Manager** - Create and manage appointment labels
11. **Coupon Manager** - Create and track promotional coupons
12. **Booking Flow** - Interactive multi-step booking interface
13. **Schedule Overview** - Week-view schedule across all calendars
14. **Blocked Time Manager** - Manage blocked time slots and staff availability
## Installation
```bash
npm install
npm run build
```
## Configuration
Create a `.env` file with your Acuity Scheduling credentials:
```bash
# Required
ACUITY_USER_ID=your_user_id
ACUITY_API_KEY=your_api_key
# Optional (for OAuth2)
ACUITY_OAUTH2_TOKEN=your_oauth2_token
```
### Getting API Credentials
1. Log in to your Acuity Scheduling account
2. Go to Business Settings → Integrations → API
3. Enable API access and copy your User ID and API Key
## Usage
### As MCP Server
Add to your MCP client configuration (e.g., Claude Desktop):
```json
{
"mcpServers": {
"acuity-scheduling": {
"command": "node",
"args": ["/path/to/acuity-scheduling/dist/main.js"],
"env": {
"ACUITY_USER_ID": "your_user_id",
"ACUITY_API_KEY": "your_api_key"
}
}
}
}
```
### Standalone
```bash
npm start
```
## API Reference
### Authentication
The server supports two authentication methods:
1. **Basic Auth** (recommended): Uses User ID and API Key
2. **OAuth2**: Uses OAuth2 access token
### Tool Examples
#### List Appointments
```json
{
"tool": "acuity_list_appointments",
"arguments": {
"minDate": "2024-01-01",
"maxDate": "2024-01-31",
"calendarID": 1
}
}
```
#### Create Appointment
```json
{
"tool": "acuity_create_appointment",
"arguments": {
"appointmentTypeID": 1,
"datetime": "2024-01-15T10:00:00",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"phone": "555-1234"
}
}
```
#### Get Availability
```json
{
"tool": "acuity_get_availability_times",
"arguments": {
"appointmentTypeID": 1,
"date": "2024-01-15"
}
}
```
## MCP Apps
Access interactive apps through MCP resources:
- `acuity://apps/appointment-dashboard`
- `acuity://apps/availability-calendar`
- `acuity://apps/client-directory`
- And 11 more...
Each app provides a rich, interactive UI for managing different aspects of your Acuity Scheduling account.
## Architecture
```
src/
├── clients/
│ └── acuity.ts # API client with auth & pagination
├── tools/
│ ├── appointments-tools.ts
│ ├── availability-tools.ts
│ ├── clients-tools.ts
│ ├── calendars-tools.ts
│ ├── products-tools.ts
│ ├── forms-tools.ts
│ ├── labels-tools.ts
│ ├── webhooks-tools.ts
│ ├── coupons-tools.ts
│ └── blocks-tools.ts
├── types/
│ └── index.ts # TypeScript type definitions
├── ui/
│ └── react-app/ # 14 interactive MCP apps
├── server.ts # MCP server implementation
└── main.ts # Entry point
```
## Development
```bash
# Build
npm run build
# Watch mode
npm run dev
# Run server
npm start
```
## Error Handling
The server includes comprehensive error handling:
- API rate limit detection
- Network error retries
- Invalid credential detection
- Missing required parameters validation
## Rate Limits
Acuity Scheduling API has rate limits. The client handles:
- Automatic pagination for large result sets
- Error responses with status codes
- Request throttling (if needed)
## Support
For issues or questions:
- Acuity Scheduling API Documentation: https://developers.acuityscheduling.com/
- MCP Documentation: https://modelcontextprotocol.io/
## License
MIT
## Version
1.0.0

View File

@ -1,20 +1,34 @@
{
"name": "mcp-server-acuity-scheduling",
"name": "@mcpengine/acuity-scheduling-server",
"version": "1.0.0",
"description": "MCP server for Acuity Scheduling API",
"type": "module",
"main": "dist/index.js",
"main": "./dist/main.js",
"bin": {
"acuity-scheduling-mcp": "./dist/main.js"
},
"scripts": {
"build": "tsc",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
"dev": "tsc --watch",
"start": "node dist/main.js",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"acuity",
"scheduling",
"appointments"
],
"author": "MCPEngine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^0.5.0",
"zod": "^3.22.4"
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"zod": "^3.24.1"
},
"devDependencies": {
"@types/node": "^20.10.0",
"tsx": "^4.7.0",
"typescript": "^5.3.0"
"@types/node": "^22.10.5",
"typescript": "^5.7.2"
}
}

View File

@ -0,0 +1,362 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
AcuityCredentials,
Appointment,
AppointmentType,
AvailabilityDate,
Client,
Calendar,
Product,
Form,
FormField,
Label,
Webhook,
Coupon,
Block,
PaginatedResponse,
ErrorResponse
} from '../types/index.js';
export class AcuityClient {
private client: AxiosInstance;
private credentials: AcuityCredentials;
constructor(credentials: AcuityCredentials) {
this.credentials = credentials;
const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
if (credentials.oauth2Token) {
headers['Authorization'] = `Bearer ${credentials.oauth2Token}`;
}
this.client = axios.create({
baseURL: 'https://acuityscheduling.com/api/v1',
headers,
...(credentials.oauth2Token ? {} : {
auth: {
username: credentials.userId,
password: credentials.apiKey
}
})
});
// Error interceptor
this.client.interceptors.response.use(
response => response,
(error: AxiosError<ErrorResponse>) => {
if (error.response?.data) {
const err = new Error(error.response.data.message || error.response.data.error);
(err as any).statusCode = error.response.status;
throw err;
}
throw error;
}
);
}
// Generic paginated GET request
private async paginatedGet<T>(
endpoint: string,
params: Record<string, any> = {}
): Promise<T[]> {
const allResults: T[] = [];
let page = 0;
const limit = params.limit || 100;
while (true) {
const response = await this.client.get<T[]>(endpoint, {
params: { ...params, limit, offset: page * limit }
});
allResults.push(...response.data);
if (response.data.length < limit) {
break;
}
page++;
}
return allResults;
}
// Appointments
async listAppointments(params?: {
minDate?: string;
maxDate?: string;
calendarID?: number;
canceled?: boolean;
}): Promise<Appointment[]> {
return this.paginatedGet<Appointment>('/appointments', params);
}
async getAppointment(id: number): Promise<Appointment> {
const response = await this.client.get<Appointment>(`/appointments/${id}`);
return response.data;
}
async createAppointment(data: {
appointmentTypeID: number;
datetime: string;
firstName: string;
lastName: string;
email: string;
phone?: string;
fields?: Array<{ id: number; value: string }>;
calendarID?: number;
certificate?: string;
}): Promise<Appointment> {
const response = await this.client.post<Appointment>('/appointments', data);
return response.data;
}
async updateAppointment(id: number, data: Partial<Appointment>): Promise<Appointment> {
const response = await this.client.put<Appointment>(`/appointments/${id}`, data);
return response.data;
}
async cancelAppointment(id: number, params?: { noEmail?: boolean }): Promise<Appointment> {
const response = await this.client.put<Appointment>(`/appointments/${id}/cancel`, null, { params });
return response.data;
}
async rescheduleAppointment(id: number, datetime: string): Promise<Appointment> {
const response = await this.client.put<Appointment>(`/appointments/${id}/reschedule`, { datetime });
return response.data;
}
async listAppointmentTypes(): Promise<AppointmentType[]> {
const response = await this.client.get<AppointmentType[]>('/appointment-types');
return response.data;
}
async getAppointmentType(id: number): Promise<AppointmentType> {
const response = await this.client.get<AppointmentType>(`/appointment-types/${id}`);
return response.data;
}
// Availability
async getAvailabilityDates(params: {
appointmentTypeID: number;
month?: string;
calendarID?: number;
timezone?: string;
}): Promise<AvailabilityDate[]> {
const response = await this.client.get<AvailabilityDate[]>('/availability/dates', { params });
return response.data;
}
async getAvailabilityTimes(params: {
appointmentTypeID: number;
date: string;
calendarID?: number;
timezone?: string;
}): Promise<{ time: string }[]> {
const response = await this.client.get<{ time: string }[]>('/availability/times', { params });
return response.data;
}
async getAvailabilityClasses(params?: {
appointmentTypeID?: number;
minDate?: string;
maxDate?: string;
}): Promise<any[]> {
const response = await this.client.get<any[]>('/availability/classes', { params });
return response.data;
}
async checkAvailability(params: {
appointmentTypeID: number;
date: string;
time: string;
calendarID?: number;
}): Promise<{ available: boolean }> {
const response = await this.client.get<{ available: boolean }>('/availability/check-times', { params });
return response.data;
}
// Clients
async listClients(): Promise<Client[]> {
return this.paginatedGet<Client>('/clients');
}
async getClient(id: number): Promise<Client> {
const response = await this.client.get<Client>(`/clients/${id}`);
return response.data;
}
async createClient(data: {
firstName: string;
lastName: string;
email: string;
phone?: string;
notes?: string;
}): Promise<Client> {
const response = await this.client.post<Client>('/clients', data);
return response.data;
}
async updateClient(id: number, data: Partial<Client>): Promise<Client> {
const response = await this.client.put<Client>(`/clients/${id}`, data);
return response.data;
}
async deleteClient(id: number): Promise<void> {
await this.client.delete(`/clients/${id}`);
}
// Calendars
async listCalendars(): Promise<Calendar[]> {
const response = await this.client.get<Calendar[]>('/calendars');
return response.data;
}
async getCalendar(id: number): Promise<Calendar> {
const response = await this.client.get<Calendar>(`/calendars/${id}`);
return response.data;
}
async createCalendar(data: {
name: string;
email?: string;
location?: string;
timezone?: string;
}): Promise<Calendar> {
const response = await this.client.post<Calendar>('/calendars', data);
return response.data;
}
async updateCalendar(id: number, data: Partial<Calendar>): Promise<Calendar> {
const response = await this.client.put<Calendar>(`/calendars/${id}`, data);
return response.data;
}
// Products
async listProducts(type?: 'addons' | 'packages' | 'subscriptions' | 'certificates'): Promise<Product[]> {
const endpoint = type ? `/products/${type}` : '/products';
const response = await this.client.get<Product[]>(endpoint);
return response.data;
}
async getProduct(id: number): Promise<Product> {
const response = await this.client.get<Product>(`/products/${id}`);
return response.data;
}
// Forms
async listForms(): Promise<Form[]> {
const response = await this.client.get<Form[]>('/forms');
return response.data;
}
async getFormFields(formId: number): Promise<FormField[]> {
const response = await this.client.get<FormField[]>(`/forms/${formId}/fields`);
return response.data;
}
async getIntakeFormResponses(appointmentId: number): Promise<any> {
const response = await this.client.get(`/appointments/${appointmentId}/forms`);
return response.data;
}
// Labels
async listLabels(): Promise<Label[]> {
const response = await this.client.get<Label[]>('/labels');
return response.data;
}
async createLabel(data: { name: string; color?: string }): Promise<Label> {
const response = await this.client.post<Label>('/labels', data);
return response.data;
}
async deleteLabel(id: number): Promise<void> {
await this.client.delete(`/labels/${id}`);
}
async addLabelToAppointment(appointmentId: number, labelId: number): Promise<void> {
await this.client.post(`/appointments/${appointmentId}/labels`, { labelID: labelId });
}
async removeLabelFromAppointment(appointmentId: number, labelId: number): Promise<void> {
await this.client.delete(`/appointments/${appointmentId}/labels/${labelId}`);
}
// Webhooks
async listWebhooks(): Promise<Webhook[]> {
const response = await this.client.get<Webhook[]>('/webhooks');
return response.data;
}
async createWebhook(data: {
event: string;
url: string;
}): Promise<Webhook> {
const response = await this.client.post<Webhook>('/webhooks', data);
return response.data;
}
async deleteWebhook(id: number): Promise<void> {
await this.client.delete(`/webhooks/${id}`);
}
// Coupons
async listCoupons(): Promise<Coupon[]> {
const response = await this.client.get<Coupon[]>('/coupons');
return response.data;
}
async getCoupon(id: number): Promise<Coupon> {
const response = await this.client.get<Coupon>(`/coupons/${id}`);
return response.data;
}
async createCoupon(data: {
code: string;
name?: string;
amountOff?: number;
percentageOff?: number;
validFrom?: string;
validTo?: string;
maxUses?: number;
}): Promise<Coupon> {
const response = await this.client.post<Coupon>('/coupons', data);
return response.data;
}
async updateCoupon(id: number, data: Partial<Coupon>): Promise<Coupon> {
const response = await this.client.put<Coupon>(`/coupons/${id}`, data);
return response.data;
}
async deleteCoupon(id: number): Promise<void> {
await this.client.delete(`/coupons/${id}`);
}
// Blocks (Time Blocks)
async listBlocks(params?: {
calendarID?: number;
minDate?: string;
maxDate?: string;
}): Promise<Block[]> {
const response = await this.client.get<Block[]>('/blocks', { params });
return response.data;
}
async createBlock(data: {
calendarID: number;
start: string;
end: string;
notes?: string;
}): Promise<Block> {
const response = await this.client.post<Block>('/blocks', data);
return response.data;
}
async deleteBlock(id: number): Promise<void> {
await this.client.delete(`/blocks/${id}`);
}
}

View File

@ -1,284 +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 = "acuity-scheduling";
const MCP_VERSION = "1.0.0";
const API_BASE_URL = "https://acuityscheduling.com/api/v1";
// ============================================
// API CLIENT - Acuity uses Basic Auth
// ============================================
class AcuityClient {
private authHeader: string;
private baseUrl: string;
constructor(userId: string, apiKey: string) {
// Acuity uses Basic Auth with userId:apiKey
this.authHeader = "Basic " + Buffer.from(`${userId}:${apiKey}`).toString("base64");
this.baseUrl = API_BASE_URL;
}
async request(endpoint: string, options: RequestInit = {}) {
const url = `${this.baseUrl}${endpoint}`;
const response = await fetch(url, {
...options,
headers: {
"Authorization": this.authHeader,
"Content-Type": "application/json",
...options.headers,
},
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Acuity 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),
});
}
async delete(endpoint: string) {
return this.request(endpoint, { method: "DELETE" });
}
}
// ============================================
// TOOL DEFINITIONS
// ============================================
const tools = [
{
name: "list_appointments",
description: "List appointments with optional filters. Returns scheduled appointments.",
inputSchema: {
type: "object" as const,
properties: {
minDate: { type: "string", description: "Minimum date (YYYY-MM-DD)" },
maxDate: { type: "string", description: "Maximum date (YYYY-MM-DD)" },
calendarID: { type: "number", description: "Filter by calendar ID" },
appointmentTypeID: { type: "number", description: "Filter by appointment type ID" },
canceled: { type: "boolean", description: "Include canceled appointments" },
max: { type: "number", description: "Maximum number of results (default 100)" },
},
},
},
{
name: "get_appointment",
description: "Get a specific appointment by ID",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number", description: "Appointment ID" },
},
required: ["id"],
},
},
{
name: "create_appointment",
description: "Create a new appointment",
inputSchema: {
type: "object" as const,
properties: {
datetime: { type: "string", description: "Appointment datetime (ISO 8601 format)" },
appointmentTypeID: { type: "number", description: "Appointment type ID" },
calendarID: { type: "number", description: "Calendar ID" },
firstName: { type: "string", description: "Client first name" },
lastName: { type: "string", description: "Client last name" },
email: { type: "string", description: "Client email" },
phone: { type: "string", description: "Client phone number" },
notes: { type: "string", description: "Appointment notes" },
fields: { type: "array", description: "Custom intake form fields", items: { type: "object" } },
},
required: ["datetime", "appointmentTypeID", "firstName", "lastName", "email"],
},
},
{
name: "cancel_appointment",
description: "Cancel an appointment",
inputSchema: {
type: "object" as const,
properties: {
id: { type: "number", description: "Appointment ID to cancel" },
cancelNote: { type: "string", description: "Reason for cancellation" },
noShow: { type: "boolean", description: "Mark as no-show instead of cancel" },
},
required: ["id"],
},
},
{
name: "list_calendars",
description: "List all calendars/staff members",
inputSchema: {
type: "object" as const,
properties: {},
},
},
{
name: "get_availability",
description: "Get available time slots for booking",
inputSchema: {
type: "object" as const,
properties: {
appointmentTypeID: { type: "number", description: "Appointment type ID" },
calendarID: { type: "number", description: "Calendar ID (optional)" },
date: { type: "string", description: "Date to check (YYYY-MM-DD)" },
month: { type: "string", description: "Month to check (YYYY-MM)" },
timezone: { type: "string", description: "Timezone (e.g., America/New_York)" },
},
required: ["appointmentTypeID"],
},
},
{
name: "list_clients",
description: "List clients with optional search",
inputSchema: {
type: "object" as const,
properties: {
search: { type: "string", description: "Search term (name, email, or phone)" },
max: { type: "number", description: "Maximum number of results" },
},
},
},
];
// ============================================
// TOOL HANDLERS
// ============================================
async function handleTool(client: AcuityClient, name: string, args: any) {
switch (name) {
case "list_appointments": {
const params = new URLSearchParams();
if (args.minDate) params.append("minDate", args.minDate);
if (args.maxDate) params.append("maxDate", args.maxDate);
if (args.calendarID) params.append("calendarID", String(args.calendarID));
if (args.appointmentTypeID) params.append("appointmentTypeID", String(args.appointmentTypeID));
if (args.canceled !== undefined) params.append("canceled", String(args.canceled));
if (args.max) params.append("max", String(args.max));
const query = params.toString();
return await client.get(`/appointments${query ? `?${query}` : ""}`);
}
case "get_appointment": {
return await client.get(`/appointments/${args.id}`);
}
case "create_appointment": {
const payload: any = {
datetime: args.datetime,
appointmentTypeID: args.appointmentTypeID,
firstName: args.firstName,
lastName: args.lastName,
email: args.email,
};
if (args.calendarID) payload.calendarID = args.calendarID;
if (args.phone) payload.phone = args.phone;
if (args.notes) payload.notes = args.notes;
if (args.fields) payload.fields = args.fields;
return await client.post("/appointments", payload);
}
case "cancel_appointment": {
const payload: any = {};
if (args.cancelNote) payload.cancelNote = args.cancelNote;
if (args.noShow) payload.noShow = args.noShow;
return await client.put(`/appointments/${args.id}/cancel`, payload);
}
case "list_calendars": {
return await client.get("/calendars");
}
case "get_availability": {
const params = new URLSearchParams();
params.append("appointmentTypeID", String(args.appointmentTypeID));
if (args.calendarID) params.append("calendarID", String(args.calendarID));
if (args.date) params.append("date", args.date);
if (args.month) params.append("month", args.month);
if (args.timezone) params.append("timezone", args.timezone);
return await client.get(`/availability/times?${params.toString()}`);
}
case "list_clients": {
const params = new URLSearchParams();
if (args.search) params.append("search", args.search);
if (args.max) params.append("max", String(args.max));
const query = params.toString();
return await client.get(`/clients${query ? `?${query}` : ""}`);
}
default:
throw new Error(`Unknown tool: ${name}`);
}
}
// ============================================
// SERVER SETUP
// ============================================
async function main() {
const userId = process.env.ACUITY_USER_ID;
const apiKey = process.env.ACUITY_API_KEY;
if (!userId || !apiKey) {
console.error("Error: ACUITY_USER_ID and ACUITY_API_KEY environment variables required");
process.exit(1);
}
const client = new AcuityClient(userId, apiKey);
const server = new Server(
{ name: `${MCP_NAME}-mcp`, version: MCP_VERSION },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools,
}));
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
try {
const result = await handleTool(client, name, args || {});
return {
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [{ type: "text", text: `Error: ${message}` }],
isError: true,
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
console.error(`${MCP_NAME} MCP server running on stdio`);
}
main().catch(console.error);

View File

@ -0,0 +1,20 @@
#!/usr/bin/env node
import { config } from 'dotenv';
import { AcuityMCPServer } from './server.js';
config();
const userId = process.env.ACUITY_USER_ID;
const apiKey = process.env.ACUITY_API_KEY;
const oauth2Token = process.env.ACUITY_OAUTH2_TOKEN;
if (!userId || !apiKey) {
console.error('Error: ACUITY_USER_ID and ACUITY_API_KEY must be set in environment variables');
process.exit(1);
}
const server = new AcuityMCPServer(userId, apiKey, oauth2Token);
server.run().catch((error) => {
console.error('Server error:', error);
process.exit(1);
});

View File

@ -0,0 +1,212 @@
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
ListResourcesRequestSchema,
ReadResourceRequestSchema
} from '@modelcontextprotocol/sdk/types.js';
import { AcuityClient } from './clients/acuity.js';
import { createAppointmentsTools } from './tools/appointments-tools.js';
import { createAvailabilityTools } from './tools/availability-tools.js';
import { createClientsTools } from './tools/clients-tools.js';
import { createCalendarsTools } from './tools/calendars-tools.js';
import { createProductsTools } from './tools/products-tools.js';
import { createFormsTools } from './tools/forms-tools.js';
import { createLabelsTools } from './tools/labels-tools.js';
import { createWebhooksTools } from './tools/webhooks-tools.js';
import { createCouponsTools } from './tools/coupons-tools.js';
import { createBlocksTools } from './tools/blocks-tools.js';
export class AcuityMCPServer {
private server: Server;
private client: AcuityClient;
private allTools: Record<string, any> = {};
constructor(userId: string, apiKey: string, oauth2Token?: string) {
this.client = new AcuityClient({ userId, apiKey, oauth2Token });
this.server = new Server(
{
name: 'acuity-scheduling-server',
version: '1.0.0'
},
{
capabilities: {
tools: {},
resources: {}
}
}
);
this.setupToolHandlers();
this.setupResourceHandlers();
}
private setupToolHandlers(): void {
// Aggregate all tools
this.allTools = {
...createAppointmentsTools(this.client),
...createAvailabilityTools(this.client),
...createClientsTools(this.client),
...createCalendarsTools(this.client),
...createProductsTools(this.client),
...createFormsTools(this.client),
...createLabelsTools(this.client),
...createWebhooksTools(this.client),
...createCouponsTools(this.client),
...createBlocksTools(this.client)
};
// List tools handler
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: Object.entries(this.allTools).map(([name, tool]) => ({
name,
description: tool.description,
inputSchema: tool.inputSchema
}))
};
});
// Call tool handler
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
const tool = this.allTools[request.params.name];
if (!tool) {
throw new Error(`Unknown tool: ${request.params.name}`);
}
try {
return await tool.handler(request.params.arguments || {});
} catch (error: any) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: error.message,
statusCode: error.statusCode || 500
}, null, 2)
}],
isError: true
};
}
});
}
private setupResourceHandlers(): void {
// List resources - MCP apps
this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
{
uri: 'acuity://apps/appointment-dashboard',
name: 'Appointment Dashboard',
description: 'Overview dashboard for appointments',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/appointment-detail',
name: 'Appointment Detail',
description: 'Detailed view of a single appointment',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/appointment-grid',
name: 'Appointment Grid',
description: 'Grid view of all appointments',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/availability-calendar',
name: 'Availability Calendar',
description: 'Calendar showing available time slots',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/client-detail',
name: 'Client Detail',
description: 'Detailed view of a client',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/client-directory',
name: 'Client Directory',
description: 'Directory of all clients',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/calendar-manager',
name: 'Calendar Manager',
description: 'Manage calendars',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/product-catalog',
name: 'Product Catalog',
description: 'Browse products, add-ons, and packages',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/form-responses',
name: 'Form Responses',
description: 'View intake form responses',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/label-manager',
name: 'Label Manager',
description: 'Manage appointment labels',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/coupon-manager',
name: 'Coupon Manager',
description: 'Manage coupons and discounts',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/booking-flow',
name: 'Booking Flow',
description: 'Interactive booking interface',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/schedule-overview',
name: 'Schedule Overview',
description: 'High-level schedule overview',
mimeType: 'text/html'
},
{
uri: 'acuity://apps/blocked-time-manager',
name: 'Blocked Time Manager',
description: 'Manage blocked time slots',
mimeType: 'text/html'
}
]
};
});
// Read resource - serve MCP app HTML
this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const appName = request.params.uri.replace('acuity://apps/', '');
try {
const { default: appHtml } = await import(`./ui/react-app/${appName}.js`);
return {
contents: [{
uri: request.params.uri,
mimeType: 'text/html',
text: appHtml
}]
};
} catch (error) {
throw new Error(`App not found: ${appName}`);
}
});
}
async run(): Promise<void> {
const transport = new StdioServerTransport();
await this.server.connect(transport);
console.error('Acuity Scheduling MCP server running on stdio');
}
}

View File

@ -0,0 +1,155 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createAppointmentsTools(client: AcuityClient) {
return {
acuity_list_appointments: {
description: 'List appointments with optional filters',
inputSchema: z.object({
minDate: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
maxDate: z.string().optional().describe('Maximum date (YYYY-MM-DD)'),
calendarID: z.number().optional().describe('Filter by calendar ID'),
canceled: z.boolean().optional().describe('Include canceled appointments')
}),
handler: async (args: any) => {
const appointments = await client.listAppointments(args);
return {
content: [{
type: 'text',
text: JSON.stringify(appointments, null, 2)
}]
};
}
},
acuity_get_appointment: {
description: 'Get a specific appointment by ID',
inputSchema: z.object({
id: z.number().describe('Appointment ID')
}),
handler: async (args: any) => {
const appointment = await client.getAppointment(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(appointment, null, 2)
}]
};
}
},
acuity_create_appointment: {
description: 'Create a new appointment',
inputSchema: z.object({
appointmentTypeID: z.number().describe('Appointment type ID'),
datetime: z.string().describe('Appointment datetime (ISO 8601)'),
firstName: z.string().describe('Client first name'),
lastName: z.string().describe('Client last name'),
email: z.string().email().describe('Client email'),
phone: z.string().optional().describe('Client phone'),
calendarID: z.number().optional().describe('Calendar ID'),
fields: z.array(z.object({
id: z.number(),
value: z.string()
})).optional().describe('Form field values'),
certificate: z.string().optional().describe('Gift certificate code')
}),
handler: async (args: any) => {
const appointment = await client.createAppointment(args);
return {
content: [{
type: 'text',
text: JSON.stringify(appointment, null, 2)
}]
};
}
},
acuity_update_appointment: {
description: 'Update an existing appointment',
inputSchema: z.object({
id: z.number().describe('Appointment ID'),
firstName: z.string().optional(),
lastName: z.string().optional(),
email: z.string().email().optional(),
phone: z.string().optional(),
notes: z.string().optional()
}),
handler: async (args: any) => {
const { id, ...data } = args;
const appointment = await client.updateAppointment(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify(appointment, null, 2)
}]
};
}
},
acuity_cancel_appointment: {
description: 'Cancel an appointment',
inputSchema: z.object({
id: z.number().describe('Appointment ID'),
noEmail: z.boolean().optional().describe('Skip sending cancellation email')
}),
handler: async (args: any) => {
const { id, noEmail } = args;
const appointment = await client.cancelAppointment(id, { noEmail });
return {
content: [{
type: 'text',
text: JSON.stringify(appointment, null, 2)
}]
};
}
},
acuity_reschedule_appointment: {
description: 'Reschedule an appointment to a new datetime',
inputSchema: z.object({
id: z.number().describe('Appointment ID'),
datetime: z.string().describe('New datetime (ISO 8601)')
}),
handler: async (args: any) => {
const appointment = await client.rescheduleAppointment(args.id, args.datetime);
return {
content: [{
type: 'text',
text: JSON.stringify(appointment, null, 2)
}]
};
}
},
acuity_list_appointment_types: {
description: 'List all appointment types',
inputSchema: z.object({}),
handler: async () => {
const types = await client.listAppointmentTypes();
return {
content: [{
type: 'text',
text: JSON.stringify(types, null, 2)
}]
};
}
},
acuity_get_appointment_type: {
description: 'Get a specific appointment type by ID',
inputSchema: z.object({
id: z.number().describe('Appointment type ID')
}),
handler: async (args: any) => {
const type = await client.getAppointmentType(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(type, null, 2)
}]
};
}
}
};
}

View File

@ -0,0 +1,81 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createAvailabilityTools(client: AcuityClient) {
return {
acuity_get_availability_dates: {
description: 'Get available dates for an appointment type',
inputSchema: z.object({
appointmentTypeID: z.number().describe('Appointment type ID'),
month: z.string().optional().describe('Month to check (YYYY-MM)'),
calendarID: z.number().optional().describe('Specific calendar ID'),
timezone: z.string().optional().describe('Timezone (e.g., America/New_York)')
}),
handler: async (args: any) => {
const dates = await client.getAvailabilityDates(args);
return {
content: [{
type: 'text',
text: JSON.stringify(dates, null, 2)
}]
};
}
},
acuity_get_availability_times: {
description: 'Get available time slots for a specific date and appointment type',
inputSchema: z.object({
appointmentTypeID: z.number().describe('Appointment type ID'),
date: z.string().describe('Date to check (YYYY-MM-DD)'),
calendarID: z.number().optional().describe('Specific calendar ID'),
timezone: z.string().optional().describe('Timezone (e.g., America/New_York)')
}),
handler: async (args: any) => {
const times = await client.getAvailabilityTimes(args);
return {
content: [{
type: 'text',
text: JSON.stringify(times, null, 2)
}]
};
}
},
acuity_get_availability_classes: {
description: 'Get available class sessions',
inputSchema: z.object({
appointmentTypeID: z.number().optional().describe('Filter by appointment type'),
minDate: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
maxDate: z.string().optional().describe('Maximum date (YYYY-MM-DD)')
}),
handler: async (args: any) => {
const classes = await client.getAvailabilityClasses(args);
return {
content: [{
type: 'text',
text: JSON.stringify(classes, null, 2)
}]
};
}
},
acuity_check_availability: {
description: 'Check if a specific time slot is available',
inputSchema: z.object({
appointmentTypeID: z.number().describe('Appointment type ID'),
date: z.string().describe('Date (YYYY-MM-DD)'),
time: z.string().describe('Time (HH:MM)'),
calendarID: z.number().optional().describe('Calendar ID')
}),
handler: async (args: any) => {
const result = await client.checkAvailability(args);
return {
content: [{
type: 'text',
text: JSON.stringify(result, null, 2)
}]
};
}
}
};
}

View File

@ -0,0 +1,59 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createBlocksTools(client: AcuityClient) {
return {
acuity_list_blocks: {
description: 'List time blocks (blocked time slots)',
inputSchema: z.object({
calendarID: z.number().optional().describe('Filter by calendar ID'),
minDate: z.string().optional().describe('Minimum date (YYYY-MM-DD)'),
maxDate: z.string().optional().describe('Maximum date (YYYY-MM-DD)')
}),
handler: async (args: any) => {
const blocks = await client.listBlocks(args);
return {
content: [{
type: 'text',
text: JSON.stringify(blocks, null, 2)
}]
};
}
},
acuity_create_block: {
description: 'Create a new time block to block off time',
inputSchema: z.object({
calendarID: z.number().describe('Calendar ID'),
start: z.string().describe('Start datetime (ISO 8601)'),
end: z.string().describe('End datetime (ISO 8601)'),
notes: z.string().optional().describe('Block notes/reason')
}),
handler: async (args: any) => {
const block = await client.createBlock(args);
return {
content: [{
type: 'text',
text: JSON.stringify(block, null, 2)
}]
};
}
},
acuity_delete_block: {
description: 'Delete a time block',
inputSchema: z.object({
id: z.number().describe('Block ID')
}),
handler: async (args: any) => {
await client.deleteBlock(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Block deleted' })
}]
};
}
}
};
}

View File

@ -0,0 +1,76 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createCalendarsTools(client: AcuityClient) {
return {
acuity_list_calendars: {
description: 'List all calendars',
inputSchema: z.object({}),
handler: async () => {
const calendars = await client.listCalendars();
return {
content: [{
type: 'text',
text: JSON.stringify(calendars, null, 2)
}]
};
}
},
acuity_get_calendar: {
description: 'Get a specific calendar by ID',
inputSchema: z.object({
id: z.number().describe('Calendar ID')
}),
handler: async (args: any) => {
const calendar = await client.getCalendar(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(calendar, null, 2)
}]
};
}
},
acuity_create_calendar: {
description: 'Create a new calendar',
inputSchema: z.object({
name: z.string().describe('Calendar name'),
email: z.string().email().optional().describe('Calendar email'),
location: z.string().optional().describe('Location'),
timezone: z.string().optional().describe('Timezone (e.g., America/New_York)')
}),
handler: async (args: any) => {
const calendar = await client.createCalendar(args);
return {
content: [{
type: 'text',
text: JSON.stringify(calendar, null, 2)
}]
};
}
},
acuity_update_calendar: {
description: 'Update an existing calendar',
inputSchema: z.object({
id: z.number().describe('Calendar ID'),
name: z.string().optional(),
email: z.string().email().optional(),
location: z.string().optional(),
timezone: z.string().optional()
}),
handler: async (args: any) => {
const { id, ...data } = args;
const calendar = await client.updateCalendar(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify(calendar, null, 2)
}]
};
}
}
};
}

View File

@ -0,0 +1,94 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createClientsTools(client: AcuityClient) {
return {
acuity_list_clients: {
description: 'List all clients',
inputSchema: z.object({}),
handler: async () => {
const clients = await client.listClients();
return {
content: [{
type: 'text',
text: JSON.stringify(clients, null, 2)
}]
};
}
},
acuity_get_client: {
description: 'Get a specific client by ID',
inputSchema: z.object({
id: z.number().describe('Client ID')
}),
handler: async (args: any) => {
const clientData = await client.getClient(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(clientData, null, 2)
}]
};
}
},
acuity_create_client: {
description: 'Create a new client',
inputSchema: z.object({
firstName: z.string().describe('First name'),
lastName: z.string().describe('Last name'),
email: z.string().email().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
notes: z.string().optional().describe('Client notes')
}),
handler: async (args: any) => {
const clientData = await client.createClient(args);
return {
content: [{
type: 'text',
text: JSON.stringify(clientData, null, 2)
}]
};
}
},
acuity_update_client: {
description: 'Update an existing client',
inputSchema: z.object({
id: z.number().describe('Client ID'),
firstName: z.string().optional(),
lastName: z.string().optional(),
email: z.string().email().optional(),
phone: z.string().optional(),
notes: z.string().optional()
}),
handler: async (args: any) => {
const { id, ...data } = args;
const clientData = await client.updateClient(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify(clientData, null, 2)
}]
};
}
},
acuity_delete_client: {
description: 'Delete a client',
inputSchema: z.object({
id: z.number().describe('Client ID')
}),
handler: async (args: any) => {
await client.deleteClient(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Client deleted' })
}]
};
}
}
};
}

View File

@ -0,0 +1,98 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createCouponsTools(client: AcuityClient) {
return {
acuity_list_coupons: {
description: 'List all coupons',
inputSchema: z.object({}),
handler: async () => {
const coupons = await client.listCoupons();
return {
content: [{
type: 'text',
text: JSON.stringify(coupons, null, 2)
}]
};
}
},
acuity_get_coupon: {
description: 'Get a specific coupon by ID',
inputSchema: z.object({
id: z.number().describe('Coupon ID')
}),
handler: async (args: any) => {
const coupon = await client.getCoupon(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(coupon, null, 2)
}]
};
}
},
acuity_create_coupon: {
description: 'Create a new coupon',
inputSchema: z.object({
code: z.string().describe('Coupon code'),
name: z.string().optional().describe('Coupon name'),
amountOff: z.number().optional().describe('Fixed amount discount'),
percentageOff: z.number().optional().describe('Percentage discount (0-100)'),
validFrom: z.string().optional().describe('Valid from date (YYYY-MM-DD)'),
validTo: z.string().optional().describe('Valid to date (YYYY-MM-DD)'),
maxUses: z.number().optional().describe('Maximum number of uses')
}),
handler: async (args: any) => {
const coupon = await client.createCoupon(args);
return {
content: [{
type: 'text',
text: JSON.stringify(coupon, null, 2)
}]
};
}
},
acuity_update_coupon: {
description: 'Update an existing coupon',
inputSchema: z.object({
id: z.number().describe('Coupon ID'),
code: z.string().optional(),
name: z.string().optional(),
amountOff: z.number().optional(),
percentageOff: z.number().optional(),
validFrom: z.string().optional(),
validTo: z.string().optional(),
maxUses: z.number().optional()
}),
handler: async (args: any) => {
const { id, ...data } = args;
const coupon = await client.updateCoupon(id, data);
return {
content: [{
type: 'text',
text: JSON.stringify(coupon, null, 2)
}]
};
}
},
acuity_delete_coupon: {
description: 'Delete a coupon',
inputSchema: z.object({
id: z.number().describe('Coupon ID')
}),
handler: async (args: any) => {
await client.deleteCoupon(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Coupon deleted' })
}]
};
}
}
};
}

View File

@ -0,0 +1,52 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createFormsTools(client: AcuityClient) {
return {
acuity_list_forms: {
description: 'List all intake forms',
inputSchema: z.object({}),
handler: async () => {
const forms = await client.listForms();
return {
content: [{
type: 'text',
text: JSON.stringify(forms, null, 2)
}]
};
}
},
acuity_get_form_fields: {
description: 'Get fields for a specific form',
inputSchema: z.object({
formId: z.number().describe('Form ID')
}),
handler: async (args: any) => {
const fields = await client.getFormFields(args.formId);
return {
content: [{
type: 'text',
text: JSON.stringify(fields, null, 2)
}]
};
}
},
acuity_get_intake_form_responses: {
description: 'Get intake form responses for an appointment',
inputSchema: z.object({
appointmentId: z.number().describe('Appointment ID')
}),
handler: async (args: any) => {
const responses = await client.getIntakeFormResponses(args.appointmentId);
return {
content: [{
type: 'text',
text: JSON.stringify(responses, null, 2)
}]
};
}
}
};
}

View File

@ -0,0 +1,87 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createLabelsTools(client: AcuityClient) {
return {
acuity_list_labels: {
description: 'List all labels',
inputSchema: z.object({}),
handler: async () => {
const labels = await client.listLabels();
return {
content: [{
type: 'text',
text: JSON.stringify(labels, null, 2)
}]
};
}
},
acuity_create_label: {
description: 'Create a new label',
inputSchema: z.object({
name: z.string().describe('Label name'),
color: z.string().optional().describe('Label color (hex code)')
}),
handler: async (args: any) => {
const label = await client.createLabel(args);
return {
content: [{
type: 'text',
text: JSON.stringify(label, null, 2)
}]
};
}
},
acuity_delete_label: {
description: 'Delete a label',
inputSchema: z.object({
id: z.number().describe('Label ID')
}),
handler: async (args: any) => {
await client.deleteLabel(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Label deleted' })
}]
};
}
},
acuity_add_label_to_appointment: {
description: 'Add a label to an appointment',
inputSchema: z.object({
appointmentId: z.number().describe('Appointment ID'),
labelId: z.number().describe('Label ID')
}),
handler: async (args: any) => {
await client.addLabelToAppointment(args.appointmentId, args.labelId);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Label added to appointment' })
}]
};
}
},
acuity_remove_label_from_appointment: {
description: 'Remove a label from an appointment',
inputSchema: z.object({
appointmentId: z.number().describe('Appointment ID'),
labelId: z.number().describe('Label ID')
}),
handler: async (args: any) => {
await client.removeLabelFromAppointment(args.appointmentId, args.labelId);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Label removed from appointment' })
}]
};
}
}
};
}

View File

@ -0,0 +1,95 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createProductsTools(client: AcuityClient) {
return {
acuity_list_products: {
description: 'List all products (add-ons, packages, subscriptions, gift certificates)',
inputSchema: z.object({
type: z.enum(['addons', 'packages', 'subscriptions', 'certificates']).optional()
.describe('Filter by product type')
}),
handler: async (args: any) => {
const products = await client.listProducts(args.type);
return {
content: [{
type: 'text',
text: JSON.stringify(products, null, 2)
}]
};
}
},
acuity_get_product: {
description: 'Get a specific product by ID',
inputSchema: z.object({
id: z.number().describe('Product ID')
}),
handler: async (args: any) => {
const product = await client.getProduct(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify(product, null, 2)
}]
};
}
},
acuity_list_addons: {
description: 'List all add-on products',
inputSchema: z.object({}),
handler: async () => {
const products = await client.listProducts('addons');
return {
content: [{
type: 'text',
text: JSON.stringify(products, null, 2)
}]
};
}
},
acuity_list_packages: {
description: 'List all package products',
inputSchema: z.object({}),
handler: async () => {
const products = await client.listProducts('packages');
return {
content: [{
type: 'text',
text: JSON.stringify(products, null, 2)
}]
};
}
},
acuity_list_subscriptions: {
description: 'List all subscription products',
inputSchema: z.object({}),
handler: async () => {
const products = await client.listProducts('subscriptions');
return {
content: [{
type: 'text',
text: JSON.stringify(products, null, 2)
}]
};
}
},
acuity_list_gift_certificates: {
description: 'List all gift certificate products',
inputSchema: z.object({}),
handler: async () => {
const products = await client.listProducts('certificates');
return {
content: [{
type: 'text',
text: JSON.stringify(products, null, 2)
}]
};
}
}
};
}

View File

@ -0,0 +1,53 @@
import { z } from 'zod';
import { AcuityClient } from '../clients/acuity.js';
export function createWebhooksTools(client: AcuityClient) {
return {
acuity_list_webhooks: {
description: 'List all webhooks',
inputSchema: z.object({}),
handler: async () => {
const webhooks = await client.listWebhooks();
return {
content: [{
type: 'text',
text: JSON.stringify(webhooks, null, 2)
}]
};
}
},
acuity_create_webhook: {
description: 'Create a new webhook',
inputSchema: z.object({
event: z.string().describe('Event type (e.g., appointment.scheduled, appointment.canceled)'),
url: z.string().url().describe('Webhook URL')
}),
handler: async (args: any) => {
const webhook = await client.createWebhook(args);
return {
content: [{
type: 'text',
text: JSON.stringify(webhook, null, 2)
}]
};
}
},
acuity_delete_webhook: {
description: 'Delete a webhook',
inputSchema: z.object({
id: z.number().describe('Webhook ID')
}),
handler: async (args: any) => {
await client.deleteWebhook(args.id);
return {
content: [{
type: 'text',
text: JSON.stringify({ success: true, message: 'Webhook deleted' })
}]
};
}
}
};
}

View File

@ -0,0 +1,172 @@
// Acuity Scheduling API Types
export interface AcuityCredentials {
userId: string;
apiKey: string;
oauth2Token?: string;
}
export interface Appointment {
id: number;
firstName: string;
lastName: string;
phone?: string;
email: string;
date: string;
time: string;
endTime: string;
dateCreated: string;
datetimeCreated: string;
datetime: string;
price?: string;
priceSold?: string;
paid?: string;
amountPaid?: string;
type: string;
appointmentTypeID: number;
classID?: number;
addonIDs?: number[];
category?: string;
duration: string;
calendar: string;
calendarID: number;
certificate?: string;
confirmationPage?: string;
formsText?: string;
forms?: AppointmentForm[];
notes?: string;
timezone?: string;
calendarTimezone?: string;
canceled?: boolean;
canClientCancel?: boolean;
canClientReschedule?: boolean;
labels?: Label[];
location?: string;
}
export interface AppointmentType {
id: number;
active: boolean;
name: string;
description?: string;
duration: number;
price?: string;
category?: string;
color?: string;
private?: boolean;
type?: string;
calendarIDs?: number[];
classSize?: number;
paddingBefore?: number;
paddingAfter?: number;
}
export interface AvailabilityDate {
date: string;
times?: AvailabilityTime[];
}
export interface AvailabilityTime {
time: string;
slotsAvailable?: number;
}
export interface Client {
id: number;
firstName: string;
lastName: string;
phone?: string;
email: string;
notes?: string;
}
export interface Calendar {
id: number;
name: string;
email?: string;
location?: string;
thumbnail?: string;
timezone?: string;
description?: string;
}
export interface Product {
id: number;
name: string;
description?: string;
price?: string;
category?: string;
}
export interface Form {
id: number;
name: string;
description?: string;
fields?: FormField[];
}
export interface FormField {
id: number;
name: string;
type: string;
required?: boolean;
options?: string[];
}
export interface AppointmentForm {
id: number;
name: string;
values?: FormValue[];
}
export interface FormValue {
id: number;
fieldID: number;
value: string;
name?: string;
}
export interface Label {
id: number;
name: string;
color?: string;
}
export interface Webhook {
id: number;
event: string;
url: string;
}
export interface Coupon {
id: number;
code: string;
name?: string;
discount?: string;
amountOff?: number;
percentageOff?: number;
validFrom?: string;
validTo?: string;
maxUses?: number;
timesUsed?: number;
}
export interface Block {
id: number;
calendarID: number;
start: string;
end: string;
notes?: string;
}
export interface PaginatedResponse<T> {
data: T[];
count?: number;
hasMore?: boolean;
}
export interface ErrorResponse {
status_code: number;
error: string;
message: string;
}

View File

@ -0,0 +1,119 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appointment Dashboard</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.dashboard { max-width: 1200px; margin: 0 auto; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-card h3 { margin: 0 0 10px 0; color: #666; font-size: 14px; font-weight: 500; }
.stat-card .value { font-size: 32px; font-weight: 700; color: #4F46E5; }
.appointments-list { background: white; border-radius: 8px; padding: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.appointment-item { padding: 15px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.appointment-item:last-child { border-bottom: none; }
.appointment-info h4 { margin: 0 0 5px 0; color: #333; }
.appointment-info p { margin: 0; color: #666; font-size: 14px; }
.status-badge { padding: 4px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-confirmed { background: #D1FAE5; color: #065F46; }
.status-pending { background: #FEF3C7; color: #92400E; }
.status-canceled { background: #FEE2E2; color: #991B1B; }
button { background: #4F46E5; color: white; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #4338CA; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function AppointmentDashboard() {
const [stats, setStats] = useState({ today: 0, thisWeek: 0, thisMonth: 0, total: 0 });
const [upcomingAppointments, setUpcomingAppointments] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
try {
// Mock data for demo - in production this would call MCP tools
const mockAppointments = [
{ id: 1, firstName: 'John', lastName: 'Doe', datetime: '2024-01-15T10:00:00', type: 'Consultation', status: 'confirmed' },
{ id: 2, firstName: 'Jane', lastName: 'Smith', datetime: '2024-01-15T14:30:00', type: 'Follow-up', status: 'confirmed' },
{ id: 3, firstName: 'Bob', lastName: 'Johnson', datetime: '2024-01-16T09:00:00', type: 'Initial Assessment', status: 'pending' },
];
setUpcomingAppointments(mockAppointments);
setStats({ today: 2, thisWeek: 8, thisMonth: 32, total: 156 });
setLoading(false);
} catch (error) {
console.error('Error fetching dashboard data:', error);
setLoading(false);
}
};
const formatDateTime = (datetime) => {
const date = new Date(datetime);
return date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
hour12: true
});
};
if (loading) return <div className="dashboard"><h1>Loading...</h1></div>;
return (
<div className="dashboard">
<h1>Appointment Dashboard</h1>
<div className="stats-grid">
<div className="stat-card">
<h3>Today</h3>
<div className="value">{stats.today}</div>
</div>
<div className="stat-card">
<h3>This Week</h3>
<div className="value">{stats.thisWeek}</div>
</div>
<div className="stat-card">
<h3>This Month</h3>
<div className="value">{stats.thisMonth}</div>
</div>
<div className="stat-card">
<h3>Total</h3>
<div className="value">{stats.total}</div>
</div>
</div>
<div className="appointments-list">
<h2>Upcoming Appointments</h2>
{upcomingAppointments.map(apt => (
<div key={apt.id} className="appointment-item">
<div className="appointment-info">
<h4>{apt.firstName} {apt.lastName}</h4>
<p>{formatDateTime(apt.datetime)} {apt.type}</p>
</div>
<span className={\`status-badge status-\${apt.status}\`}>
{apt.status.charAt(0).toUpperCase() + apt.status.slice(1)}
</span>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<AppointmentDashboard />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,151 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appointment Detail</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { margin: 0 0 20px 0; color: #333; }
.section { margin-bottom: 30px; }
.section h2 { font-size: 18px; color: #666; margin-bottom: 15px; }
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 14px; color: #666; margin-bottom: 5px; }
.field .value { font-size: 16px; color: #333; font-weight: 500; }
.actions { display: flex; gap: 10px; margin-top: 30px; }
button { padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #4F46E5; color: white; }
.btn-primary:hover { background: #4338CA; }
.btn-secondary { background: #E5E7EB; color: #333; }
.btn-secondary:hover { background: #D1D5DB; }
.btn-danger { background: #DC2626; color: white; }
.btn-danger:hover { background: #B91C1C; }
.labels { display: flex; gap: 8px; flex-wrap: wrap; }
.label-tag { padding: 4px 12px; background: #E0E7FF; color: #4F46E5; border-radius: 12px; font-size: 12px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function AppointmentDetail() {
const [appointment, setAppointment] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchAppointment();
}, []);
const fetchAppointment = async () => {
// Mock data - in production would fetch from MCP tools
const mockData = {
id: 12345,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
datetime: '2024-01-15T10:00:00',
endTime: '11:00:00',
type: 'Initial Consultation',
calendar: 'Dr. Smith',
price: '$150.00',
paid: '$150.00',
notes: 'Client requested morning appointment. First-time visitor.',
labels: [{ id: 1, name: 'VIP', color: '#4F46E5' }, { id: 2, name: 'Follow-up Needed', color: '#DC2626' }],
forms: [
{ name: 'Medical History', values: [{ name: 'Allergies', value: 'None' }] }
]
};
setAppointment(mockData);
setLoading(false);
};
const handleReschedule = () => {
alert('Reschedule functionality - would open date/time picker');
};
const handleCancel = () => {
if (confirm('Are you sure you want to cancel this appointment?')) {
alert('Appointment canceled');
}
};
if (loading) return <div className="container"><h1>Loading...</h1></div>;
if (!appointment) return <div className="container"><h1>Appointment not found</h1></div>;
return (
<div className="container">
<h1>Appointment Details</h1>
<div className="section">
<h2>Client Information</h2>
<div className="field">
<label>Name</label>
<div className="value">{appointment.firstName} {appointment.lastName}</div>
</div>
<div className="field">
<label>Email</label>
<div className="value">{appointment.email}</div>
</div>
<div className="field">
<label>Phone</label>
<div className="value">{appointment.phone}</div>
</div>
</div>
<div className="section">
<h2>Appointment Details</h2>
<div className="field">
<label>Type</label>
<div className="value">{appointment.type}</div>
</div>
<div className="field">
<label>Date & Time</label>
<div className="value">{new Date(appointment.datetime).toLocaleString()}</div>
</div>
<div className="field">
<label>Calendar</label>
<div className="value">{appointment.calendar}</div>
</div>
<div className="field">
<label>Price</label>
<div className="value">{appointment.price} (Paid: {appointment.paid})</div>
</div>
</div>
{appointment.labels && appointment.labels.length > 0 && (
<div className="section">
<h2>Labels</h2>
<div className="labels">
{appointment.labels.map(label => (
<span key={label.id} className="label-tag">{label.name}</span>
))}
</div>
</div>
)}
{appointment.notes && (
<div className="section">
<h2>Notes</h2>
<div className="value">{appointment.notes}</div>
</div>
)}
<div className="actions">
<button className="btn-primary" onClick={handleReschedule}>Reschedule</button>
<button className="btn-secondary">Edit</button>
<button className="btn-danger" onClick={handleCancel}>Cancel Appointment</button>
</div>
</div>
);
}
ReactDOM.render(<AppointmentDetail />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,150 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appointment Grid</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.filters { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; gap: 15px; align-items: center; }
.filter-group { display: flex; flex-direction: column; }
.filter-group label { font-size: 12px; color: #666; margin-bottom: 5px; }
.filter-group select, .filter-group input { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.grid-container { background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
table { width: 100%; border-collapse: collapse; }
thead { background: #F9FAFB; }
th { padding: 12px; text-align: left; font-size: 14px; font-weight: 600; color: #374151; border-bottom: 2px solid #E5E7EB; }
td { padding: 12px; border-bottom: 1px solid #E5E7EB; font-size: 14px; }
tbody tr:hover { background: #F9FAFB; cursor: pointer; }
.status-badge { padding: 4px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; display: inline-block; }
.status-confirmed { background: #D1FAE5; color: #065F46; }
.status-pending { background: #FEF3C7; color: #92400E; }
.status-canceled { background: #FEE2E2; color: #991B1B; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function AppointmentGrid() {
const [appointments, setAppointments] = useState([]);
const [filteredAppointments, setFilteredAppointments] = useState([]);
const [filters, setFilters] = useState({ calendar: 'all', status: 'all', dateRange: 'upcoming' });
useEffect(() => {
fetchAppointments();
}, []);
useEffect(() => {
applyFilters();
}, [appointments, filters]);
const fetchAppointments = async () => {
// Mock data
const mockData = [
{ id: 1, firstName: 'John', lastName: 'Doe', email: 'john@example.com', datetime: '2024-01-15T10:00:00', type: 'Consultation', calendar: 'Dr. Smith', status: 'confirmed' },
{ id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane@example.com', datetime: '2024-01-15T14:30:00', type: 'Follow-up', calendar: 'Dr. Johnson', status: 'confirmed' },
{ id: 3, firstName: 'Bob', lastName: 'Wilson', email: 'bob@example.com', datetime: '2024-01-16T09:00:00', type: 'Assessment', calendar: 'Dr. Smith', status: 'pending' },
{ id: 4, firstName: 'Alice', lastName: 'Brown', email: 'alice@example.com', datetime: '2024-01-16T11:00:00', type: 'Consultation', calendar: 'Dr. Johnson', status: 'confirmed' },
{ id: 5, firstName: 'Charlie', lastName: 'Davis', email: 'charlie@example.com', datetime: '2024-01-17T15:00:00', type: 'Follow-up', calendar: 'Dr. Smith', status: 'canceled' },
];
setAppointments(mockData);
};
const applyFilters = () => {
let filtered = [...appointments];
if (filters.calendar !== 'all') {
filtered = filtered.filter(apt => apt.calendar === filters.calendar);
}
if (filters.status !== 'all') {
filtered = filtered.filter(apt => apt.status === filters.status);
}
setFilteredAppointments(filtered);
};
const handleRowClick = (appointment) => {
alert(\`View details for appointment #\${appointment.id}\`);
};
return (
<div className="container">
<h1>Appointment Grid</h1>
<div className="filters">
<div className="filter-group">
<label>Calendar</label>
<select value={filters.calendar} onChange={(e) => setFilters({...filters, calendar: e.target.value})}>
<option value="all">All Calendars</option>
<option value="Dr. Smith">Dr. Smith</option>
<option value="Dr. Johnson">Dr. Johnson</option>
</select>
</div>
<div className="filter-group">
<label>Status</label>
<select value={filters.status} onChange={(e) => setFilters({...filters, status: e.target.value})}>
<option value="all">All Statuses</option>
<option value="confirmed">Confirmed</option>
<option value="pending">Pending</option>
<option value="canceled">Canceled</option>
</select>
</div>
<div className="filter-group">
<label>Date Range</label>
<select value={filters.dateRange} onChange={(e) => setFilters({...filters, dateRange: e.target.value})}>
<option value="upcoming">Upcoming</option>
<option value="today">Today</option>
<option value="week">This Week</option>
<option value="month">This Month</option>
</select>
</div>
</div>
<div className="grid-container">
<table>
<thead>
<tr>
<th>ID</th>
<th>Client</th>
<th>Email</th>
<th>Date & Time</th>
<th>Type</th>
<th>Calendar</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{filteredAppointments.map(apt => (
<tr key={apt.id} onClick={() => handleRowClick(apt)}>
<td>{apt.id}</td>
<td>{apt.firstName} {apt.lastName}</td>
<td>{apt.email}</td>
<td>{new Date(apt.datetime).toLocaleString()}</td>
<td>{apt.type}</td>
<td>{apt.calendar}</td>
<td>
<span className={\`status-badge status-\${apt.status}\`}>
{apt.status.charAt(0).toUpperCase() + apt.status.slice(1)}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
ReactDOM.render(<AppointmentGrid />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,163 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Availability Calendar</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 900px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.calendar-header { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.calendar-header select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; margin-right: 10px; }
.calendar-grid { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; background: white; padding: 20px; border-radius: 8px; }
.day-header { text-align: center; font-weight: 600; color: #666; padding: 10px; font-size: 14px; }
.day-cell { aspect-ratio: 1; border: 2px solid #E5E7EB; border-radius: 8px; padding: 8px; cursor: pointer; transition: all 0.2s; position: relative; }
.day-cell:hover { border-color: #4F46E5; }
.day-cell.available { background: #D1FAE5; border-color: #10B981; }
.day-cell.limited { background: #FEF3C7; border-color: #F59E0B; }
.day-cell.unavailable { background: #F3F4F6; border-color: #D1D5DB; cursor: not-allowed; }
.day-cell.selected { border-color: #4F46E5; border-width: 3px; }
.day-number { font-weight: 600; font-size: 16px; margin-bottom: 4px; }
.slots-available { font-size: 11px; color: #666; }
.time-slots { background: white; padding: 20px; border-radius: 8px; margin-top: 20px; }
.time-slot { display: inline-block; padding: 8px 16px; margin: 5px; border: 1px solid #ddd; border-radius: 6px; cursor: pointer; background: white; }
.time-slot:hover { background: #4F46E5; color: white; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function AvailabilityCalendar() {
const [selectedDate, setSelectedDate] = useState(null);
const [appointmentType, setAppointmentType] = useState('1');
const [calendar, setCalendar] = useState('all');
const [availabilityData, setAvailabilityData] = useState({});
const [timeSlots, setTimeSlots] = useState([]);
useEffect(() => {
fetchAvailability();
}, [appointmentType, calendar]);
useEffect(() => {
if (selectedDate) {
fetchTimeSlots(selectedDate);
}
}, [selectedDate]);
const fetchAvailability = async () => {
// Mock availability data
const mockData = {
'2024-01-15': { available: true, slots: 8 },
'2024-01-16': { available: true, slots: 5 },
'2024-01-17': { available: true, slots: 3 },
'2024-01-18': { available: true, slots: 10 },
'2024-01-19': { available: true, slots: 2 },
'2024-01-22': { available: true, slots: 6 },
'2024-01-23': { available: false, slots: 0 },
};
setAvailabilityData(mockData);
};
const fetchTimeSlots = async (date) => {
// Mock time slots
const mockSlots = [
'09:00 AM', '10:00 AM', '11:00 AM', '01:00 PM', '02:00 PM', '03:00 PM', '04:00 PM'
];
setTimeSlots(mockSlots);
};
const getDaysInMonth = () => {
const days = [];
const firstDay = new Date(2024, 0, 1);
const lastDay = new Date(2024, 0, 31);
for (let d = new Date(firstDay); d <= lastDay; d.setDate(d.getDate() + 1)) {
days.push(new Date(d));
}
return days;
};
const getDayClass = (date) => {
const dateStr = date.toISOString().split('T')[0];
const availability = availabilityData[dateStr];
if (!availability) return 'unavailable';
if (availability.slots > 5) return 'available';
if (availability.slots > 0) return 'limited';
return 'unavailable';
};
const handleDateClick = (date) => {
const dateStr = date.toISOString().split('T')[0];
const availability = availabilityData[dateStr];
if (availability && availability.slots > 0) {
setSelectedDate(dateStr);
}
};
return (
<div className="container">
<h1>Availability Calendar</h1>
<div className="calendar-header">
<select value={appointmentType} onChange={(e) => setAppointmentType(e.target.value)}>
<option value="1">Initial Consultation</option>
<option value="2">Follow-up Appointment</option>
<option value="3">Assessment</option>
</select>
<select value={calendar} onChange={(e) => setCalendar(e.target.value)}>
<option value="all">All Calendars</option>
<option value="dr-smith">Dr. Smith</option>
<option value="dr-johnson">Dr. Johnson</option>
</select>
</div>
<div className="calendar-grid">
{['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map(day => (
<div key={day} className="day-header">{day}</div>
))}
{getDaysInMonth().map((date, idx) => {
const dateStr = date.toISOString().split('T')[0];
const availability = availabilityData[dateStr];
const isSelected = selectedDate === dateStr;
return (
<div
key={idx}
className={\`day-cell \${getDayClass(date)} \${isSelected ? 'selected' : ''}\`}
onClick={() => handleDateClick(date)}
>
<div className="day-number">{date.getDate()}</div>
{availability && availability.slots > 0 && (
<div className="slots-available">{availability.slots} slots</div>
)}
</div>
);
})}
</div>
{selectedDate && timeSlots.length > 0 && (
<div className="time-slots">
<h2>Available Times for {selectedDate}</h2>
<div>
{timeSlots.map(slot => (
<button key={slot} className="time-slot">{slot}</button>
))}
</div>
</div>
)}
</div>
);
}
ReactDOM.render(<AvailabilityCalendar />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,267 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Blocked Time Manager</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.toolbar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.filters { display: flex; gap: 15px; }
select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.blocks-list { background: white; border-radius: 8px; overflow: hidden; }
.block-item { padding: 20px; border-bottom: 1px solid #E5E7EB; display: flex; justify-content: space-between; align-items: center; }
.block-item:last-child { border-bottom: none; }
.block-info { flex: 1; }
.block-date { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 5px; }
.block-time { font-size: 14px; color: #666; margin-bottom: 5px; }
.block-calendar { font-size: 14px; color: #4F46E5; font-weight: 500; }
.block-notes { font-size: 13px; color: #666; margin-top: 8px; padding: 8px; background: #F9FAFB; border-radius: 4px; }
.block-actions { display: flex; gap: 10px; }
.btn-small { padding: 6px 12px; font-size: 13px; }
.btn-secondary { background: #6B7280; }
.btn-secondary:hover { background: #4B5563; }
.btn-danger { background: #DC2626; }
.btn-danger:hover { background: #B91C1C; }
.modal { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { background: white; padding: 30px; border-radius: 8px; max-width: 500px; width: 90%; }
.modal-title { font-size: 20px; font-weight: 600; margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; color: #333; margin-bottom: 5px; }
.form-group input, .form-group select, .form-group textarea { width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; }
.form-group textarea { min-height: 80px; resize: vertical; }
.modal-actions { display: flex; justify-content: flex-end; gap: 10px; margin-top: 20px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function BlockedTimeManager() {
const [blocks, setBlocks] = useState([]);
const [showModal, setShowModal] = useState(false);
const [calendarFilter, setCalendarFilter] = useState('all');
const [calendars, setCalendars] = useState([]);
const [formData, setFormData] = useState({
calendarID: '',
startDate: '',
startTime: '',
endDate: '',
endTime: '',
notes: ''
});
useEffect(() => {
fetchBlocks();
fetchCalendars();
}, []);
const fetchCalendars = async () => {
const mockCalendars = [
{ id: 1, name: 'Dr. Smith' },
{ id: 2, name: 'Dr. Johnson' },
{ id: 3, name: 'Dr. Davis' }
];
setCalendars(mockCalendars);
};
const fetchBlocks = async () => {
const mockBlocks = [
{
id: 1,
calendarID: 1,
calendar: 'Dr. Smith',
start: '2024-01-20T13:00:00',
end: '2024-01-20T17:00:00',
notes: 'Attending medical conference'
},
{
id: 2,
calendarID: 2,
calendar: 'Dr. Johnson',
start: '2024-01-22T09:00:00',
end: '2024-01-22T12:00:00',
notes: 'Personal time off'
},
{
id: 3,
calendarID: 1,
calendar: 'Dr. Smith',
start: '2024-01-25T14:00:00',
end: '2024-01-25T16:00:00',
notes: 'Department meeting'
},
];
setBlocks(mockBlocks);
};
const handleAddBlock = () => {
setFormData({
calendarID: '',
startDate: '',
startTime: '',
endDate: '',
endTime: '',
notes: ''
});
setShowModal(true);
};
const handleSaveBlock = () => {
if (!formData.calendarID || !formData.startDate || !formData.startTime || !formData.endDate || !formData.endTime) {
alert('Please fill in all required fields');
return;
}
alert('Block created successfully');
setShowModal(false);
};
const handleDeleteBlock = (block) => {
if (confirm(\`Delete blocked time on \${new Date(block.start).toLocaleDateString()}?\`)) {
setBlocks(blocks.filter(b => b.id !== block.id));
}
};
const filteredBlocks = calendarFilter === 'all'
? blocks
: blocks.filter(b => b.calendarID === parseInt(calendarFilter));
const formatDateTime = (datetime) => {
const date = new Date(datetime);
return {
date: date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' }),
time: date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })
};
};
return (
<div className="container">
<h1>Blocked Time Manager</h1>
<div className="toolbar">
<div className="filters">
<div>
<label style={{ fontSize: '14px', marginRight: '10px', color: '#666' }}>Filter by Calendar:</label>
<select value={calendarFilter} onChange={(e) => setCalendarFilter(e.target.value)}>
<option value="all">All Calendars</option>
{calendars.map(cal => (
<option key={cal.id} value={cal.id}>{cal.name}</option>
))}
</select>
</div>
</div>
<button onClick={handleAddBlock}>+ Block Time</button>
</div>
<div className="blocks-list">
{filteredBlocks.map(block => {
const start = formatDateTime(block.start);
const end = formatDateTime(block.end);
return (
<div key={block.id} className="block-item">
<div className="block-info">
<div className="block-date">{start.date}</div>
<div className="block-time">{start.time} - {end.time}</div>
<div className="block-calendar">📅 {block.calendar}</div>
{block.notes && <div className="block-notes">📝 {block.notes}</div>}
</div>
<div className="block-actions">
<button className="btn-small btn-danger" onClick={() => handleDeleteBlock(block)}>
Delete
</button>
</div>
</div>
);
})}
</div>
{filteredBlocks.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666', background: 'white', borderRadius: '8px' }}>
No blocked time slots found
</div>
)}
{showModal && (
<div className="modal" onClick={() => setShowModal(false)}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<div className="modal-title">Block Time</div>
<div className="form-group">
<label>Calendar *</label>
<select value={formData.calendarID} onChange={(e) => setFormData({...formData, calendarID: e.target.value})}>
<option value="">Select a calendar...</option>
{calendars.map(cal => (
<option key={cal.id} value={cal.id}>{cal.name}</option>
))}
</select>
</div>
<div className="form-group">
<label>Start Date *</label>
<input
type="date"
value={formData.startDate}
onChange={(e) => setFormData({...formData, startDate: e.target.value})}
/>
</div>
<div className="form-group">
<label>Start Time *</label>
<input
type="time"
value={formData.startTime}
onChange={(e) => setFormData({...formData, startTime: e.target.value})}
/>
</div>
<div className="form-group">
<label>End Date *</label>
<input
type="date"
value={formData.endDate}
onChange={(e) => setFormData({...formData, endDate: e.target.value})}
/>
</div>
<div className="form-group">
<label>End Time *</label>
<input
type="time"
value={formData.endTime}
onChange={(e) => setFormData({...formData, endTime: e.target.value})}
/>
</div>
<div className="form-group">
<label>Notes</label>
<textarea
value={formData.notes}
onChange={(e) => setFormData({...formData, notes: e.target.value})}
placeholder="Reason for blocking this time..."
/>
</div>
<div className="modal-actions">
<button className="btn-secondary" onClick={() => setShowModal(false)}>Cancel</button>
<button onClick={handleSaveBlock}>Save Block</button>
</div>
</div>
</div>
)}
</div>
);
}
ReactDOM.render(<BlockedTimeManager />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,230 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Booking Flow</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 800px; margin: 0 auto; }
.booking-card { background: white; border-radius: 8px; padding: 30px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); }
h1 { color: #333; margin-bottom: 10px; }
.subtitle { color: #666; margin-bottom: 30px; }
.progress-bar { display: flex; justify-content: space-between; margin-bottom: 40px; }
.progress-step { flex: 1; text-align: center; position: relative; }
.progress-step::after { content: ''; position: absolute; top: 15px; left: 50%; width: 100%; height: 2px; background: #E5E7EB; z-index: -1; }
.progress-step:last-child::after { display: none; }
.step-circle { width: 30px; height: 30px; border-radius: 50%; background: #E5E7EB; color: #9CA3AF; display: inline-flex; align-items: center; justify-content: center; font-weight: 600; margin-bottom: 8px; }
.progress-step.active .step-circle { background: #4F46E5; color: white; }
.progress-step.completed .step-circle { background: #10B981; color: white; }
.step-label { font-size: 13px; color: #666; }
.section-title { font-size: 20px; font-weight: 600; color: #333; margin-bottom: 20px; }
.service-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 15px; margin-bottom: 30px; }
.service-card { border: 2px solid #E5E7EB; border-radius: 8px; padding: 20px; cursor: pointer; transition: all 0.2s; }
.service-card:hover { border-color: #4F46E5; }
.service-card.selected { border-color: #4F46E5; background: #EEF2FF; }
.service-name { font-weight: 600; color: #333; margin-bottom: 5px; }
.service-duration { font-size: 13px; color: #666; }
.service-price { font-size: 16px; font-weight: 700; color: #4F46E5; margin-top: 10px; }
.form-group { margin-bottom: 20px; }
.form-group label { display: block; font-size: 14px; font-weight: 500; color: #333; margin-bottom: 8px; }
.form-group input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box; }
.actions { display: flex; justify-content: space-between; margin-top: 30px; }
button { padding: 12px 24px; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
.btn-primary { background: #4F46E5; color: white; }
.btn-primary:hover { background: #4338CA; }
.btn-secondary { background: #E5E7EB; color: #374151; }
.btn-secondary:hover { background: #D1D5DB; }
.time-slots { display: grid; grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); gap: 10px; margin-bottom: 30px; }
.time-slot { padding: 12px; border: 1px solid #E5E7EB; border-radius: 6px; text-align: center; cursor: pointer; transition: all 0.2s; }
.time-slot:hover { border-color: #4F46E5; }
.time-slot.selected { border-color: #4F46E5; background: #EEF2FF; font-weight: 600; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;
function BookingFlow() {
const [step, setStep] = useState(1);
const [selectedService, setSelectedService] = useState(null);
const [selectedDate, setSelectedDate] = useState(null);
const [selectedTime, setSelectedTime] = useState(null);
const [formData, setFormData] = useState({
firstName: '',
lastName: '',
email: '',
phone: ''
});
const services = [
{ id: 1, name: 'Initial Consultation', duration: '60 min', price: '$150' },
{ id: 2, name: 'Follow-up Visit', duration: '30 min', price: '$100' },
{ id: 3, name: 'Comprehensive Assessment', duration: '90 min', price: '$225' },
];
const timeSlots = ['09:00 AM', '10:00 AM', '11:00 AM', '01:00 PM', '02:00 PM', '03:00 PM', '04:00 PM'];
const handleNext = () => {
if (step === 1 && !selectedService) {
alert('Please select a service');
return;
}
if (step === 2 && (!selectedDate || !selectedTime)) {
alert('Please select a date and time');
return;
}
if (step === 3) {
if (!formData.firstName || !formData.lastName || !formData.email || !formData.phone) {
alert('Please fill in all fields');
return;
}
handleConfirm();
return;
}
setStep(step + 1);
};
const handleBack = () => {
setStep(step - 1);
};
const handleConfirm = () => {
alert(\`Booking confirmed!
Service: \${services.find(s => s.id === selectedService)?.name}
Date: \${selectedDate}
Time: \${selectedTime}
Name: \${formData.firstName} \${formData.lastName}
Email: \${formData.email}\`);
};
return (
<div className="container">
<div className="booking-card">
<h1>Book an Appointment</h1>
<p className="subtitle">Complete the steps below to schedule your visit</p>
<div className="progress-bar">
<div className={\`progress-step \${step >= 1 ? 'active' : ''} \${step > 1 ? 'completed' : ''}\`}>
<div className="step-circle">1</div>
<div className="step-label">Select Service</div>
</div>
<div className={\`progress-step \${step >= 2 ? 'active' : ''} \${step > 2 ? 'completed' : ''}\`}>
<div className="step-circle">2</div>
<div className="step-label">Choose Date & Time</div>
</div>
<div className={\`progress-step \${step >= 3 ? 'active' : ''}\`}>
<div className="step-circle">3</div>
<div className="step-label">Your Information</div>
</div>
</div>
{step === 1 && (
<div>
<div className="section-title">Select a Service</div>
<div className="service-grid">
{services.map(service => (
<div
key={service.id}
className={\`service-card \${selectedService === service.id ? 'selected' : ''}\`}
onClick={() => setSelectedService(service.id)}
>
<div className="service-name">{service.name}</div>
<div className="service-duration">{service.duration}</div>
<div className="service-price">{service.price}</div>
</div>
))}
</div>
</div>
)}
{step === 2 && (
<div>
<div className="section-title">Choose Date & Time</div>
<div className="form-group">
<label>Select Date</label>
<input
type="date"
value={selectedDate || ''}
onChange={(e) => setSelectedDate(e.target.value)}
min={new Date().toISOString().split('T')[0]}
/>
</div>
{selectedDate && (
<div>
<label style={{ display: 'block', marginBottom: '10px', fontWeight: 500 }}>Select Time</label>
<div className="time-slots">
{timeSlots.map(time => (
<div
key={time}
className={\`time-slot \${selectedTime === time ? 'selected' : ''}\`}
onClick={() => setSelectedTime(time)}
>
{time}
</div>
))}
</div>
</div>
)}
</div>
)}
{step === 3 && (
<div>
<div className="section-title">Your Information</div>
<div className="form-group">
<label>First Name</label>
<input
type="text"
value={formData.firstName}
onChange={(e) => setFormData({...formData, firstName: e.target.value})}
/>
</div>
<div className="form-group">
<label>Last Name</label>
<input
type="text"
value={formData.lastName}
onChange={(e) => setFormData({...formData, lastName: e.target.value})}
/>
</div>
<div className="form-group">
<label>Email</label>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
/>
</div>
<div className="form-group">
<label>Phone</label>
<input
type="tel"
value={formData.phone}
onChange={(e) => setFormData({...formData, phone: e.target.value})}
/>
</div>
</div>
)}
<div className="actions">
<button className="btn-secondary" onClick={handleBack} disabled={step === 1}>
Back
</button>
<button className="btn-primary" onClick={handleNext}>
{step === 3 ? 'Confirm Booking' : 'Next'}
</button>
</div>
</div>
</div>
);
}
ReactDOM.render(<BookingFlow />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,145 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Manager</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1000px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.toolbar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.calendars-list { background: white; border-radius: 8px; overflow: hidden; }
.calendar-item { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.calendar-item:last-child { border-bottom: none; }
.calendar-info { flex: 1; }
.calendar-name { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 5px; }
.calendar-details { font-size: 14px; color: #666; }
.calendar-actions { display: flex; gap: 10px; }
.btn-small { padding: 6px 12px; font-size: 13px; }
.btn-secondary { background: #6B7280; }
.btn-secondary:hover { background: #4B5563; }
.calendar-stats { display: flex; gap: 20px; margin-top: 10px; }
.stat { font-size: 12px; color: #666; }
.stat strong { color: #4F46E5; font-weight: 600; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function CalendarManager() {
const [calendars, setCalendars] = useState([]);
useEffect(() => {
fetchCalendars();
}, []);
const fetchCalendars = async () => {
// Mock data
const mockCalendars = [
{
id: 1,
name: 'Dr. Sarah Smith',
email: 'sarah.smith@clinic.com',
location: 'Room 101',
timezone: 'America/New_York',
appointmentsToday: 6,
appointmentsWeek: 28
},
{
id: 2,
name: 'Dr. James Johnson',
email: 'james.johnson@clinic.com',
location: 'Room 102',
timezone: 'America/New_York',
appointmentsToday: 4,
appointmentsWeek: 22
},
{
id: 3,
name: 'Dr. Emily Davis',
email: 'emily.davis@clinic.com',
location: 'Room 103',
timezone: 'America/New_York',
appointmentsToday: 7,
appointmentsWeek: 31
},
];
setCalendars(mockCalendars);
};
const handleAddCalendar = () => {
alert('Add new calendar - would open form');
};
const handleEditCalendar = (calendar) => {
alert(\`Edit calendar: \${calendar.name}\`);
};
const handleDeleteCalendar = (calendar) => {
if (confirm(\`Are you sure you want to delete \${calendar.name}?\`)) {
alert('Calendar deleted');
}
};
const handleViewSchedule = (calendar) => {
alert(\`View schedule for \${calendar.name}\`);
};
return (
<div className="container">
<h1>Calendar Manager</h1>
<div className="toolbar">
<h2 style={{ margin: 0, fontSize: '18px', color: '#666' }}>
{calendars.length} Calendar{calendars.length !== 1 ? 's' : ''}
</h2>
<button onClick={handleAddCalendar}>+ Add Calendar</button>
</div>
<div className="calendars-list">
{calendars.map(calendar => (
<div key={calendar.id} className="calendar-item">
<div className="calendar-info">
<div className="calendar-name">{calendar.name}</div>
<div className="calendar-details">
📧 {calendar.email} 📍 {calendar.location} 🌍 {calendar.timezone}
</div>
<div className="calendar-stats">
<div className="stat">
<strong>{calendar.appointmentsToday}</strong> appointments today
</div>
<div className="stat">
<strong>{calendar.appointmentsWeek}</strong> this week
</div>
</div>
</div>
<div className="calendar-actions">
<button className="btn-small" onClick={() => handleViewSchedule(calendar)}>
View Schedule
</button>
<button className="btn-small btn-secondary" onClick={() => handleEditCalendar(calendar)}>
Edit
</button>
<button className="btn-small btn-secondary" onClick={() => handleDeleteCalendar(calendar)}>
Delete
</button>
</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<CalendarManager />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,148 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Detail</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1000px; margin: 0 auto; }
.header { background: white; padding: 30px; border-radius: 8px; margin-bottom: 20px; }
.client-name { font-size: 28px; font-weight: 700; color: #333; margin-bottom: 10px; }
.client-meta { display: flex; gap: 20px; color: #666; font-size: 14px; }
.content-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
.card { background: white; padding: 20px; border-radius: 8px; }
.card h2 { margin: 0 0 15px 0; font-size: 18px; color: #333; }
.info-row { display: flex; justify-content: space-between; margin-bottom: 10px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
.info-row:last-child { border-bottom: none; }
.label { color: #666; font-size: 14px; }
.value { color: #333; font-weight: 500; }
.appointments-list { background: white; padding: 20px; border-radius: 8px; }
.appointment-item { padding: 15px; border: 1px solid #eee; border-radius: 6px; margin-bottom: 10px; }
.appointment-item:last-child { margin-bottom: 0; }
.appointment-date { font-weight: 600; color: #4F46E5; margin-bottom: 5px; }
.appointment-type { color: #666; font-size: 14px; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; margin-right: 10px; }
button:hover { background: #4338CA; }
.btn-secondary { background: #6B7280; }
.btn-secondary:hover { background: #4B5563; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ClientDetail() {
const [client, setClient] = useState(null);
const [appointments, setAppointments] = useState([]);
useEffect(() => {
fetchClientData();
}, []);
const fetchClientData = async () => {
// Mock data
const mockClient = {
id: 123,
firstName: 'John',
lastName: 'Doe',
email: 'john.doe@example.com',
phone: '(555) 123-4567',
dateCreated: '2023-06-15',
totalAppointments: 8,
totalSpent: '$1,200.00',
notes: 'Prefers morning appointments. Allergic to latex.'
};
const mockAppointments = [
{ id: 1, datetime: '2024-01-15T10:00:00', type: 'Initial Consultation', status: 'confirmed', price: '$150' },
{ id: 2, datetime: '2024-01-22T14:30:00', type: 'Follow-up', status: 'confirmed', price: '$100' },
{ id: 3, datetime: '2023-12-10T09:00:00', type: 'Assessment', status: 'completed', price: '$200' },
];
setClient(mockClient);
setAppointments(mockAppointments);
};
if (!client) return <div className="container"><h1>Loading...</h1></div>;
return (
<div className="container">
<div className="header">
<div className="client-name">{client.firstName} {client.lastName}</div>
<div className="client-meta">
<span>📧 {client.email}</span>
<span>📞 {client.phone}</span>
<span>📅 Client since {new Date(client.dateCreated).toLocaleDateString()}</span>
</div>
</div>
<div className="content-grid">
<div className="card">
<h2>Contact Information</h2>
<div className="info-row">
<span className="label">Email</span>
<span className="value">{client.email}</span>
</div>
<div className="info-row">
<span className="label">Phone</span>
<span className="value">{client.phone}</span>
</div>
</div>
<div className="card">
<h2>Statistics</h2>
<div className="info-row">
<span className="label">Total Appointments</span>
<span className="value">{client.totalAppointments}</span>
</div>
<div className="info-row">
<span className="label">Total Spent</span>
<span className="value">{client.totalSpent}</span>
</div>
</div>
</div>
{client.notes && (
<div className="card" style={{ marginBottom: '20px' }}>
<h2>Notes</h2>
<p style={{ margin: 0, color: '#666' }}>{client.notes}</p>
</div>
)}
<div className="appointments-list">
<h2>Appointment History</h2>
{appointments.map(apt => (
<div key={apt.id} className="appointment-item">
<div className="appointment-date">
{new Date(apt.datetime).toLocaleString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
hour: 'numeric',
minute: '2-digit'
})}
</div>
<div className="appointment-type">
{apt.type} {apt.status} {apt.price}
</div>
</div>
))}
</div>
<div style={{ marginTop: '20px' }}>
<button>Edit Client</button>
<button className="btn-secondary">Delete Client</button>
</div>
</div>
);
}
ReactDOM.render(<ClientDetail />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,125 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Client Directory</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.toolbar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.search-bar { flex: 1; max-width: 400px; }
.search-bar input { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.clients-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.client-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); cursor: pointer; transition: all 0.2s; }
.client-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.15); transform: translateY(-2px); }
.client-name { font-size: 18px; font-weight: 600; color: #333; margin-bottom: 10px; }
.client-info { font-size: 14px; color: #666; margin-bottom: 5px; }
.client-stats { display: flex; gap: 15px; margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; }
.stat { font-size: 12px; color: #666; }
.stat strong { display: block; font-size: 18px; color: #4F46E5; font-weight: 600; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ClientDirectory() {
const [clients, setClients] = useState([]);
const [filteredClients, setFilteredClients] = useState([]);
const [searchTerm, setSearchTerm] = useState('');
useEffect(() => {
fetchClients();
}, []);
useEffect(() => {
if (searchTerm) {
const filtered = clients.filter(client =>
client.firstName.toLowerCase().includes(searchTerm.toLowerCase()) ||
client.lastName.toLowerCase().includes(searchTerm.toLowerCase()) ||
client.email.toLowerCase().includes(searchTerm.toLowerCase())
);
setFilteredClients(filtered);
} else {
setFilteredClients(clients);
}
}, [searchTerm, clients]);
const fetchClients = async () => {
// Mock data
const mockClients = [
{ id: 1, firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com', phone: '(555) 123-4567', totalAppointments: 8, upcomingAppointments: 1 },
{ id: 2, firstName: 'Jane', lastName: 'Smith', email: 'jane.smith@example.com', phone: '(555) 234-5678', totalAppointments: 12, upcomingAppointments: 2 },
{ id: 3, firstName: 'Bob', lastName: 'Wilson', email: 'bob.wilson@example.com', phone: '(555) 345-6789', totalAppointments: 5, upcomingAppointments: 0 },
{ id: 4, firstName: 'Alice', lastName: 'Brown', email: 'alice.brown@example.com', phone: '(555) 456-7890', totalAppointments: 15, upcomingAppointments: 3 },
{ id: 5, firstName: 'Charlie', lastName: 'Davis', email: 'charlie.davis@example.com', phone: '(555) 567-8901', totalAppointments: 3, upcomingAppointments: 1 },
{ id: 6, firstName: 'Emma', lastName: 'Johnson', email: 'emma.johnson@example.com', phone: '(555) 678-9012', totalAppointments: 20, upcomingAppointments: 2 },
];
setClients(mockClients);
};
const handleClientClick = (client) => {
alert(\`View details for \${client.firstName} \${client.lastName}\`);
};
const handleAddClient = () => {
alert('Add new client - would open form');
};
return (
<div className="container">
<h1>Client Directory</h1>
<div className="toolbar">
<div className="search-bar">
<input
type="text"
placeholder="Search clients by name or email..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
<button onClick={handleAddClient}>+ Add Client</button>
</div>
<div className="clients-grid">
{filteredClients.map(client => (
<div key={client.id} className="client-card" onClick={() => handleClientClick(client)}>
<div className="client-name">{client.firstName} {client.lastName}</div>
<div className="client-info">📧 {client.email}</div>
<div className="client-info">📞 {client.phone}</div>
<div className="client-stats">
<div className="stat">
<strong>{client.totalAppointments}</strong>
Total Visits
</div>
<div className="stat">
<strong>{client.upcomingAppointments}</strong>
Upcoming
</div>
</div>
</div>
))}
</div>
{filteredClients.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
No clients found matching "{searchTerm}"
</div>
)}
</div>
);
}
ReactDOM.render(<ClientDirectory />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,184 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Coupon Manager</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.toolbar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.coupons-list { background: white; border-radius: 8px; overflow: hidden; }
.coupon-item { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.coupon-item:last-child { border-bottom: none; }
.coupon-main { flex: 1; }
.coupon-code { font-size: 20px; font-weight: 700; color: #4F46E5; font-family: monospace; margin-bottom: 5px; }
.coupon-details { font-size: 14px; color: #666; }
.coupon-stats { display: flex; gap: 20px; margin-top: 10px; }
.stat { font-size: 13px; }
.stat-label { color: #666; }
.stat-value { font-weight: 600; color: #333; }
.coupon-status { padding: 6px 12px; border-radius: 12px; font-size: 12px; font-weight: 500; }
.status-active { background: #D1FAE5; color: #065F46; }
.status-expired { background: #FEE2E2; color: #991B1B; }
.status-maxed { background: #F3F4F6; color: #6B7280; }
.coupon-actions { display: flex; gap: 10px; }
.btn-small { padding: 6px 12px; font-size: 13px; }
.btn-secondary { background: #6B7280; }
.btn-secondary:hover { background: #4B5563; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function CouponManager() {
const [coupons, setCoupons] = useState([]);
useEffect(() => {
fetchCoupons();
}, []);
const fetchCoupons = async () => {
// Mock data
const mockCoupons = [
{
id: 1,
code: 'WELCOME20',
name: 'New Client Welcome',
percentageOff: 20,
validFrom: '2024-01-01',
validTo: '2024-12-31',
maxUses: 100,
timesUsed: 23,
status: 'active'
},
{
id: 2,
code: 'SUMMER50',
name: 'Summer Special',
amountOff: 50,
validFrom: '2024-06-01',
validTo: '2024-08-31',
maxUses: 50,
timesUsed: 12,
status: 'active'
},
{
id: 3,
code: 'HOLIDAY2023',
name: 'Holiday Promotion',
percentageOff: 30,
validFrom: '2023-11-01',
validTo: '2023-12-31',
maxUses: 200,
timesUsed: 156,
status: 'expired'
},
{
id: 4,
code: 'VIP10',
name: 'VIP Discount',
percentageOff: 10,
validFrom: '2024-01-01',
validTo: '2024-12-31',
maxUses: 10,
timesUsed: 10,
status: 'maxed'
},
];
setCoupons(mockCoupons);
};
const handleAddCoupon = () => {
alert('Create new coupon - would open form');
};
const handleEditCoupon = (coupon) => {
alert(\`Edit coupon: \${coupon.code}\`);
};
const handleDeleteCoupon = (coupon) => {
if (confirm(\`Delete coupon "\${coupon.code}"?\`)) {
setCoupons(coupons.filter(c => c.id !== coupon.id));
}
};
const getDiscountText = (coupon) => {
if (coupon.percentageOff) return \`\${coupon.percentageOff}% off\`;
if (coupon.amountOff) return \`$\${coupon.amountOff} off\`;
return 'Discount';
};
const getStatusClass = (status) => {
if (status === 'active') return 'status-active';
if (status === 'expired') return 'status-expired';
if (status === 'maxed') return 'status-maxed';
return '';
};
const getStatusText = (status) => {
if (status === 'active') return 'Active';
if (status === 'expired') return 'Expired';
if (status === 'maxed') return 'Max Uses Reached';
return status;
};
return (
<div className="container">
<h1>Coupon Manager</h1>
<div className="toolbar">
<div style={{ color: '#666' }}>
{coupons.length} coupon{coupons.length !== 1 ? 's' : ''}
</div>
<button onClick={handleAddCoupon}>+ Create Coupon</button>
</div>
<div className="coupons-list">
{coupons.map(coupon => (
<div key={coupon.id} className="coupon-item">
<div className="coupon-main">
<div className="coupon-code">{coupon.code}</div>
<div className="coupon-details">
{coupon.name} {getDiscountText(coupon)}
Valid {new Date(coupon.validFrom).toLocaleDateString()} - {new Date(coupon.validTo).toLocaleDateString()}
</div>
<div className="coupon-stats">
<div className="stat">
<span className="stat-label">Used: </span>
<span className="stat-value">{coupon.timesUsed}/{coupon.maxUses}</span>
</div>
<div className="stat">
<span className="stat-label">Remaining: </span>
<span className="stat-value">{coupon.maxUses - coupon.timesUsed}</span>
</div>
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '15px' }}>
<span className={\`coupon-status \${getStatusClass(coupon.status)}\`}>
{getStatusText(coupon.status)}
</span>
<div className="coupon-actions">
<button className="btn-small" onClick={() => handleEditCoupon(coupon)}>Edit</button>
<button className="btn-small btn-secondary" onClick={() => handleDeleteCoupon(coupon)}>Delete</button>
</div>
</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<CouponManager />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,161 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Form Responses</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.filters { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; gap: 15px; }
select { padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.responses-list { background: white; border-radius: 8px; padding: 20px; }
.response-item { border: 1px solid #E5E7EB; border-radius: 8px; padding: 20px; margin-bottom: 15px; }
.response-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; padding-bottom: 15px; border-bottom: 1px solid #E5E7EB; }
.client-name { font-size: 18px; font-weight: 600; color: #333; }
.appointment-info { font-size: 14px; color: #666; }
.form-field { margin-bottom: 15px; }
.field-label { font-size: 14px; color: #666; margin-bottom: 5px; font-weight: 500; }
.field-value { font-size: 14px; color: #333; padding: 10px; background: #F9FAFB; border-radius: 4px; }
button { padding: 8px 16px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; }
button:hover { background: #4338CA; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function FormResponses() {
const [responses, setResponses] = useState([]);
const [filteredResponses, setFilteredResponses] = useState([]);
const [formFilter, setFormFilter] = useState('all');
const [forms, setForms] = useState([]);
useEffect(() => {
fetchFormResponses();
}, []);
useEffect(() => {
if (formFilter === 'all') {
setFilteredResponses(responses);
} else {
setFilteredResponses(responses.filter(r => r.formId === parseInt(formFilter)));
}
}, [formFilter, responses]);
const fetchFormResponses = async () => {
// Mock data
const mockForms = [
{ id: 1, name: 'Medical History' },
{ id: 2, name: 'Intake Questionnaire' },
{ id: 3, name: 'COVID-19 Screening' }
];
const mockResponses = [
{
id: 1,
appointmentId: 101,
clientName: 'John Doe',
datetime: '2024-01-15T10:00:00',
formId: 1,
formName: 'Medical History',
fields: [
{ label: 'Current Medications', value: 'Aspirin 81mg daily' },
{ label: 'Known Allergies', value: 'Penicillin' },
{ label: 'Previous Surgeries', value: 'Appendectomy (2015)' },
{ label: 'Emergency Contact', value: 'Jane Doe - (555) 987-6543' }
]
},
{
id: 2,
appointmentId: 102,
clientName: 'Jane Smith',
datetime: '2024-01-15T14:30:00',
formId: 2,
formName: 'Intake Questionnaire',
fields: [
{ label: 'Reason for Visit', value: 'Annual checkup' },
{ label: 'Preferred Contact Method', value: 'Email' },
{ label: 'Insurance Provider', value: 'Blue Cross Blue Shield' }
]
},
{
id: 3,
appointmentId: 103,
clientName: 'Bob Wilson',
datetime: '2024-01-16T09:00:00',
formId: 3,
formName: 'COVID-19 Screening',
fields: [
{ label: 'Recent Symptoms', value: 'None' },
{ label: 'Recent Travel', value: 'No international travel' },
{ label: 'Temperature Check', value: '98.6°F' }
]
}
];
setForms(mockForms);
setResponses(mockResponses);
};
const handleExport = (response) => {
alert(\`Export responses for \${response.clientName}\`);
};
return (
<div className="container">
<h1>Form Responses</h1>
<div className="filters">
<div>
<label style={{ fontSize: '14px', marginRight: '10px', color: '#666' }}>Filter by Form:</label>
<select value={formFilter} onChange={(e) => setFormFilter(e.target.value)}>
<option value="all">All Forms</option>
{forms.map(form => (
<option key={form.id} value={form.id}>{form.name}</option>
))}
</select>
</div>
</div>
<div className="responses-list">
{filteredResponses.map(response => (
<div key={response.id} className="response-item">
<div className="response-header">
<div>
<div className="client-name">{response.clientName}</div>
<div className="appointment-info">
{response.formName} {new Date(response.datetime).toLocaleString()}
</div>
</div>
<button onClick={() => handleExport(response)}>Export</button>
</div>
{response.fields.map((field, idx) => (
<div key={idx} className="form-field">
<div className="field-label">{field.label}</div>
<div className="field-value">{field.value}</div>
</div>
))}
</div>
))}
</div>
{filteredResponses.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>
No form responses found
</div>
)}
</div>
);
}
ReactDOM.render(<FormResponses />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,122 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Label Manager</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 900px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.toolbar { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.labels-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 15px; }
.label-card { background: white; padding: 20px; border-radius: 8px; border-left: 4px solid; display: flex; justify-content: space-between; align-items: center; }
.label-info { flex: 1; }
.label-name { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 5px; }
.label-usage { font-size: 13px; color: #666; }
.label-actions { display: flex; gap: 8px; }
.btn-icon { padding: 6px 10px; background: #E5E7EB; color: #374151; font-size: 12px; }
.btn-icon:hover { background: #D1D5DB; }
.btn-danger { background: #DC2626; }
.btn-danger:hover { background: #B91C1C; }
.color-dot { width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 8px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function LabelManager() {
const [labels, setLabels] = useState([]);
useEffect(() => {
fetchLabels();
}, []);
const fetchLabels = async () => {
// Mock data
const mockLabels = [
{ id: 1, name: 'VIP Client', color: '#4F46E5', usageCount: 12 },
{ id: 2, name: 'Follow-up Needed', color: '#DC2626', usageCount: 8 },
{ id: 3, name: 'First Visit', color: '#10B981', usageCount: 24 },
{ id: 4, name: 'Payment Pending', color: '#F59E0B', usageCount: 5 },
{ id: 5, name: 'Special Accommodations', color: '#8B5CF6', usageCount: 3 },
{ id: 6, name: 'Referral', color: '#06B6D4', usageCount: 15 },
];
setLabels(mockLabels);
};
const handleAddLabel = () => {
const name = prompt('Enter label name:');
if (name) {
alert(\`Create label: \${name}\`);
}
};
const handleEditLabel = (label) => {
const newName = prompt('Edit label name:', label.name);
if (newName) {
alert(\`Update label to: \${newName}\`);
}
};
const handleDeleteLabel = (label) => {
if (confirm(\`Delete label "\${label.name}"?\`)) {
setLabels(labels.filter(l => l.id !== label.id));
}
};
return (
<div className="container">
<h1>Label Manager</h1>
<div className="toolbar">
<div style={{ color: '#666' }}>
{labels.length} label{labels.length !== 1 ? 's' : ''} {labels.reduce((sum, l) => sum + l.usageCount, 0)} total uses
</div>
<button onClick={handleAddLabel}>+ Create Label</button>
</div>
<div className="labels-grid">
{labels.map(label => (
<div key={label.id} className="label-card" style={{ borderLeftColor: label.color }}>
<div className="label-info">
<div className="label-name">
<span className="color-dot" style={{ backgroundColor: label.color }}></span>
{label.name}
</div>
<div className="label-usage">
Used {label.usageCount} time{label.usageCount !== 1 ? 's' : ''}
</div>
</div>
<div className="label-actions">
<button className="btn-icon" onClick={() => handleEditLabel(label)}>
Edit
</button>
<button className="btn-icon btn-danger" onClick={() => handleDeleteLabel(label)}>
Delete
</button>
</div>
</div>
))}
</div>
{labels.length === 0 && (
<div style={{ textAlign: 'center', padding: '40px', color: '#666', background: 'white', borderRadius: '8px' }}>
No labels yet. Create your first label to get started.
</div>
)}
</div>
);
}
ReactDOM.render(<LabelManager />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,121 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Catalog</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.tabs { background: white; padding: 20px 20px 0 20px; border-radius: 8px 8px 0 0; display: flex; gap: 20px; }
.tab { padding: 10px 20px; cursor: pointer; border-bottom: 3px solid transparent; font-weight: 500; color: #666; }
.tab.active { color: #4F46E5; border-bottom-color: #4F46E5; }
.products-grid { background: white; padding: 20px; border-radius: 0 0 8px 8px; display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.product-card { border: 1px solid #E5E7EB; border-radius: 8px; padding: 20px; transition: all 0.2s; cursor: pointer; }
.product-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
.product-name { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 10px; }
.product-description { font-size: 14px; color: #666; margin-bottom: 15px; line-height: 1.5; }
.product-price { font-size: 24px; font-weight: 700; color: #4F46E5; }
.product-category { display: inline-block; padding: 4px 8px; background: #F3F4F6; color: #6B7280; border-radius: 4px; font-size: 12px; margin-bottom: 10px; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ProductCatalog() {
const [activeTab, setActiveTab] = useState('addons');
const [products, setProducts] = useState({
addons: [],
packages: [],
subscriptions: [],
certificates: []
});
useEffect(() => {
fetchProducts();
}, []);
const fetchProducts = async () => {
// Mock data
const mockProducts = {
addons: [
{ id: 1, name: 'Extended Session', description: 'Add 30 minutes to your appointment', price: '$50', category: 'Time Extension' },
{ id: 2, name: 'Treatment Add-on', description: 'Additional specialized treatment', price: '$75', category: 'Treatment' },
{ id: 3, name: 'Product Bundle', description: 'Take-home care products', price: '$120', category: 'Products' },
],
packages: [
{ id: 4, name: '5-Session Package', description: 'Save 10% with 5 sessions', price: '$675', category: 'Multi-session' },
{ id: 5, name: '10-Session Package', description: 'Save 15% with 10 sessions', price: '$1,275', category: 'Multi-session' },
{ id: 6, name: 'VIP Package', description: 'Premium care with priority booking', price: '$2,500', category: 'Premium' },
],
subscriptions: [
{ id: 7, name: 'Monthly Membership', description: 'Unlimited access for one month', price: '$199/mo', category: 'Membership' },
{ id: 8, name: 'Quarterly Plan', description: 'Best value - 3 months', price: '$499/quarter', category: 'Membership' },
],
certificates: [
{ id: 9, name: 'Gift Certificate - $100', description: 'Perfect for any occasion', price: '$100', category: 'Gift' },
{ id: 10, name: 'Gift Certificate - $250', description: 'Premium gift option', price: '$250', category: 'Gift' },
]
};
setProducts(mockProducts);
};
const handleProductClick = (product) => {
alert(\`Product details: \${product.name}\`);
};
return (
<div className="container">
<h1>Product Catalog</h1>
<div className="tabs">
<div
className={\`tab \${activeTab === 'addons' ? 'active' : ''}\`}
onClick={() => setActiveTab('addons')}
>
Add-ons ({products.addons.length})
</div>
<div
className={\`tab \${activeTab === 'packages' ? 'active' : ''}\`}
onClick={() => setActiveTab('packages')}
>
Packages ({products.packages.length})
</div>
<div
className={\`tab \${activeTab === 'subscriptions' ? 'active' : ''}\`}
onClick={() => setActiveTab('subscriptions')}
>
Subscriptions ({products.subscriptions.length})
</div>
<div
className={\`tab \${activeTab === 'certificates' ? 'active' : ''}\`}
onClick={() => setActiveTab('certificates')}
>
Gift Certificates ({products.certificates.length})
</div>
</div>
<div className="products-grid">
{products[activeTab].map(product => (
<div key={product.id} className="product-card" onClick={() => handleProductClick(product)}>
<div className="product-category">{product.category}</div>
<div className="product-name">{product.name}</div>
<div className="product-description">{product.description}</div>
<div className="product-price">{product.price}</div>
</div>
))}
</div>
</div>
);
}
ReactDOM.render(<ProductCatalog />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -0,0 +1,171 @@
export default `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Schedule Overview</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>
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
.container { max-width: 1400px; margin: 0 auto; }
h1 { color: #333; margin-bottom: 20px; }
.controls { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; }
.date-nav { display: flex; gap: 10px; align-items: center; }
button { padding: 10px 20px; background: #4F46E5; color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 14px; font-weight: 500; }
button:hover { background: #4338CA; }
.btn-secondary { background: #6B7280; }
.btn-secondary:hover { background: #4B5563; }
.schedule-grid { background: white; border-radius: 8px; overflow: hidden; }
.calendar-header { display: grid; grid-template-columns: 100px repeat(7, 1fr); background: #F9FAFB; border-bottom: 2px solid #E5E7EB; }
.header-cell { padding: 15px; text-align: center; font-weight: 600; color: #374151; }
.time-row { display: grid; grid-template-columns: 100px repeat(7, 1fr); border-bottom: 1px solid #E5E7EB; min-height: 60px; }
.time-cell { padding: 10px; color: #666; font-size: 14px; background: #F9FAFB; border-right: 1px solid #E5E7EB; }
.appointment-cell { padding: 5px; border-right: 1px solid #E5E7EB; position: relative; }
.appointment-block { background: #EEF2FF; border-left: 3px solid #4F46E5; padding: 8px; margin: 2px; border-radius: 4px; cursor: pointer; font-size: 13px; }
.appointment-block:hover { background: #E0E7FF; }
.appointment-time { font-weight: 600; color: #4F46E5; }
.appointment-client { color: #333; margin-top: 2px; }
.appointment-type { color: #666; font-size: 11px; }
.stats-bar { display: flex; gap: 20px; padding: 15px 20px; background: #F9FAFB; border-top: 1px solid #E5E7EB; }
.stat { font-size: 13px; color: #666; }
.stat strong { color: #4F46E5; font-weight: 600; }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
function ScheduleOverview() {
const [currentWeek, setCurrentWeek] = useState(new Date());
const [appointments, setAppointments] = useState([]);
const [calendars, setCalendars] = useState([]);
useEffect(() => {
fetchScheduleData();
}, [currentWeek]);
const fetchScheduleData = async () => {
// Mock data
const mockCalendars = ['Dr. Smith', 'Dr. Johnson', 'Dr. Davis'];
const mockAppointments = [
{ id: 1, calendar: 'Dr. Smith', day: 1, time: '09:00', client: 'John Doe', type: 'Consultation' },
{ id: 2, calendar: 'Dr. Smith', day: 1, time: '10:30', client: 'Jane Smith', type: 'Follow-up' },
{ id: 3, calendar: 'Dr. Johnson', day: 1, time: '09:00', client: 'Bob Wilson', type: 'Assessment' },
{ id: 4, calendar: 'Dr. Smith', day: 2, time: '14:00', client: 'Alice Brown', type: 'Consultation' },
{ id: 5, calendar: 'Dr. Davis', day: 3, time: '11:00', client: 'Charlie Davis', type: 'Follow-up' },
];
setCalendars(mockCalendars);
setAppointments(mockAppointments);
};
const getDaysOfWeek = () => {
const days = [];
const startOfWeek = new Date(currentWeek);
startOfWeek.setDate(currentWeek.getDate() - currentWeek.getDay());
for (let i = 0; i < 7; i++) {
const day = new Date(startOfWeek);
day.setDate(startOfWeek.getDate() + i);
days.push(day);
}
return days;
};
const timeSlots = ['09:00', '10:00', '11:00', '12:00', '13:00', '14:00', '15:00', '16:00', '17:00'];
const getAppointmentsForSlot = (day, time) => {
return appointments.filter(apt =>
apt.day === day && apt.time === time
);
};
const handlePrevWeek = () => {
const newDate = new Date(currentWeek);
newDate.setDate(currentWeek.getDate() - 7);
setCurrentWeek(newDate);
};
const handleNextWeek = () => {
const newDate = new Date(currentWeek);
newDate.setDate(currentWeek.getDate() + 7);
setCurrentWeek(newDate);
};
const handleToday = () => {
setCurrentWeek(new Date());
};
const daysOfWeek = getDaysOfWeek();
const totalAppointments = appointments.length;
return (
<div className="container">
<h1>Schedule Overview</h1>
<div className="controls">
<div className="date-nav">
<button className="btn-secondary" onClick={handlePrevWeek}> Previous</button>
<button className="btn-secondary" onClick={handleToday}>Today</button>
<button className="btn-secondary" onClick={handleNextWeek}>Next </button>
<span style={{ marginLeft: '20px', fontSize: '16px', fontWeight: 600 }}>
Week of {daysOfWeek[0].toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })}
</span>
</div>
<select style={{ padding: '8px', border: '1px solid #ddd', borderRadius: '4px' }}>
<option>All Calendars</option>
{calendars.map(cal => <option key={cal}>{cal}</option>)}
</select>
</div>
<div className="schedule-grid">
<div className="calendar-header">
<div className="header-cell">Time</div>
{daysOfWeek.map((day, idx) => (
<div key={idx} className="header-cell">
{day.toLocaleDateString('en-US', { weekday: 'short', month: 'numeric', day: 'numeric' })}
</div>
))}
</div>
{timeSlots.map(time => (
<div key={time} className="time-row">
<div className="time-cell">{time}</div>
{daysOfWeek.map((_, dayIdx) => {
const apts = getAppointmentsForSlot(dayIdx, time);
return (
<div key={dayIdx} className="appointment-cell">
{apts.map(apt => (
<div key={apt.id} className="appointment-block">
<div className="appointment-time">{apt.time}</div>
<div className="appointment-client">{apt.client}</div>
<div className="appointment-type">{apt.type}</div>
</div>
))}
</div>
);
})}
</div>
))}
<div className="stats-bar">
<div className="stat">
<strong>{totalAppointments}</strong> appointments this week
</div>
<div className="stat">
<strong>{calendars.length}</strong> active calendars
</div>
</div>
</div>
</div>
);
}
ReactDOM.render(<ScheduleOverview />, document.getElementById('root'));
</script>
</body>
</html>`;

View File

@ -1,14 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"declaration": true
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]