Factory V2 complete: all 30 MCP servers with tools + apps — final cleanup

This commit is contained in:
Jake Shore 2026-02-12 23:59:18 -05:00
parent 4c546d654a
commit 14f651082c
83 changed files with 18979 additions and 1194 deletions

View File

@ -0,0 +1,789 @@
/**
* Brevo API Client
* API v3: https://api.brevo.com/v3/
*/
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
BrevoConfig,
Contact,
CreateContactParams,
UpdateContactParams,
ContactList,
EmailCampaign,
CreateEmailCampaignParams,
SendSmtpEmail,
SendSmtpEmailResponse,
SendTransacSms,
SendTransacSmsResponse,
SmsCampaign,
CreateSmsCampaignParams,
EmailTemplate,
CreateEmailTemplateParams,
Sender,
CreateSenderParams,
Automation,
Deal,
CreateDealParams,
Company,
CreateCompanyParams,
Task,
CreateTaskParams,
Webhook,
CreateWebhookParams,
AccountInfo,
EmailEventReport,
Folder,
CreateFolderParams,
Process,
CreateDoiContactParams,
PaginationParams,
DateRangeParams,
} from './types/index.js';
export class BrevoAPIClient {
private client: AxiosInstance;
constructor(config: BrevoConfig) {
this.client = axios.create({
baseURL: config.baseUrl || 'https://api.brevo.com/v3',
headers: {
'api-key': config.apiKey,
'Content-Type': 'application/json',
'Accept': 'application/json',
},
});
// Error interceptor
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
const errorMessage = error.response?.data
? JSON.stringify(error.response.data)
: error.message;
throw new Error(`Brevo API Error: ${errorMessage}`);
}
);
}
// ============================================================================
// CONTACTS
// ============================================================================
async getContact(identifier: string): Promise<Contact> {
const { data } = await this.client.get(`/contacts/${encodeURIComponent(identifier)}`);
return data;
}
async createContact(params: CreateContactParams): Promise<{ id: number }> {
const { data } = await this.client.post('/contacts', params);
return data;
}
async updateContact(identifier: string, params: UpdateContactParams): Promise<void> {
await this.client.put(`/contacts/${encodeURIComponent(identifier)}`, params);
}
async deleteContact(identifier: string): Promise<void> {
await this.client.delete(`/contacts/${encodeURIComponent(identifier)}`);
}
async getContacts(params?: PaginationParams & { modifiedSince?: string; listIds?: number[]; sort?: string }): Promise<{ contacts: Contact[]; count: number }> {
const { data } = await this.client.get('/contacts', { params });
return data;
}
async importContacts(params: {
fileBody?: string;
fileUrl?: string;
listIds?: number[];
notifyUrl?: string;
newList?: { listName: string; folderId?: number };
emailBlacklist?: boolean;
smsBlacklist?: boolean;
updateExistingContacts?: boolean;
emptyContactsAttributes?: boolean;
}): Promise<{ processId: number }> {
const { data } = await this.client.post('/contacts/import', params);
return data;
}
async exportContacts(params: {
exportAttributes?: string[];
filter?: Record<string, any>;
notifyUrl?: string;
}): Promise<{ exportId: string }> {
const { data } = await this.client.post('/contacts/export', params);
return data;
}
async addContactToList(listId: number, contactEmails: string[]): Promise<{ contacts: { success: string[]; failure: string[] } }> {
const { data } = await this.client.post(`/contacts/lists/${listId}/contacts/add`, { emails: contactEmails });
return data;
}
async removeContactFromList(listId: number, contactEmails: string[]): Promise<{ contacts: { success: string[]; failure: string[] } }> {
const { data } = await this.client.post(`/contacts/lists/${listId}/contacts/remove`, { emails: contactEmails });
return data;
}
async getContactStats(identifier: string): Promise<any> {
const { data } = await this.client.get(`/contacts/${encodeURIComponent(identifier)}/campaignStats`);
return data;
}
async getContactAttributes(): Promise<{ attributes: any[] }> {
const { data } = await this.client.get('/contacts/attributes');
return data;
}
async createAttribute(params: {
value?: string;
enumeration?: { value: number; label: string }[];
type?: 'text' | 'date' | 'float' | 'boolean' | 'id';
isRecurring?: boolean;
category?: 'normal' | 'transactional' | 'category' | 'calculated' | 'global';
}): Promise<void> {
await this.client.post('/contacts/attributes/normal', params);
}
async createDoiContact(params: CreateDoiContactParams): Promise<void> {
await this.client.post('/contacts/doubleOptinConfirmation', params);
}
// ============================================================================
// LISTS
// ============================================================================
async getLists(params?: PaginationParams & { sort?: string }): Promise<{ lists: ContactList[]; count: number }> {
const { data } = await this.client.get('/contacts/lists', { params });
return data;
}
async getList(listId: number): Promise<ContactList> {
const { data } = await this.client.get(`/contacts/lists/${listId}`);
return data;
}
async createList(params: { name: string; folderId?: number }): Promise<{ id: number }> {
const { data } = await this.client.post('/contacts/lists', params);
return data;
}
async updateList(listId: number, params: { name?: string; folderId?: number }): Promise<void> {
await this.client.put(`/contacts/lists/${listId}`, params);
}
async deleteList(listId: number): Promise<void> {
await this.client.delete(`/contacts/lists/${listId}`);
}
async getContactsFromList(listId: number, params?: PaginationParams & { modifiedSince?: string; sort?: string }): Promise<{ contacts: Contact[]; count: number }> {
const { data } = await this.client.get(`/contacts/lists/${listId}/contacts`, { params });
return data;
}
// ============================================================================
// FOLDERS
// ============================================================================
async getFolders(params?: { limit?: number; offset?: number }): Promise<{ folders: Folder[]; count: number }> {
const { data } = await this.client.get('/contacts/folders', { params });
return data;
}
async getFolder(folderId: number): Promise<Folder> {
const { data } = await this.client.get(`/contacts/folders/${folderId}`);
return data;
}
async createFolder(params: CreateFolderParams): Promise<{ id: number }> {
const { data } = await this.client.post('/contacts/folders', params);
return data;
}
async updateFolder(folderId: number, params: { name: string }): Promise<void> {
await this.client.put(`/contacts/folders/${folderId}`, params);
}
async deleteFolder(folderId: number): Promise<void> {
await this.client.delete(`/contacts/folders/${folderId}`);
}
async getListsInFolder(folderId: number, params?: PaginationParams & { sort?: string }): Promise<{ lists: ContactList[]; count: number }> {
const { data } = await this.client.get(`/contacts/folders/${folderId}/lists`, { params });
return data;
}
// ============================================================================
// EMAIL CAMPAIGNS
// ============================================================================
async getEmailCampaigns(params?: {
type?: 'classic' | 'trigger';
status?: 'draft' | 'sent' | 'queued' | 'suspended' | 'archive';
limit?: number;
offset?: number;
sort?: string;
excludeHtmlContent?: boolean;
}): Promise<{ campaigns: EmailCampaign[]; count: number }> {
const { data } = await this.client.get('/emailCampaigns', { params });
return data;
}
async getEmailCampaign(campaignId: number): Promise<EmailCampaign> {
const { data } = await this.client.get(`/emailCampaigns/${campaignId}`);
return data;
}
async createEmailCampaign(params: CreateEmailCampaignParams): Promise<{ id: number }> {
const { data } = await this.client.post('/emailCampaigns', params);
return data;
}
async updateEmailCampaign(campaignId: number, params: Partial<CreateEmailCampaignParams>): Promise<void> {
await this.client.put(`/emailCampaigns/${campaignId}`, params);
}
async deleteEmailCampaign(campaignId: number): Promise<void> {
await this.client.delete(`/emailCampaigns/${campaignId}`);
}
async sendEmailCampaignNow(campaignId: number): Promise<void> {
await this.client.post(`/emailCampaigns/${campaignId}/sendNow`);
}
async sendTestEmail(campaignId: number, emailTo: string[]): Promise<void> {
await this.client.post(`/emailCampaigns/${campaignId}/sendTest`, { emailTo });
}
async updateEmailCampaignStatus(campaignId: number, status: 'draft' | 'suspended' | 'archive'): Promise<void> {
await this.client.put(`/emailCampaigns/${campaignId}/status`, { status });
}
async getEmailCampaignReport(campaignId: number, params?: DateRangeParams): Promise<any> {
const { data } = await this.client.get(`/emailCampaigns/${campaignId}/report`, { params });
return data;
}
async exportEmailCampaignRecipients(campaignId: number, params?: {
notifyURL?: string;
recipientsType?: 'all' | 'nonClickers' | 'nonOpeners' | 'clickers' | 'openers' | 'softBounces' | 'hardBounces' | 'unsubscribed';
}): Promise<{ exportId: string }> {
const { data } = await this.client.post(`/emailCampaigns/${campaignId}/exportRecipients`, params);
return data;
}
// ============================================================================
// TRANSACTIONAL EMAILS (SMTP)
// ============================================================================
async sendTransacEmail(params: SendSmtpEmail): Promise<SendSmtpEmailResponse> {
const { data } = await this.client.post('/smtp/email', params);
return data;
}
async getTransacEmailContent(uuid: string): Promise<any> {
const { data } = await this.client.get(`/smtp/emails/${uuid}`);
return data;
}
async deleteScheduledEmail(identifier: string): Promise<void> {
await this.client.delete(`/smtp/email/${identifier}`);
}
async getTransacEmailEvents(params?: {
limit?: number;
offset?: number;
email?: string;
templateId?: number;
messageId?: string;
tags?: string;
event?: string;
days?: number;
sort?: string;
} & DateRangeParams): Promise<EmailEventReport> {
const { data } = await this.client.get('/smtp/statistics/events', { params });
return data;
}
async getTransacEmailStats(params?: DateRangeParams & { days?: number; tag?: string }): Promise<any> {
const { data } = await this.client.get('/smtp/statistics/aggregatedReport', { params });
return data;
}
async getTransacEmailReports(params?: DateRangeParams & { days?: number; tag?: string; sort?: string }): Promise<any> {
const { data } = await this.client.get('/smtp/statistics/reports', { params });
return data;
}
async getBlockedDomains(): Promise<{ domains: string[] }> {
const { data } = await this.client.get('/smtp/blockedDomains');
return data;
}
async blockDomain(domain: string): Promise<void> {
await this.client.post('/smtp/blockedDomains', { domain });
}
async unblockDomain(domain: string): Promise<void> {
await this.client.delete(`/smtp/blockedDomains/${domain}`);
}
async getBlockedEmails(): Promise<{ emails: any[] }> {
const { data } = await this.client.get('/smtp/blockedContacts');
return data;
}
async blockEmail(params: { emails: string[] }): Promise<void> {
await this.client.post('/smtp/blockedContacts', params);
}
async unblockEmail(email: string): Promise<void> {
await this.client.delete(`/smtp/blockedContacts/${email}`);
}
async getEmailActivityLog(messageId: string): Promise<any> {
const { data } = await this.client.get(`/smtp/emails/${messageId}/activity`);
return data;
}
// ============================================================================
// EMAIL TEMPLATES
// ============================================================================
async getEmailTemplates(params?: { templateStatus?: boolean; limit?: number; offset?: number; sort?: string }): Promise<{ templates: EmailTemplate[]; count: number }> {
const { data } = await this.client.get('/smtp/templates', { params });
return data;
}
async getEmailTemplate(templateId: number): Promise<EmailTemplate> {
const { data } = await this.client.get(`/smtp/templates/${templateId}`);
return data;
}
async createEmailTemplate(params: CreateEmailTemplateParams): Promise<{ id: number }> {
const { data } = await this.client.post('/smtp/templates', params);
return data;
}
async updateEmailTemplate(templateId: number, params: Partial<CreateEmailTemplateParams>): Promise<void> {
await this.client.put(`/smtp/templates/${templateId}`, params);
}
async deleteEmailTemplate(templateId: number): Promise<void> {
await this.client.delete(`/smtp/templates/${templateId}`);
}
async sendTestEmailTemplate(templateId: number, params: { emailTo: string[] }): Promise<void> {
await this.client.post(`/smtp/templates/${templateId}/sendTest`, params);
}
// ============================================================================
// SENDERS
// ============================================================================
async getSenders(params?: { ip?: string; domain?: string }): Promise<{ senders: Sender[] }> {
const { data } = await this.client.get('/senders', { params });
return data;
}
async getSender(senderId: number): Promise<Sender> {
const { data } = await this.client.get(`/senders/${senderId}`);
return data;
}
async createSender(params: CreateSenderParams): Promise<{ id: number }> {
const { data } = await this.client.post('/senders', params);
return data;
}
async updateSender(senderId: number, params: { name?: string; email?: string; ips?: { ip: string; domain: string }[] }): Promise<void> {
await this.client.put(`/senders/${senderId}`, params);
}
async deleteSender(senderId: number): Promise<void> {
await this.client.delete(`/senders/${senderId}`);
}
async validateSenderDomain(domain: string): Promise<any> {
const { data } = await this.client.get(`/senders/domains/${domain}/validate`);
return data;
}
async authenticateSenderDomain(domain: string): Promise<any> {
const { data } = await this.client.put(`/senders/domains/${domain}/authenticate`);
return data;
}
// ============================================================================
// SMS
// ============================================================================
async sendTransacSms(params: SendTransacSms): Promise<SendTransacSmsResponse> {
const { data } = await this.client.post('/transactionalSMS/sms', params);
return data;
}
async getTransacSmsEvents(params?: {
limit?: number;
offset?: number;
phoneNumber?: string;
event?: string;
tags?: string;
sort?: string;
days?: number;
} & DateRangeParams): Promise<any> {
const { data } = await this.client.get('/transactionalSMS/statistics/events', { params });
return data;
}
async getTransacSmsReports(params?: DateRangeParams & { days?: number; tag?: string; sort?: string }): Promise<any> {
const { data } = await this.client.get('/transactionalSMS/statistics/reports', { params });
return data;
}
async getTransacSmsStats(params?: DateRangeParams & { days?: number; tag?: string }): Promise<any> {
const { data } = await this.client.get('/transactionalSMS/statistics/aggregatedReport', { params });
return data;
}
async getSmsCampaigns(params?: {
status?: 'draft' | 'sent' | 'queued' | 'suspended' | 'inProgress';
limit?: number;
offset?: number;
sort?: string;
} & DateRangeParams): Promise<{ campaigns: SmsCampaign[]; count: number }> {
const { data } = await this.client.get('/smsCampaigns', { params });
return data;
}
async getSmsCampaign(campaignId: number): Promise<SmsCampaign> {
const { data } = await this.client.get(`/smsCampaigns/${campaignId}`);
return data;
}
async createSmsCampaign(params: CreateSmsCampaignParams): Promise<{ id: number }> {
const { data } = await this.client.post('/smsCampaigns', params);
return data;
}
async updateSmsCampaign(campaignId: number, params: Partial<CreateSmsCampaignParams>): Promise<void> {
await this.client.put(`/smsCampaigns/${campaignId}`, params);
}
async deleteSmsCampaign(campaignId: number): Promise<void> {
await this.client.delete(`/smsCampaigns/${campaignId}`);
}
async sendSmsCampaignNow(campaignId: number): Promise<void> {
await this.client.post(`/smsCampaigns/${campaignId}/sendNow`);
}
async sendTestSms(campaignId: number, phoneNumber: string): Promise<void> {
await this.client.post(`/smsCampaigns/${campaignId}/sendTest`, { phoneNumber });
}
async updateSmsCampaignStatus(campaignId: number, status: 'draft' | 'suspended' | 'archive'): Promise<void> {
await this.client.put(`/smsCampaigns/${campaignId}/status`, { status });
}
async requestSmsSender(senderName: string): Promise<void> {
await this.client.post('/senders/sms', { senderName });
}
async getSmsSenders(params?: { limit?: number; offset?: number }): Promise<{ senders: any[] }> {
const { data } = await this.client.get('/senders/sms', { params });
return data;
}
// ============================================================================
// AUTOMATIONS (WORKFLOWS)
// ============================================================================
async getAutomations(params?: { limit?: number; offset?: number; sort?: string }): Promise<{ automations: Automation[] }> {
const { data } = await this.client.get('/automation/workflows', { params });
return data;
}
async getAutomation(workflowId: number): Promise<Automation> {
const { data } = await this.client.get(`/automation/workflows/${workflowId}`);
return data;
}
async deleteAutomation(workflowId: number): Promise<void> {
await this.client.delete(`/automation/workflows/${workflowId}`);
}
// ============================================================================
// CRM - DEALS
// ============================================================================
async getDeals(params?: {
'filters[attributes.deal_name]'?: string;
'filters[attributes.deal_stage]'?: string;
'filters[linkedContactsIds]'?: number;
'filters[linkedCompaniesIds]'?: string;
limit?: number;
offset?: number;
sort?: string;
}): Promise<{ items: Deal[] }> {
const { data } = await this.client.get('/crm/deals', { params });
return data;
}
async getDeal(dealId: string): Promise<Deal> {
const { data } = await this.client.get(`/crm/deals/${dealId}`);
return data;
}
async createDeal(params: CreateDealParams): Promise<{ id: string }> {
const { data } = await this.client.post('/crm/deals', params);
return data;
}
async updateDeal(dealId: string, params: Partial<CreateDealParams>): Promise<void> {
await this.client.patch(`/crm/deals/${dealId}`, params);
}
async deleteDeal(dealId: string): Promise<void> {
await this.client.delete(`/crm/deals/${dealId}`);
}
async linkDealWithContact(dealId: string, contactId: number): Promise<void> {
await this.client.patch(`/crm/deals/link-unlink/${dealId}`, {
linkContactIds: [contactId],
});
}
async unlinkDealFromContact(dealId: string, contactId: number): Promise<void> {
await this.client.patch(`/crm/deals/link-unlink/${dealId}`, {
unlinkContactIds: [contactId],
});
}
async getDealsPipeline(): Promise<any> {
const { data } = await this.client.get('/crm/pipeline/details');
return data;
}
async getDealAttributes(): Promise<{ attributes: any[] }> {
const { data } = await this.client.get('/crm/deals/attributes');
return data;
}
// ============================================================================
// CRM - COMPANIES
// ============================================================================
async getCompanies(params?: {
'filters[attributes.name]'?: string;
'filters[linkedContactsIds]'?: number;
'filters[linkedDealsIds]'?: string;
limit?: number;
offset?: number;
sort?: string;
}): Promise<{ items: Company[] }> {
const { data } = await this.client.get('/crm/companies', { params });
return data;
}
async getCompany(companyId: string): Promise<Company> {
const { data } = await this.client.get(`/crm/companies/${companyId}`);
return data;
}
async createCompany(params: CreateCompanyParams): Promise<{ id: string }> {
const { data } = await this.client.post('/crm/companies', params);
return data;
}
async updateCompany(companyId: string, params: Partial<CreateCompanyParams>): Promise<void> {
await this.client.patch(`/crm/companies/${companyId}`, params);
}
async deleteCompany(companyId: string): Promise<void> {
await this.client.delete(`/crm/companies/${companyId}`);
}
async linkCompanyWithContact(companyId: string, contactId: number): Promise<void> {
await this.client.patch(`/crm/companies/link-unlink/${companyId}`, {
linkContactIds: [contactId],
});
}
async unlinkCompanyFromContact(companyId: string, contactId: number): Promise<void> {
await this.client.patch(`/crm/companies/link-unlink/${companyId}`, {
unlinkContactIds: [contactId],
});
}
async getCompanyAttributes(): Promise<{ attributes: any[] }> {
const { data } = await this.client.get('/crm/companies/attributes');
return data;
}
// ============================================================================
// CRM - TASKS
// ============================================================================
async getTasks(params?: {
'filterType'?: 'contacts' | 'companies' | 'deals';
'filterId'?: string;
'dateFrom'?: string;
'dateTo'?: string;
limit?: number;
offset?: number;
sort?: string;
}): Promise<{ items: Task[] }> {
const { data } = await this.client.get('/crm/tasks', { params });
return data;
}
async getTask(taskId: string): Promise<Task> {
const { data } = await this.client.get(`/crm/tasks/${taskId}`);
return data;
}
async createTask(params: CreateTaskParams): Promise<{ id: string }> {
const { data } = await this.client.post('/crm/tasks', params);
return data;
}
async updateTask(taskId: string, params: Partial<CreateTaskParams>): Promise<void> {
await this.client.patch(`/crm/tasks/${taskId}`, params);
}
async deleteTask(taskId: string): Promise<void> {
await this.client.delete(`/crm/tasks/${taskId}`);
}
async getTaskTypes(): Promise<{ taskTypes: any[] }> {
const { data } = await this.client.get('/crm/tasktypes');
return data;
}
// ============================================================================
// CRM - NOTES
// ============================================================================
async getNotes(params?: {
entity?: 'companies' | 'deals' | 'contacts';
entityIds?: string;
dateFrom?: number;
dateTo?: number;
limit?: number;
offset?: number;
sort?: string;
}): Promise<{ items: any[] }> {
const { data } = await this.client.get('/crm/notes', { params });
return data;
}
async getNote(noteId: string): Promise<any> {
const { data } = await this.client.get(`/crm/notes/${noteId}`);
return data;
}
async createNote(params: {
text: string;
contactIds?: number[];
dealIds?: string[];
companyIds?: string[];
}): Promise<{ id: string }> {
const { data } = await this.client.post('/crm/notes', params);
return data;
}
async updateNote(noteId: string, params: { text: string }): Promise<void> {
await this.client.patch(`/crm/notes/${noteId}`, params);
}
async deleteNote(noteId: string): Promise<void> {
await this.client.delete(`/crm/notes/${noteId}`);
}
// ============================================================================
// WEBHOOKS
// ============================================================================
async getWebhooks(params?: { type?: 'marketing' | 'transactional' | 'inbound'; sort?: string }): Promise<{ webhooks: Webhook[] }> {
const { data } = await this.client.get('/webhooks', { params });
return data;
}
async getWebhook(webhookId: number): Promise<Webhook> {
const { data } = await this.client.get(`/webhooks/${webhookId}`);
return data;
}
async createWebhook(params: CreateWebhookParams): Promise<{ id: number }> {
const { data } = await this.client.post('/webhooks', params);
return data;
}
async updateWebhook(webhookId: number, params: Partial<CreateWebhookParams>): Promise<void> {
await this.client.put(`/webhooks/${webhookId}`, params);
}
async deleteWebhook(webhookId: number): Promise<void> {
await this.client.delete(`/webhooks/${webhookId}`);
}
async exportWebhookEvents(webhookId: number, params?: {
startDate?: string;
endDate?: string;
days?: number;
messageId?: string;
email?: string;
event?: string;
notifyURL?: string;
}): Promise<{ exportId: string }> {
const { data } = await this.client.post(`/webhooks/${webhookId}/export`, params);
return data;
}
// ============================================================================
// ACCOUNT
// ============================================================================
async getAccount(): Promise<AccountInfo> {
const { data } = await this.client.get('/account');
return data;
}
// ============================================================================
// PROCESSES
// ============================================================================
async getProcess(processId: number): Promise<Process> {
const { data } = await this.client.get(`/processes/${processId}`);
return data;
}
async getProcesses(params?: { limit?: number; offset?: number; sort?: string }): Promise<{ processes: Process[] }> {
const { data } = await this.client.get('/processes', { params });
return data;
}
// ============================================================================
// INBOUND PARSING
// ============================================================================
async getInboundEmailEvents(params?: {
sender?: string;
startDate?: string;
endDate?: string;
limit?: number;
offset?: number;
sort?: string;
}): Promise<{ events: any[] }> {
const { data } = await this.client.get('/inbound/events', { params });
return data;
}
async getInboundEmailAttachment(downloadToken: string): Promise<any> {
const { data } = await this.client.get(`/inbound/attachments/${downloadToken}`);
return data;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,779 @@
/**
* Close CRM API Client
* Full implementation for Close API v1 with Basic Auth
*/
import axios, { AxiosInstance, AxiosError } from 'axios';
import type {
CloseConfig,
Lead,
Contact,
Opportunity,
Activity,
Task,
Pipeline,
SmartView,
User,
CustomField,
Sequence,
SequenceSubscription,
PaginatedResponse,
SearchParams,
LeadSearchParams,
OpportunitySearchParams,
} from '../types.js';
export class CloseAPIClient {
private client: AxiosInstance;
private apiKey: string;
constructor(config: CloseConfig) {
this.apiKey = config.apiKey;
this.client = axios.create({
baseURL: config.baseUrl || 'https://api.close.com/api/v1',
headers: {
'Content-Type': 'application/json',
},
auth: {
username: config.apiKey,
password: '', // Close uses API key as username with empty password
},
});
}
private handleError(error: any): never {
if (axios.isAxiosError(error)) {
const axiosError = error as AxiosError;
if (axiosError.response) {
throw new Error(
`Close API Error: ${axiosError.response.status} - ${
JSON.stringify(axiosError.response.data) || axiosError.message
}`
);
}
throw new Error(`Close API Request Failed: ${axiosError.message}`);
}
throw error;
}
// ============================================================================
// LEADS
// ============================================================================
async getLeads(params?: LeadSearchParams): Promise<PaginatedResponse<Lead>> {
try {
const response = await this.client.get('/lead/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getLead(id: string): Promise<Lead> {
try {
const response = await this.client.get(`/lead/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createLead(data: Partial<Lead>): Promise<Lead> {
try {
const response = await this.client.post('/lead/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateLead(id: string, data: Partial<Lead>): Promise<Lead> {
try {
const response = await this.client.put(`/lead/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteLead(id: string): Promise<void> {
try {
await this.client.delete(`/lead/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
async searchLeads(query: string, params?: SearchParams): Promise<PaginatedResponse<Lead>> {
try {
const response = await this.client.get('/lead/', {
params: { query, ...params },
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async mergeLeads(source: string, destination: string): Promise<Lead> {
try {
const response = await this.client.post('/lead/merge/', {
source,
destination,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// CONTACTS
// ============================================================================
async getContacts(params?: SearchParams): Promise<PaginatedResponse<Contact>> {
try {
const response = await this.client.get('/contact/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getContact(id: string): Promise<Contact> {
try {
const response = await this.client.get(`/contact/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createContact(data: Partial<Contact>): Promise<Contact> {
try {
const response = await this.client.post('/contact/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateContact(id: string, data: Partial<Contact>): Promise<Contact> {
try {
const response = await this.client.put(`/contact/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteContact(id: string): Promise<void> {
try {
await this.client.delete(`/contact/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// OPPORTUNITIES
// ============================================================================
async getOpportunities(params?: OpportunitySearchParams): Promise<PaginatedResponse<Opportunity>> {
try {
const response = await this.client.get('/opportunity/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getOpportunity(id: string): Promise<Opportunity> {
try {
const response = await this.client.get(`/opportunity/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createOpportunity(data: Partial<Opportunity>): Promise<Opportunity> {
try {
const response = await this.client.post('/opportunity/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateOpportunity(id: string, data: Partial<Opportunity>): Promise<Opportunity> {
try {
const response = await this.client.put(`/opportunity/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteOpportunity(id: string): Promise<void> {
try {
await this.client.delete(`/opportunity/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// ACTIVITIES
// ============================================================================
async getActivities(params?: SearchParams & { lead_id?: string }): Promise<PaginatedResponse<Activity>> {
try {
const response = await this.client.get('/activity/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getActivity(id: string): Promise<Activity> {
try {
const response = await this.client.get(`/activity/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// Email Activities
async createEmail(data: any): Promise<Activity> {
try {
const response = await this.client.post('/activity/email/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async sendEmail(data: any): Promise<Activity> {
try {
const response = await this.client.post('/activity/email/', {
...data,
status: 'sent',
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// Call Activities
async createCall(data: any): Promise<Activity> {
try {
const response = await this.client.post('/activity/call/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateCall(id: string, data: any): Promise<Activity> {
try {
const response = await this.client.put(`/activity/call/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// Note Activities
async createNote(data: { lead_id?: string; contact_id?: string; note: string }): Promise<Activity> {
try {
const response = await this.client.post('/activity/note/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// SMS Activities
async sendSMS(data: any): Promise<Activity> {
try {
const response = await this.client.post('/activity/sms/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// Meeting Activities
async createMeeting(data: any): Promise<Activity> {
try {
const response = await this.client.post('/activity/meeting/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// TASKS
// ============================================================================
async getTasks(params?: SearchParams & { lead_id?: string; is_complete?: boolean }): Promise<PaginatedResponse<Task>> {
try {
const response = await this.client.get('/task/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getTask(id: string): Promise<Task> {
try {
const response = await this.client.get(`/task/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createTask(data: Partial<Task>): Promise<Task> {
try {
const response = await this.client.post('/task/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateTask(id: string, data: Partial<Task>): Promise<Task> {
try {
const response = await this.client.put(`/task/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteTask(id: string): Promise<void> {
try {
await this.client.delete(`/task/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
async completeTask(id: string): Promise<Task> {
try {
const response = await this.client.put(`/task/${id}/`, {
is_complete: true,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// PIPELINES
// ============================================================================
async getPipelines(): Promise<{ data: Pipeline[] }> {
try {
const response = await this.client.get('/status/opportunity/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getPipeline(id: string): Promise<Pipeline> {
try {
const response = await this.client.get(`/status/opportunity/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getLeadStatuses(): Promise<any> {
try {
const response = await this.client.get('/status/lead/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// SMART VIEWS
// ============================================================================
async getSmartViews(params?: { type?: string }): Promise<PaginatedResponse<SmartView>> {
try {
const response = await this.client.get('/saved_search/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getSmartView(id: string): Promise<SmartView> {
try {
const response = await this.client.get(`/saved_search/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createSmartView(data: Partial<SmartView>): Promise<SmartView> {
try {
const response = await this.client.post('/saved_search/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateSmartView(id: string, data: Partial<SmartView>): Promise<SmartView> {
try {
const response = await this.client.put(`/saved_search/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteSmartView(id: string): Promise<void> {
try {
await this.client.delete(`/saved_search/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// USERS
// ============================================================================
async getUsers(): Promise<{ data: User[] }> {
try {
const response = await this.client.get('/user/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getUser(id: string): Promise<User> {
try {
const response = await this.client.get(`/user/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getCurrentUser(): Promise<User> {
try {
const response = await this.client.get('/me/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// CUSTOM FIELDS
// ============================================================================
async getCustomFields(type?: 'lead' | 'contact' | 'opportunity'): Promise<{ data: CustomField[] }> {
try {
const response = await this.client.get('/custom_fields/', {
params: type ? { type } : undefined,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getCustomField(id: string): Promise<CustomField> {
try {
const response = await this.client.get(`/custom_fields/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createCustomField(data: Partial<CustomField>): Promise<CustomField> {
try {
const response = await this.client.post('/custom_fields/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async updateCustomField(id: string, data: Partial<CustomField>): Promise<CustomField> {
try {
const response = await this.client.put(`/custom_fields/${id}/`, data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteCustomField(id: string): Promise<void> {
try {
await this.client.delete(`/custom_fields/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// SEQUENCES
// ============================================================================
async getSequences(): Promise<{ data: Sequence[] }> {
try {
const response = await this.client.get('/sequence/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getSequence(id: string): Promise<Sequence> {
try {
const response = await this.client.get(`/sequence/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async subscribeToSequence(data: {
sequence_id: string;
contact_id: string;
sender_account_id?: string;
}): Promise<SequenceSubscription> {
try {
const response = await this.client.post('/sequence_subscription/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getSequenceSubscriptions(params?: {
sequence_id?: string;
contact_id?: string;
lead_id?: string;
}): Promise<PaginatedResponse<SequenceSubscription>> {
try {
const response = await this.client.get('/sequence_subscription/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async pauseSequenceSubscription(id: string): Promise<SequenceSubscription> {
try {
const response = await this.client.post(`/sequence_subscription/${id}/pause/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async resumeSequenceSubscription(id: string): Promise<SequenceSubscription> {
try {
const response = await this.client.post(`/sequence_subscription/${id}/resume/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async unsubscribeFromSequence(id: string): Promise<void> {
try {
await this.client.delete(`/sequence_subscription/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// BULK OPERATIONS
// ============================================================================
async bulkDeleteLeads(leadIds: string[]): Promise<any> {
try {
const response = await this.client.post('/lead/bulk_delete/', {
lead_ids: leadIds,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async bulkUpdateLeads(leadIds: string[], data: Partial<Lead>): Promise<any> {
try {
const response = await this.client.post('/lead/bulk_update/', {
lead_ids: leadIds,
...data,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async bulkEmailLeads(leadIds: string[], emailData: any): Promise<any> {
try {
const response = await this.client.post('/lead/bulk_email/', {
lead_ids: leadIds,
...emailData,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async bulkDeleteContacts(contactIds: string[]): Promise<any> {
try {
const response = await this.client.post('/contact/bulk_delete/', {
contact_ids: contactIds,
});
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// REPORTING & ANALYTICS
// ============================================================================
async getLeadsReport(params?: any): Promise<any> {
try {
const response = await this.client.get('/report/lead/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getOpportunitiesReport(params?: any): Promise<any> {
try {
const response = await this.client.get('/report/opportunity/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getActivitiesReport(params?: any): Promise<any> {
try {
const response = await this.client.get('/report/activity/', { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getUserActivityReport(userId: string, params?: any): Promise<any> {
try {
const response = await this.client.get(`/report/user/${userId}/`, { params });
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// ORGANIZATION
// ============================================================================
async getOrganization(): Promise<any> {
try {
const response = await this.client.get('/organization/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// WEBHOOKS
// ============================================================================
async getWebhooks(): Promise<any> {
try {
const response = await this.client.get('/webhook/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async createWebhook(data: { url: string; events: string[] }): Promise<any> {
try {
const response = await this.client.post('/webhook/', data);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async deleteWebhook(id: string): Promise<void> {
try {
await this.client.delete(`/webhook/${id}/`);
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// EMAIL TEMPLATES
// ============================================================================
async getEmailTemplates(): Promise<any> {
try {
const response = await this.client.get('/email_template/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
async getEmailTemplate(id: string): Promise<any> {
try {
const response = await this.client.get(`/email_template/${id}/`);
return response.data;
} catch (error) {
return this.handleError(error);
}
}
// ============================================================================
// CONNECTED ACCOUNTS
// ============================================================================
async getConnectedAccounts(): Promise<any> {
try {
const response = await this.client.get('/connected_account/');
return response.data;
} catch (error) {
return this.handleError(error);
}
}
}

View File

@ -0,0 +1,133 @@
import React, { useState, useEffect } from 'react';
import { Contact, Mail, Phone, Plus, Search } from 'lucide-react';
export const ContactsManager: React.FC = () => {
const [contacts, setContacts] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const loadContacts = async () => {
setLoading(true);
try {
const result = await window.mcp?.callTool('close_list_contacts', { _limit: 100 });
if (result?.content?.[0]?.text) {
const data = JSON.parse(result.content[0].text);
setContacts(data.data || []);
}
} catch (error) {
console.error('Failed to load contacts:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadContacts();
}, []);
const filteredContacts = contacts.filter(c =>
c.name?.toLowerCase().includes(searchQuery.toLowerCase()) ||
c.emails?.some((e: any) => e.email.toLowerCase().includes(searchQuery.toLowerCase()))
);
return (
<div style={{ background: '#1a1a1a', color: '#e0e0e0', minHeight: '100vh', padding: '20px' }}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Contact size={32} color="#8b5cf6" />
<h1 style={{ fontSize: '24px', fontWeight: 'bold', margin: 0 }}>Contacts</h1>
</div>
<button
onClick={() => window.mcp?.showPrompt('Create a new contact')}
style={{
background: '#8b5cf6',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<Plus size={16} />
New Contact
</button>
</div>
<div style={{ marginBottom: '20px' }}>
<div style={{ position: 'relative' }}>
<Search
size={20}
style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: '#666' }}
/>
<input
type="text"
placeholder="Search contacts..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
style={{
width: '100%',
background: '#2a2a2a',
border: '1px solid #3a3a3a',
color: '#e0e0e0',
padding: '12px 12px 12px 44px',
borderRadius: '6px',
}}
/>
</div>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>Loading...</div>
) : (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))', gap: '16px' }}>
{filteredContacts.map((contact) => (
<div
key={contact.id}
onClick={() => window.mcp?.showPrompt(`Show contact ${contact.id}`)}
style={{
background: '#2a2a2a',
border: '1px solid #3a3a3a',
borderRadius: '8px',
padding: '16px',
cursor: 'pointer',
}}
onMouseEnter={(e) => (e.currentTarget.style.borderColor = '#8b5cf6')}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = '#3a3a3a')}
>
<div style={{ fontSize: '16px', fontWeight: '600', marginBottom: '4px' }}>
{contact.name || 'Unnamed Contact'}
</div>
{contact.title && (
<div style={{ fontSize: '13px', color: '#888', marginBottom: '12px' }}>
{contact.title}
</div>
)}
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{contact.emails?.[0] && (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>
<Mail size={14} color="#666" />
<span style={{ color: '#888' }}>{contact.emails[0].email}</span>
</div>
)}
{contact.phones?.[0] && (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', fontSize: '13px' }}>
<Phone size={14} color="#666" />
<span style={{ color: '#888' }}>{contact.phones[0].phone}</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
);
};
export default ContactsManager;

View File

@ -0,0 +1,154 @@
import React, { useState, useEffect } from 'react';
import { Users, Search, Filter, Plus } from 'lucide-react';
export const LeadsDashboard: React.FC = () => {
const [leads, setLeads] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const loadLeads = async () => {
setLoading(true);
try {
const result = await window.mcp?.callTool('close_list_leads', {
query: searchQuery,
_limit: 50,
});
if (result?.content?.[0]?.text) {
const data = JSON.parse(result.content[0].text);
setLeads(data.data || []);
}
} catch (error) {
console.error('Failed to load leads:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
loadLeads();
}, []);
return (
<div style={{ background: '#1a1a1a', color: '#e0e0e0', minHeight: '100vh', padding: '20px' }}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{/* Header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '24px' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
<Users size={32} color="#3b82f6" />
<h1 style={{ fontSize: '24px', fontWeight: 'bold', margin: 0 }}>Leads Dashboard</h1>
</div>
<button
onClick={() => window.mcp?.showPrompt('Create a new lead')}
style={{
background: '#3b82f6',
color: 'white',
border: 'none',
padding: '10px 20px',
borderRadius: '6px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
}}
>
<Plus size={16} />
New Lead
</button>
</div>
{/* Search Bar */}
<div style={{ marginBottom: '20px' }}>
<div style={{ position: 'relative' }}>
<Search
size={20}
style={{ position: 'absolute', left: '12px', top: '50%', transform: 'translateY(-50%)', color: '#666' }}
/>
<input
type="text"
placeholder="Search leads..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && loadLeads()}
style={{
width: '100%',
background: '#2a2a2a',
border: '1px solid #3a3a3a',
color: '#e0e0e0',
padding: '12px 12px 12px 44px',
borderRadius: '6px',
fontSize: '14px',
}}
/>
</div>
</div>
{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}>
<div style={{ background: '#2a2a2a', padding: '16px', borderRadius: '8px', border: '1px solid #3a3a3a' }}>
<div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>Total Leads</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#3b82f6' }}>{leads.length}</div>
</div>
<div style={{ background: '#2a2a2a', padding: '16px', borderRadius: '8px', border: '1px solid #3a3a3a' }}>
<div style={{ fontSize: '12px', color: '#888', marginBottom: '4px' }}>Active</div>
<div style={{ fontSize: '24px', fontWeight: 'bold', color: '#10b981' }}>
{leads.filter(l => l.status_label?.toLowerCase().includes('active')).length}
</div>
</div>
</div>
{/* Leads Table */}
{loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>Loading...</div>
) : (
<div style={{ background: '#2a2a2a', borderRadius: '8px', border: '1px solid #3a3a3a', overflow: 'hidden' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ background: '#333', borderBottom: '1px solid #3a3a3a' }}>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#888' }}>Name</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#888' }}>Status</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#888' }}>Contacts</th>
<th style={{ padding: '12px', textAlign: 'left', fontSize: '12px', fontWeight: '600', color: '#888' }}>Created</th>
</tr>
</thead>
<tbody>
{leads.map((lead) => (
<tr
key={lead.id}
onClick={() => window.mcp?.showPrompt(`Show details for lead ${lead.id}`)}
style={{ borderBottom: '1px solid #3a3a3a', cursor: 'pointer' }}
onMouseEnter={(e) => (e.currentTarget.style.background = '#333')}
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
>
<td style={{ padding: '12px' }}>
<div style={{ fontWeight: '500' }}>{lead.display_name}</div>
{lead.url && <div style={{ fontSize: '12px', color: '#666' }}>{lead.url}</div>}
</td>
<td style={{ padding: '12px' }}>
<span
style={{
background: '#3b82f622',
color: '#3b82f6',
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
}}
>
{lead.status_label}
</span>
</td>
<td style={{ padding: '12px', color: '#888' }}>{lead.contacts?.length || 0}</td>
<td style={{ padding: '12px', color: '#888', fontSize: '12px' }}>
{new Date(lead.date_created).toLocaleDateString()}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
);
};
export default LeadsDashboard;

View File

@ -0,0 +1,147 @@
import React, { useState, useEffect } from 'react';
import { TrendingUp, DollarSign, Target } from 'lucide-react';
export const OpportunitiesPipeline: React.FC = () => {
const [opportunities, setOpportunities] = useState<any[]>([]);
const [pipelines, setPipelines] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
setLoading(true);
try {
const [oppsResult, pipelinesResult] = await Promise.all([
window.mcp?.callTool('close_list_opportunities', { _limit: 100 }),
window.mcp?.callTool('close_get_opportunity_pipelines', {}),
]);
if (oppsResult?.content?.[0]?.text) {
const data = JSON.parse(oppsResult.content[0].text);
setOpportunities(data.data || []);
}
if (pipelinesResult?.content?.[0]?.text) {
const data = JSON.parse(pipelinesResult.content[0].text);
setPipelines(data.data || []);
}
} catch (error) {
console.error('Failed to load data:', error);
} finally {
setLoading(false);
}
};
const totalValue = opportunities.reduce((sum, opp) => sum + (opp.value || 0), 0) / 100;
const activeOpps = opportunities.filter(o => o.status_type === 'active').length;
const wonOpps = opportunities.filter(o => o.status_type === 'won').length;
return (
<div style={{ background: '#1a1a1a', color: '#e0e0e0', minHeight: '100vh', padding: '20px' }}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '24px' }}>
<TrendingUp size={32} color="#10b981" />
<h1 style={{ fontSize: '24px', fontWeight: 'bold', margin: 0 }}>Opportunities Pipeline</h1>
</div>
{/* Stats */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))', gap: '16px', marginBottom: '32px' }}>
<div style={{ background: '#2a2a2a', padding: '20px', borderRadius: '8px', border: '1px solid #3a3a3a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<DollarSign size={20} color="#10b981" />
<span style={{ fontSize: '12px', color: '#888' }}>Total Pipeline Value</span>
</div>
<div style={{ fontSize: '28px', fontWeight: 'bold', color: '#10b981' }}>
${totalValue.toLocaleString()}
</div>
</div>
<div style={{ background: '#2a2a2a', padding: '20px', borderRadius: '8px', border: '1px solid #3a3a3a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<Target size={20} color="#3b82f6" />
<span style={{ fontSize: '12px', color: '#888' }}>Active Opportunities</span>
</div>
<div style={{ fontSize: '28px', fontWeight: 'bold', color: '#3b82f6' }}>
{activeOpps}
</div>
</div>
<div style={{ background: '#2a2a2a', padding: '20px', borderRadius: '8px', border: '1px solid #3a3a3a' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', marginBottom: '8px' }}>
<TrendingUp size={20} color="#f59e0b" />
<span style={{ fontSize: '12px', color: '#888' }}>Won This Month</span>
</div>
<div style={{ fontSize: '28px', fontWeight: 'bold', color: '#f59e0b' }}>
{wonOpps}
</div>
</div>
</div>
{/* Pipeline Board */}
{loading ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#666' }}>Loading pipeline...</div>
) : (
<div style={{ display: 'flex', gap: '16px', overflowX: 'auto', paddingBottom: '16px' }}>
{['active', 'won', 'lost'].map((statusType) => {
const statusOpps = opportunities.filter(o => o.status_type === statusType);
const statusValue = statusOpps.reduce((sum, opp) => sum + (opp.value || 0), 0) / 100;
return (
<div
key={statusType}
style={{
minWidth: '320px',
background: '#2a2a2a',
borderRadius: '8px',
border: '1px solid #3a3a3a',
padding: '16px',
}}
>
<div style={{ marginBottom: '16px' }}>
<div style={{ fontSize: '14px', fontWeight: '600', textTransform: 'capitalize', marginBottom: '4px' }}>
{statusType}
</div>
<div style={{ fontSize: '12px', color: '#888' }}>
{statusOpps.length} deals · ${statusValue.toLocaleString()}
</div>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
{statusOpps.map((opp) => (
<div
key={opp.id}
onClick={() => window.mcp?.showPrompt(`Show opportunity ${opp.id}`)}
style={{
background: '#333',
border: '1px solid #3a3a3a',
borderRadius: '6px',
padding: '12px',
cursor: 'pointer',
}}
onMouseEnter={(e) => (e.currentTarget.style.borderColor = '#3b82f6')}
onMouseLeave={(e) => (e.currentTarget.style.borderColor = '#3a3a3a')}
>
<div style={{ fontSize: '14px', fontWeight: '500', marginBottom: '4px' }}>
{opp.lead_name || `Opportunity ${opp.id.slice(0, 8)}`}
</div>
<div style={{ fontSize: '16px', fontWeight: 'bold', color: '#10b981', marginBottom: '4px' }}>
${((opp.value || 0) / 100).toLocaleString()}
</div>
{opp.confidence !== undefined && (
<div style={{ fontSize: '11px', color: '#888' }}>
{opp.confidence}% confidence
</div>
)}
</div>
))}
</div>
</div>
);
})}
</div>
)}
</div>
</div>
);
};
export default OpportunitiesPipeline;

View File

@ -0,0 +1,976 @@
/**
* Close CRM MCP Tools - 60+ Tools
* Complete tool definitions for MCP server
*/
import { Tool } from '@modelcontextprotocol/sdk/types.js';
export const CLOSE_TOOLS: Tool[] = [
// ============================================================================
// LEAD TOOLS (10 tools)
// ============================================================================
{
name: 'close_list_leads',
description: 'List all leads with optional filtering and pagination',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
status_id: { type: 'string', description: 'Filter by status ID' },
user_id: { type: 'string', description: 'Filter by assigned user ID' },
_skip: { type: 'number', description: 'Number of records to skip (pagination)' },
_limit: { type: 'number', description: 'Max number of records to return (default: 100)' },
},
},
},
{
name: 'close_get_lead',
description: 'Get a specific lead by ID with all details',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Lead ID' },
},
required: ['id'],
},
},
{
name: 'close_create_lead',
description: 'Create a new lead',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Company/lead name' },
description: { type: 'string', description: 'Lead description' },
url: { type: 'string', description: 'Company website URL' },
status_id: { type: 'string', description: 'Lead status ID' },
contacts: {
type: 'array',
description: 'Array of contacts to create with this lead',
items: { type: 'object' },
},
custom: { type: 'object', description: 'Custom field values (key-value pairs)' },
},
required: ['name'],
},
},
{
name: 'close_update_lead',
description: 'Update an existing lead',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Lead ID' },
name: { type: 'string', description: 'Company/lead name' },
description: { type: 'string', description: 'Lead description' },
url: { type: 'string', description: 'Company website URL' },
status_id: { type: 'string', description: 'Lead status ID' },
custom: { type: 'object', description: 'Custom field values' },
},
required: ['id'],
},
},
{
name: 'close_delete_lead',
description: 'Delete a lead permanently',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Lead ID to delete' },
},
required: ['id'],
},
},
{
name: 'close_search_leads',
description: 'Search leads with advanced query',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query (supports field:value syntax)' },
_limit: { type: 'number', description: 'Max results' },
},
required: ['query'],
},
},
{
name: 'close_merge_leads',
description: 'Merge two leads (moves all data from source to destination)',
inputSchema: {
type: 'object',
properties: {
source: { type: 'string', description: 'Source lead ID (will be deleted)' },
destination: { type: 'string', description: 'Destination lead ID (will receive all data)' },
},
required: ['source', 'destination'],
},
},
{
name: 'close_get_lead_statuses',
description: 'Get all available lead statuses',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_bulk_update_leads',
description: 'Update multiple leads at once',
inputSchema: {
type: 'object',
properties: {
lead_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of lead IDs to update',
},
status_id: { type: 'string', description: 'New status ID' },
custom: { type: 'object', description: 'Custom field updates' },
},
required: ['lead_ids'],
},
},
{
name: 'close_bulk_delete_leads',
description: 'Delete multiple leads at once',
inputSchema: {
type: 'object',
properties: {
lead_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of lead IDs to delete',
},
},
required: ['lead_ids'],
},
},
// ============================================================================
// CONTACT TOOLS (7 tools)
// ============================================================================
{
name: 'close_list_contacts',
description: 'List all contacts with pagination',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Filter by lead ID' },
_skip: { type: 'number', description: 'Pagination skip' },
_limit: { type: 'number', description: 'Pagination limit' },
},
},
},
{
name: 'close_get_contact',
description: 'Get a specific contact by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
},
required: ['id'],
},
},
{
name: 'close_create_contact',
description: 'Create a new contact',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID this contact belongs to' },
name: { type: 'string', description: 'Contact full name' },
title: { type: 'string', description: 'Job title' },
emails: {
type: 'array',
description: 'Array of email objects [{email: "...", type: "office"}]',
items: { type: 'object' },
},
phones: {
type: 'array',
description: 'Array of phone objects [{phone: "...", type: "mobile"}]',
items: { type: 'object' },
},
},
required: ['lead_id'],
},
},
{
name: 'close_update_contact',
description: 'Update an existing contact',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
name: { type: 'string', description: 'Contact name' },
title: { type: 'string', description: 'Job title' },
emails: { type: 'array', items: { type: 'object' } },
phones: { type: 'array', items: { type: 'object' } },
},
required: ['id'],
},
},
{
name: 'close_delete_contact',
description: 'Delete a contact',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Contact ID' },
},
required: ['id'],
},
},
{
name: 'close_bulk_delete_contacts',
description: 'Delete multiple contacts at once',
inputSchema: {
type: 'object',
properties: {
contact_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of contact IDs to delete',
},
},
required: ['contact_ids'],
},
},
{
name: 'close_search_contacts',
description: 'Search contacts by name, email, or phone',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Search query' },
_limit: { type: 'number', description: 'Max results' },
},
required: ['query'],
},
},
// ============================================================================
// OPPORTUNITY TOOLS (8 tools)
// ============================================================================
{
name: 'close_list_opportunities',
description: 'List all opportunities with filtering',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Filter by lead ID' },
status_id: { type: 'string', description: 'Filter by status ID' },
status_type: {
type: 'string',
enum: ['active', 'won', 'lost'],
description: 'Filter by status type',
},
user_id: { type: 'string', description: 'Filter by assigned user' },
_skip: { type: 'number' },
_limit: { type: 'number' },
},
},
},
{
name: 'close_get_opportunity',
description: 'Get a specific opportunity by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opportunity ID' },
},
required: ['id'],
},
},
{
name: 'close_create_opportunity',
description: 'Create a new opportunity/deal',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
status_id: { type: 'string', description: 'Pipeline status ID' },
value: { type: 'number', description: 'Deal value in cents' },
value_period: {
type: 'string',
enum: ['one_time', 'monthly', 'annual'],
description: 'Value period',
},
confidence: { type: 'number', description: 'Win probability (0-100)' },
note: { type: 'string', description: 'Opportunity note' },
contact_id: { type: 'string', description: 'Primary contact ID' },
},
required: ['lead_id', 'status_id'],
},
},
{
name: 'close_update_opportunity',
description: 'Update an existing opportunity',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opportunity ID' },
status_id: { type: 'string', description: 'New status ID' },
value: { type: 'number', description: 'Deal value' },
confidence: { type: 'number', description: 'Win probability' },
note: { type: 'string', description: 'Note' },
},
required: ['id'],
},
},
{
name: 'close_delete_opportunity',
description: 'Delete an opportunity',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opportunity ID' },
},
required: ['id'],
},
},
{
name: 'close_mark_opportunity_won',
description: 'Mark an opportunity as won',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opportunity ID' },
status_id: { type: 'string', description: 'Won status ID' },
date_won: { type: 'string', description: 'Won date (ISO 8601)' },
},
required: ['id', 'status_id'],
},
},
{
name: 'close_mark_opportunity_lost',
description: 'Mark an opportunity as lost',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Opportunity ID' },
status_id: { type: 'string', description: 'Lost status ID' },
date_lost: { type: 'string', description: 'Lost date (ISO 8601)' },
},
required: ['id', 'status_id'],
},
},
{
name: 'close_get_opportunity_pipelines',
description: 'Get all opportunity pipelines and their statuses',
inputSchema: {
type: 'object',
properties: {},
},
},
// ============================================================================
// ACTIVITY TOOLS (12 tools)
// ============================================================================
{
name: 'close_list_activities',
description: 'List activities (emails, calls, notes, etc.)',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Filter by lead ID' },
contact_id: { type: 'string', description: 'Filter by contact ID' },
_skip: { type: 'number' },
_limit: { type: 'number' },
},
},
},
{
name: 'close_get_activity',
description: 'Get a specific activity by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Activity ID' },
},
required: ['id'],
},
},
{
name: 'close_send_email',
description: 'Send an email to a lead or contact',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
to: {
type: 'array',
items: { type: 'string' },
description: 'Array of recipient email addresses',
},
subject: { type: 'string', description: 'Email subject' },
body_text: { type: 'string', description: 'Plain text body' },
body_html: { type: 'string', description: 'HTML body' },
cc: { type: 'array', items: { type: 'string' }, description: 'CC recipients' },
bcc: { type: 'array', items: { type: 'string' }, description: 'BCC recipients' },
},
required: ['to', 'subject'],
},
},
{
name: 'close_create_email_draft',
description: 'Create an email draft (not sent)',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string' },
contact_id: { type: 'string' },
to: { type: 'array', items: { type: 'string' } },
subject: { type: 'string' },
body_text: { type: 'string' },
body_html: { type: 'string' },
},
required: ['to', 'subject'],
},
},
{
name: 'close_log_call',
description: 'Log a phone call activity',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
direction: {
type: 'string',
enum: ['inbound', 'outbound'],
description: 'Call direction',
},
status: {
type: 'string',
enum: ['completed', 'canceled', 'no-answer', 'busy', 'failed'],
description: 'Call outcome',
},
duration: { type: 'number', description: 'Call duration in seconds' },
note: { type: 'string', description: 'Call notes' },
phone: { type: 'string', description: 'Phone number called' },
},
required: ['direction', 'status'],
},
},
{
name: 'close_update_call',
description: 'Update a call activity (e.g., add notes after call)',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Call activity ID' },
note: { type: 'string', description: 'Call notes' },
disposition: { type: 'string', description: 'Call disposition' },
},
required: ['id'],
},
},
{
name: 'close_create_note',
description: 'Add a note to a lead or contact',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
note: { type: 'string', description: 'Note content' },
},
required: ['note'],
},
},
{
name: 'close_send_sms',
description: 'Send an SMS message',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
text: { type: 'string', description: 'SMS message text' },
remote_phone: { type: 'string', description: 'Recipient phone number' },
local_phone: { type: 'string', description: 'Sender phone number' },
},
required: ['text', 'remote_phone'],
},
},
{
name: 'close_create_meeting',
description: 'Schedule a meeting activity',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
title: { type: 'string', description: 'Meeting title' },
note: { type: 'string', description: 'Meeting notes/agenda' },
starts_at: { type: 'string', description: 'Meeting start time (ISO 8601)' },
ends_at: { type: 'string', description: 'Meeting end time (ISO 8601)' },
location: { type: 'string', description: 'Meeting location' },
},
required: ['starts_at'],
},
},
{
name: 'close_bulk_email',
description: 'Send bulk email to multiple leads',
inputSchema: {
type: 'object',
properties: {
lead_ids: {
type: 'array',
items: { type: 'string' },
description: 'Array of lead IDs',
},
subject: { type: 'string', description: 'Email subject' },
body_text: { type: 'string', description: 'Email body (plain text)' },
body_html: { type: 'string', description: 'Email body (HTML)' },
},
required: ['lead_ids', 'subject'],
},
},
{
name: 'close_get_email_templates',
description: 'List all email templates',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_get_email_template',
description: 'Get a specific email template',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Template ID' },
},
required: ['id'],
},
},
// ============================================================================
// TASK TOOLS (6 tools)
// ============================================================================
{
name: 'close_list_tasks',
description: 'List tasks with filtering',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Filter by lead ID' },
contact_id: { type: 'string', description: 'Filter by contact ID' },
is_complete: { type: 'boolean', description: 'Filter by completion status' },
assigned_to: { type: 'string', description: 'Filter by assigned user ID' },
_skip: { type: 'number' },
_limit: { type: 'number' },
},
},
},
{
name: 'close_get_task',
description: 'Get a specific task by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
},
required: ['id'],
},
},
{
name: 'close_create_task',
description: 'Create a new task',
inputSchema: {
type: 'object',
properties: {
lead_id: { type: 'string', description: 'Lead ID' },
contact_id: { type: 'string', description: 'Contact ID' },
assigned_to: { type: 'string', description: 'User ID to assign to' },
text: { type: 'string', description: 'Task description' },
date: { type: 'string', description: 'Due date (ISO 8601)' },
},
required: ['text', 'assigned_to'],
},
},
{
name: 'close_update_task',
description: 'Update an existing task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
text: { type: 'string', description: 'Task description' },
date: { type: 'string', description: 'Due date' },
assigned_to: { type: 'string', description: 'User ID' },
is_complete: { type: 'boolean', description: 'Completion status' },
},
required: ['id'],
},
},
{
name: 'close_complete_task',
description: 'Mark a task as complete',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
},
required: ['id'],
},
},
{
name: 'close_delete_task',
description: 'Delete a task',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Task ID' },
},
required: ['id'],
},
},
// ============================================================================
// SMART VIEW TOOLS (5 tools)
// ============================================================================
{
name: 'close_list_smart_views',
description: 'List all saved smart views/searches',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['lead', 'opportunity', 'contact'],
description: 'Filter by view type',
},
},
},
},
{
name: 'close_get_smart_view',
description: 'Get a specific smart view by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Smart view ID' },
},
required: ['id'],
},
},
{
name: 'close_create_smart_view',
description: 'Create a new smart view/saved search',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'View name' },
type: {
type: 'string',
enum: ['lead', 'opportunity', 'contact'],
description: 'View type',
},
query: { type: 'object', description: 'Search query object' },
},
required: ['name', 'type', 'query'],
},
},
{
name: 'close_update_smart_view',
description: 'Update an existing smart view',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Smart view ID' },
name: { type: 'string', description: 'View name' },
query: { type: 'object', description: 'Search query' },
},
required: ['id'],
},
},
{
name: 'close_delete_smart_view',
description: 'Delete a smart view',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Smart view ID' },
},
required: ['id'],
},
},
// ============================================================================
// USER TOOLS (3 tools)
// ============================================================================
{
name: 'close_list_users',
description: 'List all users in the organization',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_get_user',
description: 'Get a specific user by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'User ID' },
},
required: ['id'],
},
},
{
name: 'close_get_current_user',
description: 'Get the currently authenticated user',
inputSchema: {
type: 'object',
properties: {},
},
},
// ============================================================================
// CUSTOM FIELD TOOLS (5 tools)
// ============================================================================
{
name: 'close_list_custom_fields',
description: 'List all custom fields',
inputSchema: {
type: 'object',
properties: {
type: {
type: 'string',
enum: ['lead', 'contact', 'opportunity'],
description: 'Filter by object type',
},
},
},
},
{
name: 'close_get_custom_field',
description: 'Get a specific custom field by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Custom field ID' },
},
required: ['id'],
},
},
{
name: 'close_create_custom_field',
description: 'Create a new custom field',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string', description: 'Field name' },
type: {
type: 'string',
enum: ['text', 'number', 'date', 'choices', 'user'],
description: 'Field type',
},
choices: {
type: 'array',
items: { type: 'string' },
description: 'Available choices (for choices type)',
},
},
required: ['name', 'type'],
},
},
{
name: 'close_update_custom_field',
description: 'Update a custom field definition',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Custom field ID' },
name: { type: 'string', description: 'Field name' },
choices: { type: 'array', items: { type: 'string' } },
},
required: ['id'],
},
},
{
name: 'close_delete_custom_field',
description: 'Delete a custom field',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Custom field ID' },
},
required: ['id'],
},
},
// ============================================================================
// SEQUENCE TOOLS (6 tools)
// ============================================================================
{
name: 'close_list_sequences',
description: 'List all email sequences',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_get_sequence',
description: 'Get a specific sequence by ID',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Sequence ID' },
},
required: ['id'],
},
},
{
name: 'close_subscribe_to_sequence',
description: 'Subscribe a contact to an email sequence',
inputSchema: {
type: 'object',
properties: {
sequence_id: { type: 'string', description: 'Sequence ID' },
contact_id: { type: 'string', description: 'Contact ID' },
sender_account_id: { type: 'string', description: 'Email account ID to send from' },
},
required: ['sequence_id', 'contact_id'],
},
},
{
name: 'close_list_sequence_subscriptions',
description: 'List sequence subscriptions with filtering',
inputSchema: {
type: 'object',
properties: {
sequence_id: { type: 'string', description: 'Filter by sequence ID' },
contact_id: { type: 'string', description: 'Filter by contact ID' },
lead_id: { type: 'string', description: 'Filter by lead ID' },
},
},
},
{
name: 'close_pause_sequence_subscription',
description: 'Pause a sequence subscription',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Subscription ID' },
},
required: ['id'],
},
},
{
name: 'close_unsubscribe_from_sequence',
description: 'Unsubscribe/remove a contact from a sequence',
inputSchema: {
type: 'object',
properties: {
id: { type: 'string', description: 'Subscription ID' },
},
required: ['id'],
},
},
// ============================================================================
// REPORTING TOOLS (4 tools)
// ============================================================================
{
name: 'close_get_leads_report',
description: 'Get leads report/analytics',
inputSchema: {
type: 'object',
properties: {
date_start: { type: 'string', description: 'Start date (ISO 8601)' },
date_end: { type: 'string', description: 'End date (ISO 8601)' },
group_by: { type: 'string', description: 'Grouping field' },
},
},
},
{
name: 'close_get_opportunities_report',
description: 'Get opportunities/pipeline report',
inputSchema: {
type: 'object',
properties: {
date_start: { type: 'string', description: 'Start date' },
date_end: { type: 'string', description: 'End date' },
group_by: { type: 'string', description: 'Grouping field (e.g., status_id, user_id)' },
},
},
},
{
name: 'close_get_activities_report',
description: 'Get activities report (calls, emails, etc.)',
inputSchema: {
type: 'object',
properties: {
date_start: { type: 'string', description: 'Start date' },
date_end: { type: 'string', description: 'End date' },
activity_type: { type: 'string', description: 'Filter by activity type' },
},
},
},
{
name: 'close_get_user_activity_report',
description: 'Get activity report for a specific user',
inputSchema: {
type: 'object',
properties: {
user_id: { type: 'string', description: 'User ID' },
date_start: { type: 'string', description: 'Start date' },
date_end: { type: 'string', description: 'End date' },
},
required: ['user_id'],
},
},
// ============================================================================
// ORGANIZATION & MISC TOOLS (4 tools)
// ============================================================================
{
name: 'close_get_organization',
description: 'Get organization details and settings',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_list_webhooks',
description: 'List all configured webhooks',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'close_create_webhook',
description: 'Create a new webhook subscription',
inputSchema: {
type: 'object',
properties: {
url: { type: 'string', description: 'Webhook endpoint URL' },
events: {
type: 'array',
items: { type: 'string' },
description: 'Array of event types to subscribe to',
},
},
required: ['url', 'events'],
},
},
{
name: 'close_get_connected_accounts',
description: 'List all connected email/calendar accounts',
inputSchema: {
type: 'object',
properties: {},
},
},
];

302
servers/close/src/types.ts Normal file
View File

@ -0,0 +1,302 @@
/**
* Close CRM MCP Server - TypeScript Type Definitions
* Complete type system for Close API v1
*/
// Configuration
export interface CloseConfig {
apiKey: string;
baseUrl?: string;
}
// Core Resources
export interface Lead {
id: string;
name: string;
display_name: string;
status_id: string;
status_label: string;
description?: string;
url?: string;
created_by: string;
updated_by: string;
date_created: string;
date_updated: string;
organization_id: string;
contacts?: Contact[];
opportunities?: Opportunity[];
tasks?: Task[];
custom?: Record<string, any>;
addresses?: Address[];
}
export interface Contact {
id: string;
lead_id: string;
name: string;
title?: string;
emails: EmailAddress[];
phones: PhoneNumber[];
urls?: UrlField[];
date_created: string;
date_updated: string;
created_by: string;
updated_by: string;
organization_id: string;
}
export interface EmailAddress {
email: string;
type: string;
}
export interface PhoneNumber {
phone: string;
type: string;
country?: string;
}
export interface UrlField {
url: string;
type?: string;
}
export interface Address {
address_1?: string;
address_2?: string;
city?: string;
state?: string;
zipcode?: string;
country?: string;
label?: string;
}
export interface Opportunity {
id: string;
lead_id: string;
status_id: string;
status_label: string;
status_type: 'active' | 'won' | 'lost';
confidence?: number;
value?: number;
value_period?: 'one_time' | 'monthly' | 'annual';
value_currency?: string;
date_created: string;
date_updated: string;
created_by: string;
updated_by: string;
note?: string;
organization_id: string;
contact_id?: string;
user_id?: string;
date_won?: string;
date_lost?: string;
}
export interface Activity {
id: string;
type: string;
lead_id?: string;
contact_id?: string;
opportunity_id?: string;
user_id: string;
created_by: string;
date_created: string;
date_updated: string;
organization_id: string;
}
export interface Task {
id: string;
_type: 'lead' | 'contact';
lead_id?: string;
contact_id?: string;
assigned_to: string;
text: string;
date: string;
is_complete: boolean;
created_by: string;
date_created: string;
date_updated: string;
organization_id: string;
view?: string;
}
export interface Pipeline {
id: string;
name: string;
organization_id: string;
statuses: PipelineStatus[];
}
export interface PipelineStatus {
id: string;
label: string;
type: 'active' | 'won' | 'lost';
}
export interface SmartView {
id: string;
name: string;
type: 'lead' | 'opportunity' | 'contact';
query: any;
organization_id: string;
created_by: string;
date_created: string;
date_updated: string;
}
export interface User {
id: string;
first_name: string;
last_name: string;
email: string;
image?: string;
date_created: string;
date_updated: string;
}
export interface CustomField {
id: string;
name: string;
type: 'text' | 'number' | 'date' | 'choices' | 'user';
choices?: string[];
accepts_multiple_values?: boolean;
editable_with_roles?: string[];
created_by: string;
date_created: string;
date_updated: string;
organization_id: string;
}
export interface Sequence {
id: string;
name: string;
status: 'active' | 'paused' | 'archived';
created_by: string;
updated_by: string;
date_created: string;
date_updated: string;
organization_id: string;
}
export interface SequenceSubscription {
id: string;
sequence_id: string;
contact_id: string;
lead_id: string;
sender_account_id: string;
sender_name: string;
sender_email: string;
status: 'active' | 'paused' | 'finished';
date_created: string;
date_updated: string;
}
// Activity Types
export interface EmailActivity extends Activity {
type: 'Email';
direction: 'incoming' | 'outgoing';
status: 'draft' | 'scheduled' | 'sent' | 'delivered' | 'bounced';
subject?: string;
body_text?: string;
body_html?: string;
sender?: string;
to?: string[];
cc?: string[];
bcc?: string[];
template_id?: string;
thread_id?: string;
opens?: any[];
clicks?: any[];
}
export interface CallActivity extends Activity {
type: 'Call';
direction: 'inbound' | 'outbound';
status: 'completed' | 'canceled' | 'no-answer' | 'busy' | 'failed';
duration?: number;
note?: string;
phone?: string;
disposition?: string;
recording_url?: string;
voicemail_url?: string;
}
export interface NoteActivity extends Activity {
type: 'Note';
note: string;
}
export interface SMSActivity extends Activity {
type: 'SMS';
direction: 'incoming' | 'outgoing';
status: 'sent' | 'delivered' | 'failed';
text: string;
local_phone?: string;
remote_phone?: string;
}
export interface MeetingActivity extends Activity {
type: 'Meeting';
title?: string;
note?: string;
starts_at?: string;
ends_at?: string;
location?: string;
attendees?: string[];
}
// Bulk Operations
export interface BulkAction {
id: string;
type: string;
status: 'pending' | 'processing' | 'completed' | 'failed';
created_by: string;
date_created: string;
date_updated: string;
organization_id: string;
}
// Reporting
export interface Report {
data: any[];
aggregate?: Record<string, any>;
cursor?: string;
}
// API Response Types
export interface PaginatedResponse<T> {
data: T[];
has_more: boolean;
cursor?: string;
}
export interface ApiResponse<T> {
data?: T;
error?: string;
}
// Search/Filter
export interface SearchParams {
query?: string;
_skip?: number;
_limit?: number;
_fields?: string[];
_order_by?: string;
}
export interface LeadSearchParams extends SearchParams {
status_id?: string;
user_id?: string;
}
export interface OpportunitySearchParams extends SearchParams {
status_id?: string;
status_type?: 'active' | 'won' | 'lost';
user_id?: string;
lead_id?: string;
}
// Export types
export type ActivityType = EmailActivity | CallActivity | NoteActivity | SMSActivity | MeetingActivity;

View File

@ -1,290 +1,392 @@
/**
* FieldEdge API Client
* Handles authentication, rate limiting, pagination, and error handling
* Handles authentication, requests, error mapping, and rate limiting
*/
import axios, { AxiosInstance, AxiosError, AxiosRequestConfig } from 'axios';
import type {
FieldEdgeConfig,
ApiResponse,
Customer,
Job,
Invoice,
Estimate,
Equipment,
Technician,
InventoryItem,
Payment,
Appointment,
WorkOrder,
Location,
StockAdjustment,
PaginatedResponse,
QueryParams,
ApiResponse,
CustomerSearchParams,
JobSearchParams,
InvoiceSearchParams,
} from '../types/index.js';
export class FieldEdgeAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public response?: any
) {
super(message);
this.name = 'FieldEdgeAPIError';
}
}
export class FieldEdgeClient {
private client: AxiosInstance;
private config: FieldEdgeConfig;
private rateLimitRemaining: number = 1000;
private rateLimitReset: number = 0;
private apiKey: string;
private baseUrl: string;
private headers: Record<string, string>;
constructor(config: FieldEdgeConfig) {
this.config = {
apiUrl: 'https://api.fieldedge.com/v1',
timeout: 30000,
...config,
};
this.client = axios.create({
baseURL: this.config.apiUrl,
timeout: this.config.timeout,
headers: {
'Authorization': `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
...(this.config.companyId && { 'X-Company-Id': this.config.companyId }),
},
});
// Request interceptor for rate limiting
this.client.interceptors.request.use(
async (config) => {
// Check rate limit
if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) {
const waitTime = this.rateLimitReset - Date.now();
console.warn(`Rate limit exceeded. Waiting ${waitTime}ms...`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
return config;
},
(error) => Promise.reject(error)
);
// Response interceptor for error handling and rate limit tracking
this.client.interceptors.response.use(
(response) => {
// Update rate limit info from headers
const remaining = response.headers['x-ratelimit-remaining'];
const reset = response.headers['x-ratelimit-reset'];
if (remaining) this.rateLimitRemaining = parseInt(remaining);
if (reset) this.rateLimitReset = parseInt(reset) * 1000;
return response;
},
(error: AxiosError) => {
if (error.response) {
const { status, data } = error.response;
// Handle specific error codes
switch (status) {
case 401:
throw new FieldEdgeAPIError('Authentication failed. Invalid API key.', 401, data);
case 403:
throw new FieldEdgeAPIError('Access forbidden. Check permissions.', 403, data);
case 404:
throw new FieldEdgeAPIError('Resource not found.', 404, data);
case 429:
throw new FieldEdgeAPIError('Rate limit exceeded.', 429, data);
case 500:
case 502:
case 503:
throw new FieldEdgeAPIError('FieldEdge server error. Please try again later.', status, data);
default:
const message = (data as any)?.message || (data as any)?.error || 'API request failed';
throw new FieldEdgeAPIError(message, status, data);
}
} else if (error.request) {
throw new FieldEdgeAPIError('No response from FieldEdge API. Check network connection.');
} else {
throw new FieldEdgeAPIError(`Request error: ${error.message}`);
}
}
);
}
/**
* Generic GET request with pagination support
*/
async get<T>(endpoint: string, params?: QueryParams): Promise<T> {
const response = await this.client.get(endpoint, { params });
return response.data;
}
/**
* Generic GET request for paginated data
*/
async getPaginated<T>(
endpoint: string,
params?: QueryParams
): Promise<PaginatedResponse<T>> {
const queryParams = {
page: params?.page || 1,
pageSize: params?.pageSize || 50,
...params,
};
const response = await this.client.get(endpoint, { params: queryParams });
this.apiKey = config.apiKey;
this.baseUrl = config.baseUrl || 'https://api.fieldedge.com/v1';
return {
items: response.data.items || response.data.data || response.data,
total: response.data.total || response.data.items?.length || 0,
page: response.data.page || queryParams.page,
pageSize: response.data.pageSize || queryParams.pageSize,
totalPages: response.data.totalPages || Math.ceil((response.data.total || 0) / queryParams.pageSize),
if (config.environment === 'sandbox') {
this.baseUrl = 'https://sandbox-api.fieldedge.com/v1';
}
this.headers = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
/**
* Get all pages of paginated data
*/
async getAllPaginated<T>(
private async request<T>(
method: string,
endpoint: string,
params?: QueryParams
): Promise<T[]> {
const allItems: T[] = [];
let page = 1;
let hasMore = true;
while (hasMore) {
const response = await this.getPaginated<T>(endpoint, {
...params,
page,
pageSize: params?.pageSize || 100,
data?: any,
params?: Record<string, any>
): Promise<T> {
const url = new URL(`${this.baseUrl}${endpoint}`);
if (params) {
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
url.searchParams.append(key, String(value));
}
});
allItems.push(...response.items);
hasMore = page < response.totalPages;
page++;
}
return allItems;
}
/**
* Generic POST request
*/
async post<T>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
const response = await this.client.post(endpoint, data, config);
return response.data;
}
/**
* Generic PUT request
*/
async put<T>(endpoint: string, data?: any): Promise<T> {
const response = await this.client.put(endpoint, data);
return response.data;
}
/**
* Generic PATCH request
*/
async patch<T>(endpoint: string, data?: any): Promise<T> {
const response = await this.client.patch(endpoint, data);
return response.data;
}
/**
* Generic DELETE request
*/
async delete<T>(endpoint: string): Promise<T> {
const response = await this.client.delete(endpoint);
return response.data;
}
/**
* Upload file
*/
async uploadFile(endpoint: string, file: Buffer, filename: string, mimeType?: string): Promise<any> {
const formData = new FormData();
const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength) as ArrayBuffer;
const blob = new Blob([arrayBuffer], { type: mimeType || 'application/octet-stream' });
formData.append('file', blob, filename);
const response = await this.client.post(endpoint, formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return response.data;
}
/**
* Download file
*/
async downloadFile(endpoint: string): Promise<Buffer> {
const response = await this.client.get(endpoint, {
responseType: 'arraybuffer',
});
return Buffer.from(response.data);
}
/**
* Test API connectivity
*/
async testConnection(): Promise<boolean> {
try {
await this.client.get('/health');
return true;
} catch (error) {
return false;
}
}
/**
* Get API usage/rate limit info
*/
getRateLimitInfo(): { remaining: number; resetAt: number } {
return {
remaining: this.rateLimitRemaining,
resetAt: this.rateLimitReset,
const options: RequestInit = {
method,
headers: this.headers,
};
if (data) {
options.body = JSON.stringify(data);
}
try {
const response = await fetch(url.toString(), options);
if (!response.ok) {
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch {
errorData = { message: errorText };
}
throw new Error(
`FieldEdge API Error (${response.status}): ${errorData.message || errorText}`
);
}
const responseText = await response.text();
if (!responseText) return {} as T;
return JSON.parse(responseText) as T;
} catch (error) {
if (error instanceof Error) {
throw new Error(`FieldEdge API request failed: ${error.message}`);
}
throw error;
}
}
/**
* Update API configuration
*/
updateConfig(config: Partial<FieldEdgeConfig>): void {
this.config = { ...this.config, ...config };
if (config.apiKey) {
this.client.defaults.headers['Authorization'] = `Bearer ${config.apiKey}`;
}
if (config.companyId) {
this.client.defaults.headers['X-Company-Id'] = config.companyId;
}
if (config.apiUrl) {
this.client.defaults.baseURL = config.apiUrl;
}
if (config.timeout) {
this.client.defaults.timeout = config.timeout;
}
// Customer Methods
async getCustomers(params?: CustomerSearchParams): Promise<PaginatedResponse<Customer>> {
return this.request<PaginatedResponse<Customer>>('GET', '/customers', undefined, params);
}
async getCustomer(customerId: string): Promise<Customer> {
return this.request<Customer>('GET', `/customers/${customerId}`);
}
async createCustomer(data: Partial<Customer>): Promise<Customer> {
return this.request<Customer>('POST', '/customers', data);
}
async updateCustomer(customerId: string, data: Partial<Customer>): Promise<Customer> {
return this.request<Customer>('PUT', `/customers/${customerId}`, data);
}
async deleteCustomer(customerId: string): Promise<void> {
return this.request<void>('DELETE', `/customers/${customerId}`);
}
async searchCustomers(query: string): Promise<Customer[]> {
const response = await this.request<PaginatedResponse<Customer>>(
'GET',
'/customers/search',
undefined,
{ q: query }
);
return response.data;
}
// Job/Work Order Methods
async getJobs(params?: JobSearchParams): Promise<PaginatedResponse<Job>> {
return this.request<PaginatedResponse<Job>>('GET', '/jobs', undefined, params);
}
async getJob(jobId: string): Promise<Job> {
return this.request<Job>('GET', `/jobs/${jobId}`);
}
async createJob(data: Partial<Job>): Promise<Job> {
return this.request<Job>('POST', '/jobs', data);
}
async updateJob(jobId: string, data: Partial<Job>): Promise<Job> {
return this.request<Job>('PUT', `/jobs/${jobId}`, data);
}
async deleteJob(jobId: string): Promise<void> {
return this.request<void>('DELETE', `/jobs/${jobId}`);
}
async getWorkOrders(jobId?: string): Promise<WorkOrder[]> {
const endpoint = jobId ? `/jobs/${jobId}/work-orders` : '/work-orders';
return this.request<WorkOrder[]>('GET', endpoint);
}
async getWorkOrder(workOrderId: string): Promise<WorkOrder> {
return this.request<WorkOrder>('GET', `/work-orders/${workOrderId}`);
}
async createWorkOrder(data: Partial<WorkOrder>): Promise<WorkOrder> {
return this.request<WorkOrder>('POST', '/work-orders', data);
}
async updateWorkOrder(workOrderId: string, data: Partial<WorkOrder>): Promise<WorkOrder> {
return this.request<WorkOrder>('PUT', `/work-orders/${workOrderId}`, data);
}
// Scheduling Methods
async getAppointments(params?: {
technicianId?: string;
startDate?: string;
endDate?: string;
}): Promise<Appointment[]> {
return this.request<Appointment[]>('GET', '/appointments', undefined, params);
}
async getAppointment(appointmentId: string): Promise<Appointment> {
return this.request<Appointment>('GET', `/appointments/${appointmentId}`);
}
async createAppointment(data: Partial<Appointment>): Promise<Appointment> {
return this.request<Appointment>('POST', '/appointments', data);
}
async updateAppointment(appointmentId: string, data: Partial<Appointment>): Promise<Appointment> {
return this.request<Appointment>('PUT', `/appointments/${appointmentId}`, data);
}
async deleteAppointment(appointmentId: string): Promise<void> {
return this.request<void>('DELETE', `/appointments/${appointmentId}`);
}
async getTechnicianSchedule(technicianId: string, startDate: string, endDate: string): Promise<Appointment[]> {
return this.request<Appointment[]>(
'GET',
`/technicians/${technicianId}/schedule`,
undefined,
{ startDate, endDate }
);
}
// Invoice Methods
async getInvoices(params?: InvoiceSearchParams): Promise<PaginatedResponse<Invoice>> {
return this.request<PaginatedResponse<Invoice>>('GET', '/invoices', undefined, params);
}
async getInvoice(invoiceId: string): Promise<Invoice> {
return this.request<Invoice>('GET', `/invoices/${invoiceId}`);
}
async createInvoice(data: Partial<Invoice>): Promise<Invoice> {
return this.request<Invoice>('POST', '/invoices', data);
}
async updateInvoice(invoiceId: string, data: Partial<Invoice>): Promise<Invoice> {
return this.request<Invoice>('PUT', `/invoices/${invoiceId}`, data);
}
async deleteInvoice(invoiceId: string): Promise<void> {
return this.request<void>('DELETE', `/invoices/${invoiceId}`);
}
async sendInvoice(invoiceId: string, email?: string): Promise<void> {
return this.request<void>('POST', `/invoices/${invoiceId}/send`, { email });
}
async recordPayment(invoiceId: string, paymentData: Partial<Payment>): Promise<Payment> {
return this.request<Payment>('POST', `/invoices/${invoiceId}/payments`, paymentData);
}
async getPayments(invoiceId: string): Promise<Payment[]> {
return this.request<Payment[]>('GET', `/invoices/${invoiceId}/payments`);
}
// Estimate Methods
async getEstimates(customerId?: string): Promise<Estimate[]> {
const params = customerId ? { customerId } : undefined;
return this.request<Estimate[]>('GET', '/estimates', undefined, params);
}
async getEstimate(estimateId: string): Promise<Estimate> {
return this.request<Estimate>('GET', `/estimates/${estimateId}`);
}
async createEstimate(data: Partial<Estimate>): Promise<Estimate> {
return this.request<Estimate>('POST', '/estimates', data);
}
async updateEstimate(estimateId: string, data: Partial<Estimate>): Promise<Estimate> {
return this.request<Estimate>('PUT', `/estimates/${estimateId}`, data);
}
async deleteEstimate(estimateId: string): Promise<void> {
return this.request<void>('DELETE', `/estimates/${estimateId}`);
}
async sendEstimate(estimateId: string, email?: string): Promise<void> {
return this.request<void>('POST', `/estimates/${estimateId}/send`, { email });
}
async convertEstimateToJob(estimateId: string): Promise<Job> {
return this.request<Job>('POST', `/estimates/${estimateId}/convert-to-job`);
}
// Equipment Methods
async getEquipment(customerId?: string): Promise<Equipment[]> {
const params = customerId ? { customerId } : undefined;
return this.request<Equipment[]>('GET', '/equipment', undefined, params);
}
async getEquipmentById(equipmentId: string): Promise<Equipment> {
return this.request<Equipment>('GET', `/equipment/${equipmentId}`);
}
async createEquipment(data: Partial<Equipment>): Promise<Equipment> {
return this.request<Equipment>('POST', '/equipment', data);
}
async updateEquipment(equipmentId: string, data: Partial<Equipment>): Promise<Equipment> {
return this.request<Equipment>('PUT', `/equipment/${equipmentId}`, data);
}
async deleteEquipment(equipmentId: string): Promise<void> {
return this.request<void>('DELETE', `/equipment/${equipmentId}`);
}
async getEquipmentHistory(equipmentId: string): Promise<Job[]> {
return this.request<Job[]>('GET', `/equipment/${equipmentId}/history`);
}
// Technician Methods
async getTechnicians(status?: 'active' | 'inactive'): Promise<Technician[]> {
const params = status ? { status } : undefined;
return this.request<Technician[]>('GET', '/technicians', undefined, params);
}
async getTechnician(technicianId: string): Promise<Technician> {
return this.request<Technician>('GET', `/technicians/${technicianId}`);
}
async createTechnician(data: Partial<Technician>): Promise<Technician> {
return this.request<Technician>('POST', '/technicians', data);
}
async updateTechnician(technicianId: string, data: Partial<Technician>): Promise<Technician> {
return this.request<Technician>('PUT', `/technicians/${technicianId}`, data);
}
async deleteTechnician(technicianId: string): Promise<void> {
return this.request<void>('DELETE', `/technicians/${technicianId}`);
}
// Inventory Methods
async getInventoryItems(category?: string): Promise<InventoryItem[]> {
const params = category ? { category } : undefined;
return this.request<InventoryItem[]>('GET', '/inventory', undefined, params);
}
async getInventoryItem(itemId: string): Promise<InventoryItem> {
return this.request<InventoryItem>('GET', `/inventory/${itemId}`);
}
async createInventoryItem(data: Partial<InventoryItem>): Promise<InventoryItem> {
return this.request<InventoryItem>('POST', '/inventory', data);
}
async updateInventoryItem(itemId: string, data: Partial<InventoryItem>): Promise<InventoryItem> {
return this.request<InventoryItem>('PUT', `/inventory/${itemId}`, data);
}
async deleteInventoryItem(itemId: string): Promise<void> {
return this.request<void>('DELETE', `/inventory/${itemId}`);
}
async adjustStock(itemId: string, adjustment: Partial<StockAdjustment>): Promise<StockAdjustment> {
return this.request<StockAdjustment>('POST', `/inventory/${itemId}/adjust`, adjustment);
}
async getStockHistory(itemId: string): Promise<StockAdjustment[]> {
return this.request<StockAdjustment[]>('GET', `/inventory/${itemId}/history`);
}
async getLowStockItems(): Promise<InventoryItem[]> {
return this.request<InventoryItem[]>('GET', '/inventory/low-stock');
}
// Location Methods
async getLocations(customerId: string): Promise<Location[]> {
return this.request<Location[]>('GET', `/customers/${customerId}/locations`);
}
async getLocation(locationId: string): Promise<Location> {
return this.request<Location>('GET', `/locations/${locationId}`);
}
async createLocation(customerId: string, data: Partial<Location>): Promise<Location> {
return this.request<Location>('POST', `/customers/${customerId}/locations`, data);
}
async updateLocation(locationId: string, data: Partial<Location>): Promise<Location> {
return this.request<Location>('PUT', `/locations/${locationId}`, data);
}
async deleteLocation(locationId: string): Promise<void> {
return this.request<void>('DELETE', `/locations/${locationId}`);
}
// Reporting Methods
async getRevenueReport(startDate: string, endDate: string): Promise<any> {
return this.request<any>('GET', '/reports/revenue', undefined, { startDate, endDate });
}
async getTechnicianPerformance(startDate: string, endDate: string, technicianId?: string): Promise<any> {
const params: any = { startDate, endDate };
if (technicianId) params.technicianId = technicianId;
return this.request<any>('GET', '/reports/technician-performance', undefined, params);
}
async getCustomerReport(customerId: string): Promise<any> {
return this.request<any>('GET', `/reports/customers/${customerId}`);
}
async getJobStatusReport(startDate: string, endDate: string): Promise<any> {
return this.request<any>('GET', '/reports/job-status', undefined, { startDate, endDate });
}
async getInventoryValuation(): Promise<any> {
return this.request<any>('GET', '/reports/inventory-valuation');
}
}
// Singleton instance
let clientInstance: FieldEdgeClient | null = null;
export function getFieldEdgeClient(config?: FieldEdgeConfig): FieldEdgeClient {
if (!clientInstance && config) {
clientInstance = new FieldEdgeClient(config);
}
if (!clientInstance) {
throw new Error('FieldEdge client not initialized. Provide API key configuration.');
}
return clientInstance;
}
export function initializeFieldEdgeClient(config: FieldEdgeConfig): FieldEdgeClient {
clientInstance = new FieldEdgeClient(config);
return clientInstance;
}

View File

@ -0,0 +1,160 @@
/**
* FieldEdge Customer Tools
*/
import { z } from 'zod';
import type { FieldEdgeClient } from '../clients/fieldedge.js';
export function registerCustomerTools(client: FieldEdgeClient) {
return {
fieldedge_list_customers: {
description: 'List all customers with optional filters (status, type, search query)',
parameters: z.object({
query: z.string().optional().describe('Search query for customer name, email, or phone'),
status: z.enum(['active', 'inactive']).optional().describe('Filter by customer status'),
type: z.enum(['residential', 'commercial']).optional().describe('Filter by customer type'),
tags: z.array(z.string()).optional().describe('Filter by tags'),
page: z.number().optional().describe('Page number for pagination'),
pageSize: z.number().optional().describe('Number of results per page'),
sortBy: z.enum(['name', 'createdDate', 'balance']).optional().describe('Sort field'),
sortOrder: z.enum(['asc', 'desc']).optional().describe('Sort order'),
}),
execute: async (args: any) => {
const customers = await client.getCustomers(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(customers, null, 2),
},
],
};
},
},
fieldedge_get_customer: {
description: 'Get detailed information about a specific customer by ID',
parameters: z.object({
customerId: z.string().describe('The customer ID'),
}),
execute: async (args: { customerId: string }) => {
const customer = await client.getCustomer(args.customerId);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
fieldedge_create_customer: {
description: 'Create a new customer',
parameters: z.object({
firstName: z.string().describe('Customer first name'),
lastName: z.string().describe('Customer last name'),
companyName: z.string().optional().describe('Company name for commercial customers'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Primary phone number'),
mobilePhone: z.string().optional().describe('Mobile phone number'),
type: z.enum(['residential', 'commercial']).optional().describe('Customer type'),
address: z.object({
street1: z.string(),
street2: z.string().optional(),
city: z.string(),
state: z.string(),
zip: z.string(),
country: z.string().optional(),
}).optional().describe('Customer address'),
tags: z.array(z.string()).optional().describe('Customer tags'),
notes: z.string().optional().describe('Notes about the customer'),
}),
execute: async (args: any) => {
const customer = await client.createCustomer(args);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
fieldedge_update_customer: {
description: 'Update an existing customer',
parameters: z.object({
customerId: z.string().describe('The customer ID to update'),
firstName: z.string().optional().describe('Customer first name'),
lastName: z.string().optional().describe('Customer last name'),
companyName: z.string().optional().describe('Company name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Primary phone number'),
mobilePhone: z.string().optional().describe('Mobile phone number'),
status: z.enum(['active', 'inactive']).optional().describe('Customer status'),
type: z.enum(['residential', 'commercial']).optional().describe('Customer type'),
address: z.object({
street1: z.string(),
street2: z.string().optional(),
city: z.string(),
state: z.string(),
zip: z.string(),
country: z.string().optional(),
}).optional().describe('Customer address'),
tags: z.array(z.string()).optional().describe('Customer tags'),
notes: z.string().optional().describe('Notes about the customer'),
}),
execute: async (args: any) => {
const { customerId, ...updateData } = args;
const customer = await client.updateCustomer(customerId, updateData);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(customer, null, 2),
},
],
};
},
},
fieldedge_delete_customer: {
description: 'Delete a customer',
parameters: z.object({
customerId: z.string().describe('The customer ID to delete'),
}),
execute: async (args: { customerId: string }) => {
await client.deleteCustomer(args.customerId);
return {
content: [
{
type: 'text' as const,
text: `Customer ${args.customerId} deleted successfully`,
},
],
};
},
},
fieldedge_search_customers: {
description: 'Search for customers by name, email, phone, or other criteria',
parameters: z.object({
query: z.string().describe('Search query'),
}),
execute: async (args: { query: string }) => {
const customers = await client.searchCustomers(args.query);
return {
content: [
{
type: 'text' as const,
text: JSON.stringify(customers, null, 2),
},
],
};
},
},
};
}

View File

@ -1,13 +1,12 @@
/**
* FieldEdge MCP Server Type Definitions
* Comprehensive types for field service management
* FieldEdge API TypeScript Types
* Field service management for HVAC, plumbing, electrical contractors
*/
export interface FieldEdgeConfig {
apiKey: string;
apiUrl?: string;
companyId?: string;
timeout?: number;
baseUrl?: string;
environment?: 'production' | 'sandbox';
}
// Customer Types
@ -20,17 +19,14 @@ export interface Customer {
phone?: string;
mobilePhone?: string;
address?: Address;
billingAddress?: Address;
status: 'active' | 'inactive' | 'prospect';
customerType: 'residential' | 'commercial';
balance: number;
creditLimit?: number;
taxExempt: boolean;
notes?: string;
tags?: string[];
balance?: number;
status: 'active' | 'inactive';
type?: 'residential' | 'commercial';
createdDate?: string;
modifiedDate?: string;
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
tags?: string[];
notes?: string;
}
export interface Address {
@ -50,210 +46,78 @@ export interface Job {
jobNumber: string;
customerId: string;
locationId?: string;
jobType: string;
status: JobStatus;
status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold';
priority: 'low' | 'normal' | 'high' | 'emergency';
description: string;
scheduledStart?: string;
scheduledEnd?: string;
actualStart?: string;
actualEnd?: string;
assignedTechnicians: string[];
equipmentIds?: string[];
subtotal: number;
tax: number;
total: number;
invoiceId?: string;
jobType: string;
description?: string;
scheduledDate?: string;
scheduledStartTime?: string;
scheduledEndTime?: string;
actualStartTime?: string;
actualEndTime?: string;
assignedTechnicianIds?: string[];
estimatedDuration?: number;
tags?: string[];
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
createdDate: string;
modifiedDate?: string;
completedDate?: string;
}
export type JobStatus =
| 'scheduled'
| 'dispatched'
| 'in-progress'
| 'on-hold'
| 'completed'
| 'cancelled'
| 'invoiced';
// Invoice Types
export interface Invoice {
export interface WorkOrder {
id: string;
invoiceNumber: string;
customerId: string;
jobId?: string;
status: InvoiceStatus;
issueDate: string;
dueDate: string;
paidDate?: string;
lineItems: LineItem[];
subtotal: number;
tax: number;
discount: number;
total: number;
amountPaid: number;
balance: number;
paymentTerms?: string;
jobId: string;
workOrderNumber: string;
status: 'open' | 'in_progress' | 'completed' | 'invoiced';
lineItems?: LineItem[];
subtotal?: number;
tax?: number;
total?: number;
notes?: string;
createdAt: string;
updatedAt: string;
createdDate: string;
}
export type InvoiceStatus = 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void';
export interface LineItem {
id: string;
type: 'service' | 'part' | 'equipment' | 'labor';
id?: string;
type: 'labor' | 'material' | 'equipment' | 'service';
description: string;
quantity: number;
unitPrice: number;
discount: number;
tax: number;
total: number;
taxable?: boolean;
itemId?: string;
}
// Estimate Types
export interface Estimate {
id: string;
estimateNumber: string;
customerId: string;
status: EstimateStatus;
issueDate: string;
expiryDate: string;
lineItems: LineItem[];
subtotal: number;
tax: number;
discount: number;
total: number;
approvedDate?: string;
jobId?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export type EstimateStatus = 'draft' | 'sent' | 'viewed' | 'approved' | 'declined' | 'expired';
// Scheduling/Dispatch Types
// Scheduling Types
export interface Appointment {
id: string;
jobId: string;
customerId: string;
technicianId: string;
scheduledDate: string;
startTime: string;
endTime: string;
duration: number;
status: AppointmentStatus;
appointmentType: string;
arrivalWindow?: {
start: string;
end: string;
};
actualArrival?: string;
actualDeparture?: string;
status: 'scheduled' | 'confirmed' | 'en_route' | 'arrived' | 'completed' | 'cancelled';
notes?: string;
createdAt: string;
updatedAt: string;
customerNotified?: boolean;
}
export type AppointmentStatus = 'scheduled' | 'confirmed' | 'dispatched' | 'en-route' | 'arrived' | 'completed' | 'cancelled' | 'no-show';
export interface DispatchBoard {
date: string;
technicians: TechnicianSchedule[];
unassignedJobs: Job[];
}
export interface TechnicianSchedule {
technicianId: string;
technicianName: string;
appointments: Appointment[];
availability: TimeSlot[];
capacity: number;
}
export interface TimeSlot {
start: string;
end: string;
available: boolean;
}
// Inventory Types
export interface InventoryItem {
id: string;
sku: string;
name: string;
description?: string;
category: string;
manufacturer?: string;
modelNumber?: string;
unitOfMeasure: string;
costPrice: number;
sellPrice: number;
msrp?: number;
quantityOnHand: number;
quantityAllocated: number;
quantityAvailable: number;
reorderPoint: number;
reorderQuantity: number;
warehouse?: string;
binLocation?: string;
serialized: boolean;
taxable: boolean;
tags?: string[];
createdAt: string;
updatedAt: string;
}
export interface InventoryTransaction {
id: string;
itemId: string;
type: 'receipt' | 'issue' | 'adjustment' | 'transfer' | 'return';
quantity: number;
unitCost?: number;
totalCost?: number;
reference?: string;
jobId?: string;
technicianId?: string;
notes?: string;
createdAt: string;
}
// Technician Types
export interface Technician {
id: string;
employeeNumber: string;
firstName: string;
lastName: string;
email: string;
phone: string;
status: 'active' | 'inactive' | 'on-leave';
role: string;
skills: string[];
certifications: Certification[];
phone?: string;
status: 'active' | 'inactive';
certifications?: string[];
skills?: string[];
hourlyRate?: number;
overtimeRate?: number;
serviceRadius?: number;
homeAddress?: Address;
vehicleId?: string;
defaultWorkHours?: WorkHours;
createdAt: string;
updatedAt: string;
color?: string; // For calendar display
defaultSchedule?: WeeklySchedule;
avatar?: string;
}
export interface Certification {
name: string;
number: string;
issuer: string;
issueDate: string;
expiryDate?: string;
}
export interface WorkHours {
export interface WeeklySchedule {
monday?: DaySchedule;
tuesday?: DaySchedule;
wednesday?: DaySchedule;
@ -264,339 +128,225 @@ export interface WorkHours {
}
export interface DaySchedule {
start: string;
start: string; // HH:mm format
end: string;
breaks?: TimeSlot[];
breaks?: TimeRange[];
}
export interface TimeRange {
start: string;
end: string;
}
// Invoice Types
export interface Invoice {
id: string;
invoiceNumber: string;
customerId: string;
jobId?: string;
workOrderId?: string;
status: 'draft' | 'sent' | 'viewed' | 'partial' | 'paid' | 'overdue' | 'void';
issueDate: string;
dueDate: string;
lineItems: LineItem[];
subtotal: number;
taxRate?: number;
taxAmount: number;
discountAmount?: number;
total: number;
amountPaid?: number;
balance?: number;
notes?: string;
terms?: string;
createdDate: string;
paidDate?: string;
}
// Payment Types
export interface Payment {
id: string;
invoiceId: string;
customerId: string;
amount: number;
paymentMethod: PaymentMethod;
paymentDate: string;
reference?: string;
cardLast4?: string;
checkNumber?: string;
status: 'pending' | 'processed' | 'failed' | 'refunded';
paymentMethod: 'cash' | 'check' | 'credit_card' | 'ach' | 'other';
referenceNumber?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export type PaymentMethod = 'cash' | 'check' | 'credit-card' | 'debit-card' | 'ach' | 'wire' | 'other';
// Estimate Types
export interface Estimate {
id: string;
estimateNumber: string;
customerId: string;
locationId?: string;
status: 'draft' | 'sent' | 'viewed' | 'accepted' | 'declined' | 'expired';
issueDate: string;
expirationDate?: string;
lineItems: LineItem[];
subtotal: number;
taxRate?: number;
taxAmount: number;
discountAmount?: number;
total: number;
notes?: string;
terms?: string;
acceptedDate?: string;
jobId?: string; // If converted to job
createdDate: string;
}
// Equipment Types
export interface Equipment {
id: string;
customerId: string;
locationId?: string;
type: string;
manufacturer: string;
model: string;
type: string; // HVAC, Plumbing, Electrical
manufacturer?: string;
model?: string;
serialNumber?: string;
installDate?: string;
warrantyExpiry?: string;
warrantyExpiration?: string;
notes?: string;
status: 'active' | 'inactive' | 'retired';
maintenanceSchedule?: MaintenanceSchedule;
lastServiceDate?: string;
nextServiceDue?: string;
status: 'active' | 'inactive' | 'decommissioned';
notes?: string;
customFields?: Record<string, any>;
createdAt: string;
updatedAt: string;
}
export interface ServiceHistory {
export interface MaintenanceSchedule {
frequency: 'monthly' | 'quarterly' | 'semi_annual' | 'annual';
lastServiceDate?: string;
nextServiceDate?: string;
notes?: string;
}
// Inventory Types
export interface InventoryItem {
id: string;
equipmentId: string;
jobId: string;
serviceDate: string;
technicianId: string;
serviceType: string;
description: string;
partsReplaced?: LineItem[];
cost: number;
sku: string;
name: string;
description?: string;
category?: string;
type: 'part' | 'material' | 'equipment';
unitCost: number;
unitPrice: number;
quantityOnHand: number;
reorderPoint?: number;
reorderQuantity?: number;
preferredVendor?: string;
location?: string;
status: 'active' | 'inactive' | 'discontinued';
}
export interface StockAdjustment {
id: string;
itemId: string;
adjustmentType: 'addition' | 'subtraction' | 'transfer' | 'count';
quantity: number;
reason?: string;
technicianId?: string;
jobId?: string;
date: string;
notes?: string;
}
// Location Types
// Reporting Types
export interface RevenueReport {
startDate: string;
endDate: string;
totalRevenue: number;
invoicedAmount: number;
paidAmount: number;
outstandingAmount: number;
byJobType?: Record<string, number>;
byTechnician?: Record<string, number>;
byCustomer?: Record<string, number>;
}
export interface TechnicianPerformance {
technicianId: string;
technicianName: string;
period: string;
jobsCompleted: number;
revenueGenerated: number;
hoursWorked: number;
avgJobDuration: number;
customerRating?: number;
efficiency?: number;
}
export interface CustomerReport {
customerId: string;
totalJobs: number;
totalRevenue: number;
averageInvoice: number;
lastServiceDate?: string;
lifetimeValue: number;
}
// Location Types (for multi-location customers)
export interface Location {
id: string;
customerId: string;
name: string;
address: Address;
type: 'primary' | 'secondary' | 'billing' | 'service';
contactName?: string;
contactPhone?: string;
accessNotes?: string;
gateCode?: string;
equipmentIds?: string[];
status: 'active' | 'inactive';
createdAt: string;
updatedAt: string;
}
// Price Book Types
export interface PriceBook {
id: string;
name: string;
description?: string;
effectiveDate: string;
expiryDate?: string;
isDefault: boolean;
status: 'active' | 'inactive';
items: PriceBookItem[];
createdAt: string;
updatedAt: string;
}
export interface PriceBookItem {
id: string;
itemId: string;
itemType: 'service' | 'part' | 'equipment' | 'labor';
name: string;
sku?: string;
description?: string;
unitPrice: number;
costPrice?: number;
margin?: number;
taxable: boolean;
category?: string;
}
// Service Agreement Types
export interface ServiceAgreement {
id: string;
customerId: string;
agreementNumber: string;
name: string;
type: 'maintenance' | 'warranty' | 'service-plan';
status: 'active' | 'expired' | 'cancelled';
startDate: string;
endDate: string;
renewalDate?: string;
autoRenew: boolean;
billingCycle: 'monthly' | 'quarterly' | 'annual';
amount: number;
services: ServiceItem[];
equipmentIds?: string[];
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface ServiceItem {
id: string;
name: string;
description: string;
frequency: 'weekly' | 'monthly' | 'quarterly' | 'semi-annual' | 'annual';
nextDue?: string;
lastCompleted?: string;
}
// Task Types
export interface Task {
id: string;
jobId?: string;
customerId?: string;
technicianId?: string;
title: string;
description: string;
type: 'call' | 'email' | 'follow-up' | 'inspection' | 'other';
priority: 'low' | 'normal' | 'high' | 'urgent';
status: 'pending' | 'in-progress' | 'completed' | 'cancelled';
dueDate?: string;
completedDate?: string;
assignedTo?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Call Tracking Types
export interface Call {
id: string;
customerId?: string;
phone: string;
direction: 'inbound' | 'outbound';
callType: 'inquiry' | 'booking' | 'follow-up' | 'support' | 'sales';
status: 'answered' | 'missed' | 'voicemail' | 'busy';
duration?: number;
recordingUrl?: string;
notes?: string;
outcome?: string;
jobId?: string;
technicianId?: string;
timestamp: string;
}
// Reporting Types
export interface Report {
id: string;
name: string;
type: ReportType;
parameters?: Record<string, any>;
generatedAt: string;
data: any;
}
export type ReportType =
| 'revenue'
| 'technician-productivity'
| 'job-completion'
| 'customer-satisfaction'
| 'inventory-valuation'
| 'aging-receivables'
| 'sales-by-category'
| 'equipment-maintenance'
| 'custom';
export interface RevenueReport {
period: string;
totalRevenue: number;
invoicedAmount: number;
collectedAmount: number;
outstandingAmount: number;
byCategory: Record<string, number>;
byTechnician: Record<string, number>;
trend: RevenueDataPoint[];
}
export interface RevenueDataPoint {
date: string;
revenue: number;
jobs: number;
}
export interface TechnicianProductivityReport {
period: string;
technicians: TechnicianMetrics[];
}
export interface TechnicianMetrics {
technicianId: string;
technicianName: string;
jobsCompleted: number;
hoursWorked: number;
revenue: number;
averageJobTime: number;
utilizationRate: number;
isPrimary?: boolean;
}
// API Response Types
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}
export interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
data: T[];
totalCount: number;
pageSize: number;
currentPage: number;
totalPages: number;
}
export interface QueryParams {
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
message?: string;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, any>;
}
// Search/Filter Types
export interface CustomerSearchParams {
query?: string;
status?: 'active' | 'inactive';
type?: 'residential' | 'commercial';
tags?: string[];
page?: number;
pageSize?: number;
sortBy?: string;
sortBy?: 'name' | 'createdDate' | 'balance';
sortOrder?: 'asc' | 'desc';
filter?: Record<string, any>;
search?: string;
[key: string]: any; // Allow additional query parameters
}
// Vehicle Types
export interface Vehicle {
id: string;
name: string;
type: 'truck' | 'van' | 'car' | 'trailer';
make: string;
model: string;
year: number;
vin?: string;
licensePlate: string;
status: 'active' | 'maintenance' | 'inactive';
assignedTechnicianId?: string;
mileage?: number;
lastServiceDate?: string;
nextServiceDue?: string;
inventoryItems?: string[];
gpsEnabled: boolean;
createdAt: string;
updatedAt: string;
}
// Time Tracking Types
export interface TimeEntry {
id: string;
technicianId: string;
jobId?: string;
type: 'regular' | 'overtime' | 'travel' | 'break';
startTime: string;
endTime?: string;
duration?: number;
billable: boolean;
approved: boolean;
notes?: string;
createdAt: string;
updatedAt: string;
}
// Forms/Checklist Types
export interface Form {
id: string;
name: string;
description?: string;
type: 'inspection' | 'safety' | 'maintenance' | 'quote' | 'custom';
status: 'active' | 'inactive';
fields: FormField[];
createdAt: string;
updatedAt: string;
}
export interface FormField {
id: string;
label: string;
type: 'text' | 'number' | 'checkbox' | 'select' | 'radio' | 'date' | 'signature' | 'photo';
required: boolean;
options?: string[];
defaultValue?: any;
validationRules?: Record<string, any>;
}
export interface FormSubmission {
id: string;
formId: string;
jobId?: string;
technicianId: string;
export interface JobSearchParams {
customerId?: string;
responses: Record<string, any>;
attachments?: string[];
submittedAt: string;
status?: Job['status'];
priority?: Job['priority'];
technicianId?: string;
startDate?: string;
endDate?: string;
jobType?: string;
page?: number;
pageSize?: number;
}
// Marketing Campaign Types
export interface Campaign {
id: string;
name: string;
type: 'email' | 'sms' | 'direct-mail' | 'social';
status: 'draft' | 'scheduled' | 'active' | 'completed' | 'cancelled';
startDate: string;
export interface InvoiceSearchParams {
customerId?: string;
status?: Invoice['status'];
startDate?: string;
endDate?: string;
targetAudience: string[];
message: string;
sentCount: number;
deliveredCount: number;
openedCount: number;
clickedCount: number;
convertedCount: number;
roi?: number;
createdAt: string;
updatedAt: string;
minAmount?: number;
maxAmount?: number;
page?: number;
pageSize?: number;
}

View File

@ -0,0 +1,503 @@
// FreshDesk API v2 Client with Basic Auth
import type { FreshDeskConfig } from './types/index.js';
export class FreshDeskClient {
private domain: string;
private authHeader: string;
private baseUrl: string;
constructor(config: FreshDeskConfig) {
this.domain = config.domain;
// Basic auth: base64(apiKey:X)
this.authHeader = 'Basic ' + Buffer.from(`${config.apiKey}:X`).toString('base64');
this.baseUrl = `https://${this.domain}.freshdesk.com/api/v2`;
}
private async request<T>(
endpoint: string,
options: RequestInit = {}
): Promise<T> {
const url = `${this.baseUrl}${endpoint}`;
const headers = new Headers(options.headers);
headers.set('Authorization', this.authHeader);
headers.set('Content-Type', 'application/json');
const response = await fetch(url, {
...options,
headers,
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(
`FreshDesk API error (${response.status}): ${errorText}`
);
}
// Handle 204 No Content
if (response.status === 204) {
return {} as T;
}
const data = await response.json();
return data as T;
}
// GET request
async get<T>(endpoint: string, params?: Record<string, any>): Promise<T> {
let url = endpoint;
if (params) {
const searchParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
if (Array.isArray(value)) {
value.forEach((v) => searchParams.append(key, String(v)));
} else {
searchParams.append(key, String(value));
}
}
});
const queryString = searchParams.toString();
if (queryString) {
url += `?${queryString}`;
}
}
return this.request<T>(url, { method: 'GET' });
}
// POST request
async post<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'POST',
body: JSON.stringify(body),
});
}
// PUT request
async put<T>(endpoint: string, body?: any): Promise<T> {
return this.request<T>(endpoint, {
method: 'PUT',
body: JSON.stringify(body),
});
}
// DELETE request
async delete<T>(endpoint: string): Promise<T> {
return this.request<T>(endpoint, { method: 'DELETE' });
}
// TICKETS
async getTickets(params?: any) {
return this.get('/tickets', params);
}
async getTicket(id: number, include?: string[]) {
const params = include ? { include: include.join(',') } : undefined;
return this.get(`/tickets/${id}`, params);
}
async createTicket(data: any) {
return this.post('/tickets', data);
}
async updateTicket(id: number, data: any) {
return this.put(`/tickets/${id}`, data);
}
async deleteTicket(id: number) {
return this.delete(`/tickets/${id}`);
}
async restoreTicket(id: number) {
return this.put(`/tickets/${id}/restore`, {});
}
async getTicketConversations(id: number) {
return this.get(`/tickets/${id}/conversations`);
}
async addTicketReply(id: number, data: any) {
return this.post(`/tickets/${id}/reply`, data);
}
async addTicketNote(id: number, data: any) {
return this.post(`/tickets/${id}/notes`, data);
}
async getTicketTimeEntries(id: number) {
return this.get(`/tickets/${id}/time_entries`);
}
async getTicketSatisfactionRatings(id: number) {
return this.get(`/tickets/${id}/satisfaction_ratings`);
}
async searchTickets(query: string) {
return this.get(`/search/tickets?query="${encodeURIComponent(query)}"`);
}
async filterTickets(query: string, params?: any) {
return this.get(`/tickets/filter?query=${encodeURIComponent(query)}`, params);
}
// CONTACTS
async getContacts(params?: any) {
return this.get('/contacts', params);
}
async getContact(id: number) {
return this.get(`/contacts/${id}`);
}
async createContact(data: any) {
return this.post('/contacts', data);
}
async updateContact(id: number, data: any) {
return this.put(`/contacts/${id}`, data);
}
async deleteContact(id: number) {
return this.delete(`/contacts/${id}`);
}
async makeContactAgent(id: number) {
return this.put(`/contacts/${id}/make_agent`, {});
}
async searchContacts(params: any) {
return this.get('/contacts/autocomplete', params);
}
async filterContacts(query: string) {
return this.get(`/contacts?query=${encodeURIComponent(query)}`);
}
// COMPANIES
async getCompanies(params?: any) {
return this.get('/companies', params);
}
async getCompany(id: number) {
return this.get(`/companies/${id}`);
}
async createCompany(data: any) {
return this.post('/companies', data);
}
async updateCompany(id: number, data: any) {
return this.put(`/companies/${id}`, data);
}
async deleteCompany(id: number) {
return this.delete(`/companies/${id}`);
}
async searchCompanies(name: string) {
return this.get('/companies/autocomplete', { name });
}
async filterCompanies(query: string) {
return this.get(`/companies/filter?query=${encodeURIComponent(query)}`);
}
// AGENTS
async getAgents(params?: any) {
return this.get('/agents', params);
}
async getAgent(id: number) {
return this.get(`/agents/${id}`);
}
async updateAgent(id: number, data: any) {
return this.put(`/agents/${id}`, data);
}
async deleteAgent(id: number) {
return this.delete(`/agents/${id}`);
}
async getCurrentAgent() {
return this.get('/agents/me');
}
// GROUPS
async getGroups(params?: any) {
return this.get('/groups', params);
}
async getGroup(id: number) {
return this.get(`/groups/${id}`);
}
async createGroup(data: any) {
return this.post('/groups', data);
}
async updateGroup(id: number, data: any) {
return this.put(`/groups/${id}`, data);
}
async deleteGroup(id: number) {
return this.delete(`/groups/${id}`);
}
// PRODUCTS
async getProducts(params?: any) {
return this.get('/products', params);
}
async getProduct(id: number) {
return this.get(`/products/${id}`);
}
async createProduct(data: any) {
return this.post('/products', data);
}
async updateProduct(id: number, data: any) {
return this.put(`/products/${id}`, data);
}
async deleteProduct(id: number) {
return this.delete(`/products/${id}`);
}
// ROLES
async getRoles() {
return this.get('/roles');
}
async getRole(id: number) {
return this.get(`/roles/${id}`);
}
// CANNED RESPONSES
async getCannedResponses(params?: any) {
return this.get('/canned_responses', params);
}
async getCannedResponse(id: number) {
return this.get(`/canned_responses/${id}`);
}
async createCannedResponse(data: any) {
return this.post('/canned_responses', data);
}
async updateCannedResponse(id: number, data: any) {
return this.put(`/canned_responses/${id}`, data);
}
async deleteCannedResponse(id: number) {
return this.delete(`/canned_responses/${id}`);
}
async getCannedResponseFolders() {
return this.get('/canned_response_folders');
}
// SOLUTIONS (KB)
async getSolutionCategories(params?: any) {
return this.get('/solutions/categories', params);
}
async getSolutionCategory(id: number) {
return this.get(`/solutions/categories/${id}`);
}
async createSolutionCategory(data: any) {
return this.post('/solutions/categories', data);
}
async updateSolutionCategory(id: number, data: any) {
return this.put(`/solutions/categories/${id}`, data);
}
async deleteSolutionCategory(id: number) {
return this.delete(`/solutions/categories/${id}`);
}
async getSolutionFolders(category_id: number) {
return this.get(`/solutions/categories/${category_id}/folders`);
}
async getSolutionFolder(category_id: number, folder_id: number) {
return this.get(`/solutions/categories/${category_id}/folders/${folder_id}`);
}
async createSolutionFolder(category_id: number, data: any) {
return this.post(`/solutions/categories/${category_id}/folders`, data);
}
async updateSolutionFolder(category_id: number, folder_id: number, data: any) {
return this.put(`/solutions/categories/${category_id}/folders/${folder_id}`, data);
}
async deleteSolutionFolder(category_id: number, folder_id: number) {
return this.delete(`/solutions/categories/${category_id}/folders/${folder_id}`);
}
async getSolutionArticles(folder_id: number, params?: any) {
return this.get(`/solutions/folders/${folder_id}/articles`, params);
}
async getSolutionArticle(id: number) {
return this.get(`/solutions/articles/${id}`);
}
async createSolutionArticle(folder_id: number, data: any) {
return this.post(`/solutions/folders/${folder_id}/articles`, data);
}
async updateSolutionArticle(id: number, data: any) {
return this.put(`/solutions/articles/${id}`, data);
}
async deleteSolutionArticle(id: number) {
return this.delete(`/solutions/articles/${id}`);
}
async searchSolutions(term: string) {
return this.get(`/solutions/articles?term=${encodeURIComponent(term)}`);
}
// FORUMS
async getForumCategories() {
return this.get('/discussions/categories');
}
async getForums(category_id?: number) {
if (category_id) {
return this.get(`/discussions/categories/${category_id}/forums`);
}
return this.get('/discussions/forums');
}
async getForum(id: number) {
return this.get(`/discussions/forums/${id}`);
}
async createForum(data: any) {
return this.post('/discussions/forums', data);
}
async updateForum(id: number, data: any) {
return this.put(`/discussions/forums/${id}`, data);
}
async getForumTopics(forum_id: number, params?: any) {
return this.get(`/discussions/forums/${forum_id}/topics`, params);
}
async getForumTopic(id: number) {
return this.get(`/discussions/topics/${id}`);
}
async createForumTopic(forum_id: number, data: any) {
return this.post(`/discussions/forums/${forum_id}/topics`, data);
}
async updateForumTopic(id: number, data: any) {
return this.put(`/discussions/topics/${id}`, data);
}
async deleteForumTopic(id: number) {
return this.delete(`/discussions/topics/${id}`);
}
async getForumPosts(topic_id: number) {
return this.get(`/discussions/topics/${topic_id}/posts`);
}
async createForumPost(topic_id: number, data: any) {
return this.post(`/discussions/topics/${topic_id}/posts`, data);
}
// SURVEYS
async getSurveys() {
return this.get('/surveys');
}
async getSurveyResults(survey_id: number, params?: any) {
return this.get(`/surveys/${survey_id}/survey_results`, params);
}
async getSatisfactionRatings(params?: any) {
return this.get('/surveys/satisfaction_ratings', params);
}
// BUSINESS HOURS
async getBusinessHours() {
return this.get('/business_hours');
}
async getBusinessHour(id: number) {
return this.get(`/business_hours/${id}`);
}
// SLA POLICIES
async getSLAPolicies() {
return this.get('/sla_policies');
}
async getSLAPolicy(id: number) {
return this.get(`/sla_policies/${id}`);
}
// EMAIL CONFIGS
async getEmailConfigs() {
return this.get('/email_configs');
}
async getEmailConfig(id: number) {
return this.get(`/email_configs/${id}`);
}
// TICKET FIELDS
async getTicketFields() {
return this.get('/ticket_fields');
}
async getTicketField(id: number) {
return this.get(`/ticket_fields/${id}`);
}
// SETTINGS
async getSettings() {
return this.get('/settings/helpdesk');
}
// TIME ENTRIES
async getAllTimeEntries(params?: any) {
return this.get('/time_entries', params);
}
async getTimeEntry(id: number) {
return this.get(`/time_entries/${id}`);
}
async createTimeEntry(data: any) {
return this.post('/time_entries', data);
}
async updateTimeEntry(id: number, data: any) {
return this.put(`/time_entries/${id}`, data);
}
async deleteTimeEntry(id: number) {
return this.delete(`/time_entries/${id}`);
}
async startTimer(id: number) {
return this.put(`/time_entries/${id}/start_timer`, {});
}
async stopTimer(id: number) {
return this.put(`/time_entries/${id}/stop_timer`, {});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,3 @@
# HelpScout API Credentials
# Get these from: https://secure.helpscout.net/apps/
HELPSCOUT_APP_ID=your_app_id_here
HELPSCOUT_APP_SECRET=your_app_secret_here
HELPSCOUT_ACCESS_TOKEN=your_oauth2_access_token_here

View File

@ -1,34 +1,38 @@
{
"name": "@mcpengine/helpscout-server",
"name": "@mcpengine/helpscout-mcp-server",
"version": "1.0.0",
"description": "HelpScout MCP Server - Complete Mailbox API v2 integration",
"description": "Complete HelpScout Mailbox API v2 MCP Server with OAuth2 authentication",
"type": "module",
"main": "dist/main.js",
"bin": {
"helpscout-mcp": "./dist/main.js"
},
"scripts": {
"build": "tsc",
"build": "tsc && npm run copy-apps",
"copy-apps": "mkdir -p dist/apps && cp -r src/apps/*.tsx dist/apps/ 2>/dev/null || true",
"dev": "tsc --watch",
"start": "node dist/main.js",
"prepare": "npm run build"
},
"keywords": [
"mcp",
"helpscout",
"customer-support",
"mailbox",
"conversations"
"mcp",
"support",
"customer-service",
"helpdesk",
"oauth2"
],
"author": "MCPEngine",
"author": "MCP Engine",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.0.4",
"axios": "^1.7.9",
"dotenv": "^16.4.7"
"dotenv": "^16.4.7",
"react": "^18.3.1"
},
"devDependencies": {
"@types/node": "^22.10.2",
"@types/node": "^22.10.5",
"@types/react": "^18.3.18",
"typescript": "^5.7.2"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -1,252 +1,127 @@
// HelpScout API Types
// HelpScout Mailbox API v2 Types
export interface HelpScoutConfig {
appId: string;
appSecret: string;
accessToken?: string;
tokenExpiry?: number;
appId?: string;
appSecret?: string;
accessToken: string;
}
export interface AccessTokenResponse {
access_token: string;
expires_in: number;
token_type: string;
}
export interface PaginatedResponse<T> {
_embedded?: { [key: string]: T[] };
export interface APIError {
message: string;
logref?: string;
_links?: {
about?: { href: string };
};
}
export interface PagedResponse<T> {
_embedded: T;
_links?: {
self?: { href: string };
first?: { href: string };
last?: { href: string };
next?: { href: string };
prev?: { href: string };
self?: { href: string };
page?: { href: string };
};
page?: {
number: number;
size: number;
totalElements: number;
totalPages: number;
number: number;
};
}
// Conversations
export interface Conversation {
id: number;
number: number;
threads: number;
type: 'email' | 'chat' | 'phone';
folderId: number;
status: 'active' | 'pending' | 'closed' | 'spam';
state: 'draft' | 'published' | 'deleted';
status: 'active' | 'closed' | 'open' | 'pending' | 'spam';
state: 'deleted' | 'draft' | 'published';
subject: string;
preview: string;
mailboxId: number;
assignee?: User;
createdBy: User | Customer;
assignee?: Person;
createdBy: Person;
createdAt: string;
closedBy?: number;
closedByUser?: Person;
closedAt?: string;
closedBy?: User;
updatedAt?: string;
userUpdatedAt?: string;
customerWaitingSince?: CustomerWaitingSince;
source?: ConversationSource;
userUpdatedAt: string;
customerWaitingSince?: {
time: string;
friendly: string;
};
source: {
type: string;
via: 'user' | 'customer';
};
tags?: Tag[];
cc?: string[];
bcc?: string[];
primaryCustomer: Customer;
snooze?: {
snoozedBy: number;
snoozedUntil: string;
unsnoozeOnCustomerReply: boolean;
};
nextEvent?: {
time: string;
eventType: 'snooze' | 'scheduled';
userId: number;
cancelOnCustomerReply: boolean;
};
customFields?: CustomField[];
_embedded?: {
threads?: Thread[];
};
_links?: Record<string, { href: string }>;
}
export interface Thread {
id: number;
assignedTo?: User;
status: 'active' | 'nochange' | 'pending' | 'closed';
createdAt: string;
createdBy: User | Customer;
source?: ThreadSource;
type: 'note' | 'message' | 'reply' | 'customer' | 'lineitem' | 'forwardparent' | 'forwardchild' | 'phone';
state: 'draft' | 'published' | 'deleted' | 'hidden';
type: 'note' | 'message' | 'customer' | 'lineitem' | 'chat' | 'phone';
status: 'active' | 'nochange' | 'pending';
state: 'published' | 'draft' | 'deleted' | 'hidden';
action?: {
type: string;
text: string;
};
body: string;
source: {
type: string;
via: string;
};
customer?: Customer;
fromMailbox?: Mailbox;
body?: string;
createdBy: Person;
assignedTo?: Person;
savedReplyId?: number;
to?: string[];
cc?: string[];
bcc?: string[];
attachments?: Attachment[];
savedReplyId?: number;
createdAt: string;
openedAt?: string;
_embedded?: {
attachments?: Attachment[];
};
}
export interface Customer {
id: number;
firstName: string;
lastName: string;
email: string;
phone?: string;
photoUrl?: string;
photoType?: string;
gender?: string;
age?: string;
organization?: string;
jobTitle?: string;
location?: string;
background?: string;
address?: CustomerAddress;
socialProfiles?: SocialProfile[];
chats?: Chat[];
websites?: Website[];
emails?: CustomerEmail[];
phones?: CustomerPhone[];
createdAt: string;
updatedAt: string;
}
export interface CustomerAddress {
id: number;
city: string;
state: string;
postalCode: string;
country: string;
lines: string[];
createdAt: string;
}
export interface CustomerEmail {
id: number;
value: string;
type: string;
location: string;
}
export interface CustomerPhone {
id: number;
value: string;
type: string;
location: string;
}
export interface SocialProfile {
type: string;
value: string;
}
export interface Chat {
id: number;
type: string;
value: string;
}
export interface Website {
id: number;
value: string;
}
export interface Mailbox {
id: number;
name: string;
slug: string;
email: string;
createdAt: string;
updatedAt: string;
}
export interface Folder {
id: number;
name: string;
type: 'mine' | 'drafts' | 'assigned' | 'closed' | 'spam' | 'open';
userId: number;
totalCount: number;
activeCount: number;
updatedAt: string;
}
export interface User {
id: number;
firstName: string;
lastName: string;
email: string;
role: string;
timezone: string;
photoUrl?: string;
createdAt: string;
updatedAt: string;
type?: 'user' | 'team';
}
export interface Tag {
id?: number;
name?: string;
color?: string;
createdAt?: string;
updatedAt?: string;
ticketCount?: number;
}
export interface Workflow {
id: number;
mailboxId: number;
type: 'manual' | 'automatic';
status: 'active' | 'inactive' | 'invalid';
order: number;
name: string;
createdAt: string;
modifiedAt: string;
}
export interface WorkflowStats {
totalRuns: number;
successfulRuns: number;
failedRuns: number;
}
export interface SavedReply {
id: number;
name: string;
export interface CreateThread {
type: 'note' | 'message' | 'customer' | 'reply' | 'forwardCustomer' | 'forwardParent';
text: string;
mailboxId?: number;
userId?: number;
createdAt: string;
updatedAt: string;
}
export interface Team {
id: number;
name: string;
createdAt: string;
updatedAt: string;
memberCount?: number;
}
export interface Webhook {
id: string;
url: string;
secret: string;
events: string[];
state: 'enabled' | 'disabled';
createdAt: string;
}
export interface CustomField {
id: number;
name: string;
value: any;
type: 'TEXT' | 'NUMBER' | 'DATE' | 'DROPDOWN';
}
export interface ConversationSource {
type: string;
via: string;
}
export interface ThreadSource {
type: string;
via: string;
}
export interface CustomerWaitingSince {
time: string;
friendly: string;
latestReplyFrom: 'customer' | 'user';
user?: number;
customer?: number;
imported?: boolean;
createdAt?: string;
status?: 'active' | 'nochange' | 'pending';
to?: string[];
cc?: string[];
bcc?: string[];
attachments?: CreateAttachment[];
draft?: boolean;
}
export interface Attachment {
@ -257,64 +132,369 @@ export interface Attachment {
width?: number;
height?: number;
url: string;
_links?: {
data?: { href: string };
};
}
export interface CreateAttachment {
fileName: string;
mimeType: string;
data: string; // base64
}
// Customers
export interface Customer {
id: number;
type?: 'customer';
first?: string;
last?: string;
email: string;
phone?: string;
photoUrl?: string;
photoType?: 'twitter' | 'facebook' | 'gravatar' | 'google' | 'unknown';
gender?: 'male' | 'female' | 'unknown';
age?: string;
organization?: string;
jobTitle?: string;
location?: string;
createdAt?: string;
updatedAt?: string;
background?: string;
address?: Address;
socialProfiles?: SocialProfile[];
emails?: CustomerEmail[];
phones?: CustomerPhone[];
chats?: CustomerChat[];
websites?: CustomerWebsite[];
_embedded?: {
entries?: CustomerEntry[];
};
_links?: Record<string, { href: string }>;
}
export interface Address {
id?: number;
lines: string[];
city: string;
state: string;
postalCode: string;
country: string;
createdAt?: string;
updatedAt?: string;
}
export interface SocialProfile {
id?: number;
type: 'twitter' | 'facebook' | 'linkedin' | 'aboutme' | 'google' | 'googleplus' | 'tungleme' | 'quora' | 'foursquare' | 'youtube' | 'flickr' | 'other';
value: string;
createdAt?: string;
updatedAt?: string;
}
export interface CustomerEmail {
id?: number;
type: 'home' | 'work' | 'other';
value: string;
createdAt?: string;
updatedAt?: string;
}
export interface CustomerPhone {
id?: number;
type: 'home' | 'work' | 'mobile' | 'fax' | 'pager' | 'other';
value: string;
createdAt?: string;
updatedAt?: string;
}
export interface CustomerChat {
id?: number;
type: 'aim' | 'gtalk' | 'icq' | 'xmpp' | 'msn' | 'skype' | 'yahoo' | 'qq' | 'other';
value: string;
createdAt?: string;
updatedAt?: string;
}
export interface CustomerWebsite {
id?: number;
value: string;
createdAt?: string;
updatedAt?: string;
}
export interface CustomerEntry {
id: number;
type: 'email' | 'phone' | 'chat' | 'website';
value: string;
}
export interface CreateCustomer {
firstName: string;
lastName: string;
email?: string;
phone?: string;
photoUrl?: string;
photoType?: 'twitter' | 'facebook' | 'gravatar' | 'google' | 'unknown';
gender?: 'male' | 'female' | 'unknown';
age?: string;
organization?: string;
jobTitle?: string;
location?: string;
background?: string;
address?: Omit<Address, 'id' | 'createdAt' | 'updatedAt'>;
socialProfiles?: Omit<SocialProfile, 'id' | 'createdAt' | 'updatedAt'>[];
emails?: Omit<CustomerEmail, 'id' | 'createdAt' | 'updatedAt'>[];
phones?: Omit<CustomerPhone, 'id' | 'createdAt' | 'updatedAt'>[];
chats?: Omit<CustomerChat, 'id' | 'createdAt' | 'updatedAt'>[];
websites?: Omit<CustomerWebsite, 'id' | 'createdAt' | 'updatedAt'>[];
}
// Mailboxes
export interface Mailbox {
id: number;
name: string;
slug: string;
email: string;
createdAt: string;
updatedAt: string;
_links?: Record<string, { href: string }>;
}
export interface MailboxFields {
id: number;
name: string;
type: 'SINGLE_LINE' | 'MULTI_LINE' | 'DATE' | 'NUMBER' | 'DROPDOWN';
order: number;
required: boolean;
options?: string[];
_links?: Record<string, { href: string }>;
}
export interface Folder {
id: number;
name: string;
type: 'mytickets' | 'unassigned' | 'drafts' | 'assigned' | 'closed' | 'spam' | 'deleted' | 'mine';
userId?: number;
totalCount: number;
activeCount: number;
updatedAt: string;
_links?: Record<string, { href: string }>;
}
// Users
export interface User {
id: number;
type: 'user' | 'team';
first?: string;
last?: string;
email: string;
role: 'owner' | 'admin' | 'user';
timezone: string;
photoUrl?: string;
createdAt: string;
updatedAt: string;
_links?: Record<string, { href: string }>;
}
export interface Person {
id: number;
type: 'user' | 'customer' | 'team';
first?: string;
last?: string;
email?: string;
photoUrl?: string;
}
// Teams
export interface Team {
id: number;
name: string;
createdAt: string;
updatedAt: string;
_links?: Record<string, { href: string }>;
}
export interface TeamMember {
id: number;
first: string;
last: string;
email: string;
role: 'owner' | 'admin' | 'user';
photoUrl?: string;
_links?: Record<string, { href: string }>;
}
// Tags
export interface Tag {
id: number;
tag: string;
color: string;
createdAt?: string;
updatedAt?: string;
_links?: Record<string, { href: string }>;
}
// Workflows
export interface Workflow {
id: number;
mailboxId: number;
type: 'manual' | 'automatic';
status: 'active' | 'inactive' | 'invalid';
order: number;
name: string;
createdAt: string;
modifiedAt: string;
_links?: Record<string, { href: string }>;
}
// Saved Replies
export interface SavedReply {
id: number;
text: string;
name: string;
_links?: Record<string, { href: string }>;
}
// Webhooks
export interface Webhook {
id: number;
url: string;
state: 'enabled' | 'disabled';
events: string[];
notification: boolean;
payloadVersion: 'V1' | 'V2';
label?: string;
secret?: string;
_links?: Record<string, { href: string }>;
}
export interface CreateWebhook {
url: string;
events: string[];
secret?: string;
}
// Reports
export interface Report {
filterTags?: any;
current?: any;
previous?: any;
deltas?: any;
filterTags?: string[];
}
export interface CompanyReport extends Report {
current?: {
conversationsCreated: number;
conversationsClosed: number;
customersHelped: number;
happinessScore: number;
repliesSent: number;
resolutionTime: number;
firstResponseTime: number;
export interface ConversationReport {
current: ReportMetrics;
previous?: ReportMetrics;
deltas?: {
[key: string]: {
value: number;
percent: number;
};
};
}
export interface ConversationReport extends Report {
current?: {
newConversations: number;
totalConversations: number;
conversationsBusiest: any;
conversationsVolumeByDay: any[];
export interface ReportMetrics {
startDate: string;
endDate: string;
conversations: number;
conversationsCreated: number;
newConversations?: number;
customers?: number;
resolved?: number;
replies?: number;
repliesSent?: number;
resolvedOnFirstReply?: number;
responseTime?: TimeMetrics;
resolutionTime?: TimeMetrics;
firstResponseTime?: TimeMetrics;
}
export interface TimeMetrics {
friendly: string;
seconds: number;
}
export interface UserReport {
user: Person;
current: UserMetrics;
previous?: UserMetrics;
deltas?: Record<string, { value: number; percent: number }>;
}
export interface UserMetrics {
startDate: string;
endDate: string;
totalConversations: number;
conversationsCreated: number;
conversationsResolved: number;
repliesSent: number;
resolvedOnFirstReply?: number;
responseTime?: TimeMetrics;
resolutionTime?: TimeMetrics;
percentResolved?: number;
happiness?: {
score: number;
};
}
export interface HappinessReport extends Report {
current?: {
happinessScore: number;
ratingsCount: number;
greatCount: number;
okayCount: number;
notGoodCount: number;
};
export interface HappinessReport {
current: HappinessMetrics;
previous?: HappinessMetrics;
}
export interface ProductivityReport extends Report {
current?: {
repliesSent: number;
resolutionTime: number;
firstResponseTime: number;
responseTime: number;
resolvedConversations: number;
};
export interface HappinessMetrics {
startDate: string;
endDate: string;
happinessScore: number;
ratingsCount: number;
}
export interface UserReport extends Report {
current?: {
user: User;
repliesSent: number;
conversationsCreated: number;
conversationsClosed: number;
happinessScore: number;
resolutionTime: number;
firstResponseTime: number;
};
// Custom Fields
export interface CustomField {
id: number;
name: string;
value: string | number;
text?: string;
}
// Notes
export interface Note {
text: string;
}
// Search
export interface SearchConversation {
id: number;
number: number;
mailboxid: number;
subject: string;
status: string;
threadCount: number;
preview: string;
customerId: number;
customerEmail: string;
customerName: string;
updatedAt: string;
url?: string;
}
export interface SearchCustomer {
id: number;
firstName: string;
lastName: string;
email: string;
phone?: string;
photoUrl?: string;
url?: string;
}
// Ratings
export interface Rating {
id: number;
customerId: number;
userId?: number;
threadId: number;
rating: 'great' | 'okay' | 'bad';
comments?: string;
createdAt: string;
modifiedAt?: string;
_links?: Record<string, { href: string }>;
}

View File

@ -13,7 +13,8 @@
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
"sourceMap": true,
"jsx": "react"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]

View File

@ -0,0 +1,392 @@
# TouchBistro MCP Server
A comprehensive Model Context Protocol (MCP) server for TouchBistro restaurant management platform, providing **190 tools** across 12 domains and **15 React MCP Apps** for visual management.
## Features
### 🍽️ Core Restaurant Management
- **Orders Management** (15 tools)
- Create, update, track orders with full CRUD operations
- Send to kitchen, manage modifications, void/cancel orders
- Table orders, open orders, customer order history
- **Menu Management** (21 tools)
- Menu items, categories, modifiers with full CRUD
- Bulk updates, availability control
- Modifier groups and custom modifiers
- **Customer Management** (12 tools)
- Customer profiles with preferences and allergens
- VIP tracking, tagging, address management
- Customer statistics and analytics
- **Employee Management** (19 tools)
- Employee profiles and permissions
- Shift scheduling and time clock
- Performance tracking and labor management
- **Table Management** (17 tools)
- Floor plans and table layouts
- Table status tracking (available, occupied, reserved)
- Table merging/splitting, server assignments
### 💰 Financial Operations
- **Payment Processing** (13 tools)
- Multiple payment methods (cash, card, mobile, gift cards)
- Authorization, capture, void, refund operations
- Split payments and tip management
- **Reservations** (15 tools)
- Create, confirm, seat, and track reservations
- Availability checking and reminders
- No-show tracking
- **Loyalty Programs** (10 tools)
- Points earning and redemption
- Birthday bonuses and rewards
- Member analytics and tier management
- **Gift Cards** (13 tools)
- Purchase, reload, redeem gift cards
- Balance checking and email delivery
- Transaction history and sales tracking
### 📊 Inventory & Analytics
- **Inventory Management** (17 tools)
- Stock tracking with par levels
- Purchase orders and receiving
- Low stock alerts and reorder management
- **Reports & Analytics** (24 tools)
- Sales reports (daily, hourly, by category)
- Employee performance and labor costs
- Customer acquisition and retention
- Menu performance and profitability
- Real-time dashboard
- **Discounts & Promotions** (14 tools)
- Create and manage discounts
- Happy hour and daily specials
- Promotion scheduling and tracking
## Installation
```bash
npm install touchbistro-mcp-server
```
## Configuration
Set the following environment variables:
```bash
TOUCHBISTRO_API_KEY=your_api_key
TOUCHBISTRO_RESTAURANT_ID=your_restaurant_id
TOUCHBISTRO_BASE_URL=https://api.touchbistro.com/v1 # Optional
```
## Usage
### As an MCP Server
Add to your Claude Desktop or other MCP client configuration:
```json
{
"mcpServers": {
"touchbistro": {
"command": "touchbistro-mcp-server",
"env": {
"TOUCHBISTRO_API_KEY": "your_api_key",
"TOUCHBISTRO_RESTAURANT_ID": "your_restaurant_id"
}
}
}
}
```
### Direct Usage
```bash
touchbistro-mcp-server
```
## React MCP Apps (15 Apps)
This server includes 15 standalone React applications for visual management:
1. **Orders Dashboard** - Real-time order tracking and management
2. **Menu Manager** - Visual menu builder with drag-and-drop
3. **Reservations Calendar** - Interactive reservation scheduling
4. **Table Layout** - Visual floor plan editor
5. **Customer Directory** - Customer profiles and history
6. **Employee Scheduler** - Shift scheduling and time tracking
7. **Payments Dashboard** - Payment processing and reconciliation
8. **Loyalty Manager** - Loyalty program administration
9. **Gift Card Manager** - Gift card sales and tracking
10. **Inventory Tracker** - Stock levels and purchase orders
11. **Sales Reports** - Interactive sales analytics
12. **Analytics Dashboard** - Business intelligence and KPIs
13. **Discounts Manager** - Promotion creation and tracking
14. **Settings Panel** - Restaurant configuration
15. **Real-time Dashboard** - Live business overview
Each app is:
- Dark theme optimized
- Fully responsive
- Built with React 18 + TypeScript
- Vite-powered for fast development
- Standalone deployable
### Building Apps
```bash
# Build all apps
npm run build
# Build specific app
npm run build:orders-app
npm run build:menu-app
# ... etc
```
## API Structure
### Orders
```typescript
// Create an order
{
"name": "create_order",
"arguments": {
"tableId": "table-123",
"employeeId": "emp-456",
"orderType": "dine_in",
"items": [
{
"menuItemId": "item-789",
"quantity": 2,
"modifiers": [{ "modifierId": "mod-123" }]
}
]
}
}
```
### Menu Management
```typescript
// Create menu item
{
"name": "create_menu_item",
"arguments": {
"name": "Margherita Pizza",
"categoryId": "cat-pizza",
"price": 14.99,
"description": "Classic Italian pizza",
"allergens": ["gluten", "dairy"]
}
}
```
### Reservations
```typescript
// Create reservation
{
"name": "create_reservation",
"arguments": {
"customerName": "John Doe",
"customerPhone": "555-0123",
"partySize": 4,
"date": "2024-12-25",
"time": "19:00"
}
}
```
### Reports
```typescript
// Get sales report
{
"name": "get_sales_report",
"arguments": {
"startDate": "2024-01-01",
"endDate": "2024-01-31"
}
}
```
## Tool Categories
### Orders (15 tools)
- list_orders, get_order, create_order, update_order
- add_order_items, remove_order_item, update_order_item
- send_to_kitchen, complete_order, cancel_order, void_order
- get_table_orders, get_open_orders, search_orders, get_customer_orders
### Menu (21 tools)
- **Items**: list_menu_items, get_menu_item, create_menu_item, update_menu_item, delete_menu_item, bulk_update_menu_items, set_item_availability
- **Categories**: list_menu_categories, get_menu_category, create_menu_category, update_menu_category, delete_menu_category, reorder_menu_categories
- **Modifiers**: list_modifier_groups, get_modifier_group, create_modifier_group, update_modifier_group, delete_modifier_group, add_modifier, update_modifier, delete_modifier
### Customers (12 tools)
- list_customers, get_customer, create_customer, update_customer, delete_customer
- search_customers, add_customer_address, update_customer_preferences
- get_customer_stats, get_vip_customers, tag_customer, untag_customer
### Employees (19 tools)
- **Staff**: list_employees, get_employee, create_employee, update_employee, delete_employee, deactivate_employee, update_employee_permissions
- **Shifts**: list_shifts, get_shift, create_shift, start_shift, end_shift, get_current_shifts
- **Time Clock**: clock_in, clock_out, start_break, end_break, get_time_clock_entries, get_employee_hours
### Tables (17 tools)
- **Tables**: list_tables, get_table, create_table, update_table, delete_table, set_table_status, assign_server, get_available_tables, get_occupied_tables, merge_tables, split_tables
- **Floors**: list_floors, get_floor, create_floor, update_floor, delete_floor, get_floor_layout
### Payments (13 tools)
- list_payments, get_payment, process_payment, authorize_payment, capture_payment
- void_payment, refund_payment, get_payment_refunds, list_refunds
- split_payment, add_tip, get_payment_summary, get_payment_methods_summary
### Reservations (15 tools)
- list_reservations, get_reservation, create_reservation, update_reservation, delete_reservation
- confirm_reservation, cancel_reservation, seat_reservation, mark_no_show, complete_reservation
- get_reservations_by_date, get_upcoming_reservations, search_reservations, send_reminder, get_available_slots
### Loyalty (10 tools)
- get_loyalty_program, update_loyalty_program, get_customer_points
- add_loyalty_points, redeem_loyalty_points, get_loyalty_transactions
- get_loyalty_members, get_top_loyalty_members, award_birthday_points, get_loyalty_analytics
### Gift Cards (13 tools)
- list_gift_cards, get_gift_card, get_gift_card_by_number, create_gift_card
- purchase_gift_card, reload_gift_card, redeem_gift_card, void_gift_card
- activate_gift_card, check_balance, get_gift_card_transactions, send_gift_card_email, get_gift_card_sales
### Inventory (17 tools)
- **Items**: list_inventory_items, get_inventory_item, create_inventory_item, update_inventory_item, delete_inventory_item, adjust_stock, get_stock_adjustments, get_low_stock_items, get_items_to_reorder, get_inventory_valuation
- **Purchase Orders**: list_purchase_orders, get_purchase_order, create_purchase_order, update_purchase_order, send_purchase_order, receive_purchase_order, cancel_purchase_order
### Reports (24 tools)
- **Sales**: get_sales_report, get_daily_sales, get_sales_by_hour, get_sales_by_day, get_sales_by_category, get_sales_by_payment_method
- **Menu**: get_top_selling_items, get_menu_performance, get_menu_item_analytics
- **Staff**: get_employee_performance, get_employee_sales, get_labor_cost_report
- **Customers**: get_customer_analytics, get_customer_acquisition, get_customer_retention
- **Other**: get_tax_report, get_discount_usage, get_tip_report, get_order_type_distribution, get_table_turnover, get_average_order_value, export_report_csv, get_realtime_dashboard, get_profit_loss_report
### Discounts (14 tools)
- **Discounts**: list_discounts, get_discount, create_discount, update_discount, delete_discount, validate_discount_code, apply_discount, remove_discount
- **Promotions**: list_promotions, get_promotion, create_promotion, update_promotion, delete_promotion, get_active_promotions
## Resources
- `touchbistro://config` - View current API configuration
- `touchbistro://docs` - Complete documentation
## Architecture
```
touchbistro/
├── src/
│ ├── clients/
│ │ └── touchbistro.ts # API client with auth & error handling
│ ├── types/
│ │ └── index.ts # TypeScript type definitions
│ ├── tools/
│ │ ├── orders.ts # Order management tools
│ │ ├── menus.ts # Menu management tools
│ │ ├── customers.ts # Customer management tools
│ │ ├── employees.ts # Employee management tools
│ │ ├── tables.ts # Table management tools
│ │ ├── payments.ts # Payment processing tools
│ │ ├── reservations.ts # Reservation tools
│ │ ├── loyalty.ts # Loyalty program tools
│ │ ├── giftcards.ts # Gift card tools
│ │ ├── inventory.ts # Inventory management tools
│ │ ├── reports.ts # Reporting tools
│ │ └── discounts.ts # Discount & promotion tools
│ ├── ui/ # 15 React MCP Apps
│ │ ├── orders-app/
│ │ ├── menu-app/
│ │ ├── reservations-app/
│ │ ├── tables-app/
│ │ ├── customers-app/
│ │ ├── employees-app/
│ │ ├── payments-app/
│ │ ├── loyalty-app/
│ │ ├── giftcards-app/
│ │ ├── inventory-app/
│ │ ├── reports-app/
│ │ ├── analytics-app/
│ │ ├── discounts-app/
│ │ ├── settings-app/
│ │ └── dashboard-app/
│ ├── server.ts # MCP server implementation
│ └── main.ts # Entry point
├── package.json
├── tsconfig.json
└── README.md
```
## Development
```bash
# Install dependencies
npm install
# Build TypeScript
npm run build
# Watch mode for development
npm run dev
# Run the server
npm start
```
## Testing
```bash
# Test connection
echo '{"method":"tools/list"}' | touchbistro-mcp-server
```
## License
MIT
## Support
For issues and questions, please visit the [GitHub repository](https://github.com/BusyBee3333/mcpengine).
## TouchBistro API
This server implements the TouchBistro REST API v1. For API documentation, contact TouchBistro or visit their developer portal.
### API Authentication
TouchBistro uses API key authentication:
- Header: `Authorization: Bearer {api_key}`
- Header: `X-Restaurant-ID: {restaurant_id}`
### Rate Limiting
Please be aware of TouchBistro's API rate limits. This server implements automatic retry with exponential backoff for rate-limited requests.
## Contributing
Contributions welcome! Please submit pull requests to the main repository.
## Changelog
### v1.0.0 (2024)
- Initial release
- 190 tools across 12 domains
- 15 React MCP Apps
- Full CRUD operations for all resources
- Comprehensive reporting and analytics

View File

@ -0,0 +1,623 @@
/**
* TouchBistro API Client
* Production-grade client with auth, rate limiting, retry logic
*/
import type {
TouchBistroConfig,
ApiResponse,
ListParams,
Order,
OrderItem,
MenuItem,
MenuCategory,
Menu,
ModifierGroup,
Table,
Section,
FloorPlan,
Staff,
TimeSheet,
Customer,
LoyaltyTransaction,
Reservation,
InventoryItem,
StockAdjustment,
Supplier,
PurchaseOrder,
Payment,
SalesReport,
InventoryReport,
StaffReport,
} from './types.js';
export class TouchBistroApiClient {
private config: TouchBistroConfig;
private baseUrl: string;
private accessToken: string | null = null;
private tokenExpiry: number = 0;
private rateLimitRemaining: number = 1000;
private rateLimitReset: number = 0;
constructor(config: TouchBistroConfig) {
this.config = config;
this.baseUrl = config.baseUrl || 'https://cloud.touchbistro.com/api/v1';
if (config.sandbox) {
this.baseUrl = 'https://sandbox.touchbistro.com/api/v1';
}
}
// ============================================================================
// Authentication & Request Management
// ============================================================================
private async ensureAuthenticated(): Promise<void> {
if (this.accessToken && Date.now() < this.tokenExpiry) {
return;
}
const response = await fetch(`${this.baseUrl}/auth/token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
client_id: this.config.clientId,
client_secret: this.config.clientSecret,
grant_type: 'client_credentials',
}),
});
if (!response.ok) {
throw new Error(`Authentication failed: ${response.statusText}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.tokenExpiry = Date.now() + (data.expires_in - 60) * 1000; // Refresh 1 min early
}
private async checkRateLimit(): Promise<void> {
if (this.rateLimitRemaining <= 0 && Date.now() < this.rateLimitReset) {
const waitTime = this.rateLimitReset - Date.now();
await new Promise((resolve) => setTimeout(resolve, waitTime));
}
}
private async request<T>(
method: string,
endpoint: string,
body?: any,
retries = 3
): Promise<ApiResponse<T>> {
await this.ensureAuthenticated();
await this.checkRateLimit();
const url = `${this.baseUrl}${endpoint}`;
const headers: Record<string, string> = {
'Authorization': `Bearer ${this.accessToken}`,
'X-Restaurant-ID': this.config.restaurantId,
'Content-Type': 'application/json',
};
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
// Update rate limit tracking
this.rateLimitRemaining = parseInt(
response.headers.get('X-RateLimit-Remaining') || '1000'
);
this.rateLimitReset = parseInt(
response.headers.get('X-RateLimit-Reset') || '0'
);
if (!response.ok) {
if (response.status === 429 && retries > 0) {
// Rate limited, wait and retry
await new Promise((resolve) => setTimeout(resolve, 1000));
return this.request<T>(method, endpoint, body, retries - 1);
}
if (response.status >= 500 && retries > 0) {
// Server error, retry with exponential backoff
await new Promise((resolve) => setTimeout(resolve, (4 - retries) * 1000));
return this.request<T>(method, endpoint, body, retries - 1);
}
const error = await response.json().catch(() => ({
code: 'UNKNOWN_ERROR',
message: response.statusText,
}));
return {
success: false,
error: {
code: error.code || `HTTP_${response.status}`,
message: error.message || response.statusText,
details: error.details,
},
};
}
const data = await response.json();
return {
success: true,
data,
metadata: {
timestamp: new Date().toISOString(),
},
};
} catch (error: any) {
if (retries > 0) {
await new Promise((resolve) => setTimeout(resolve, 1000));
return this.request<T>(method, endpoint, body, retries - 1);
}
return {
success: false,
error: {
code: 'NETWORK_ERROR',
message: error.message,
},
};
}
}
// ============================================================================
// Orders API
// ============================================================================
async getOrders(params?: ListParams): Promise<ApiResponse<Order[]>> {
const queryParams = new URLSearchParams();
if (params?.page) queryParams.set('page', params.page.toString());
if (params?.pageSize) queryParams.set('pageSize', params.pageSize.toString());
if (params?.sortBy) queryParams.set('sortBy', params.sortBy);
if (params?.sortOrder) queryParams.set('sortOrder', params.sortOrder);
if (params?.filter) {
Object.entries(params.filter).forEach(([key, value]) => {
queryParams.set(key, String(value));
});
}
return this.request<Order[]>('GET', `/orders?${queryParams}`);
}
async getOrder(orderId: string): Promise<ApiResponse<Order>> {
return this.request<Order>('GET', `/orders/${orderId}`);
}
async createOrder(order: Partial<Order>): Promise<ApiResponse<Order>> {
return this.request<Order>('POST', '/orders', order);
}
async updateOrder(orderId: string, updates: Partial<Order>): Promise<ApiResponse<Order>> {
return this.request<Order>('PATCH', `/orders/${orderId}`, updates);
}
async addOrderItem(orderId: string, item: Partial<OrderItem>): Promise<ApiResponse<Order>> {
return this.request<Order>('POST', `/orders/${orderId}/items`, item);
}
async updateOrderItem(
orderId: string,
itemId: string,
updates: Partial<OrderItem>
): Promise<ApiResponse<Order>> {
return this.request<Order>('PATCH', `/orders/${orderId}/items/${itemId}`, updates);
}
async removeOrderItem(orderId: string, itemId: string): Promise<ApiResponse<Order>> {
return this.request<Order>('DELETE', `/orders/${orderId}/items/${itemId}`);
}
async sendOrderToKitchen(orderId: string): Promise<ApiResponse<Order>> {
return this.request<Order>('POST', `/orders/${orderId}/send-to-kitchen`);
}
async completeOrder(orderId: string): Promise<ApiResponse<Order>> {
return this.request<Order>('POST', `/orders/${orderId}/complete`);
}
async voidOrder(orderId: string, reason: string): Promise<ApiResponse<Order>> {
return this.request<Order>('POST', `/orders/${orderId}/void`, { reason });
}
async splitOrder(
orderId: string,
splits: { items: string[]; amount?: number }[]
): Promise<ApiResponse<Order[]>> {
return this.request<Order[]>('POST', `/orders/${orderId}/split`, { splits });
}
// ============================================================================
// Menu Management API
// ============================================================================
async getMenuItems(params?: ListParams): Promise<ApiResponse<MenuItem[]>> {
const queryParams = new URLSearchParams();
if (params?.filter) {
Object.entries(params.filter).forEach(([key, value]) => {
queryParams.set(key, String(value));
});
}
return this.request<MenuItem[]>('GET', `/menu/items?${queryParams}`);
}
async getMenuItem(itemId: string): Promise<ApiResponse<MenuItem>> {
return this.request<MenuItem>('GET', `/menu/items/${itemId}`);
}
async createMenuItem(item: Partial<MenuItem>): Promise<ApiResponse<MenuItem>> {
return this.request<MenuItem>('POST', '/menu/items', item);
}
async updateMenuItem(itemId: string, updates: Partial<MenuItem>): Promise<ApiResponse<MenuItem>> {
return this.request<MenuItem>('PATCH', `/menu/items/${itemId}`, updates);
}
async deleteMenuItem(itemId: string): Promise<ApiResponse<void>> {
return this.request<void>('DELETE', `/menu/items/${itemId}`);
}
async getMenuCategories(): Promise<ApiResponse<MenuCategory[]>> {
return this.request<MenuCategory[]>('GET', '/menu/categories');
}
async createMenuCategory(category: Partial<MenuCategory>): Promise<ApiResponse<MenuCategory>> {
return this.request<MenuCategory>('POST', '/menu/categories', category);
}
async updateMenuCategory(
categoryId: string,
updates: Partial<MenuCategory>
): Promise<ApiResponse<MenuCategory>> {
return this.request<MenuCategory>('PATCH', `/menu/categories/${categoryId}`, updates);
}
async deleteMenuCategory(categoryId: string): Promise<ApiResponse<void>> {
return this.request<void>('DELETE', `/menu/categories/${categoryId}`);
}
async getMenus(): Promise<ApiResponse<Menu[]>> {
return this.request<Menu[]>('GET', '/menu/menus');
}
async createMenu(menu: Partial<Menu>): Promise<ApiResponse<Menu>> {
return this.request<Menu>('POST', '/menu/menus', menu);
}
async updateMenu(menuId: string, updates: Partial<Menu>): Promise<ApiResponse<Menu>> {
return this.request<Menu>('PATCH', `/menu/menus/${menuId}`, updates);
}
async getModifierGroups(): Promise<ApiResponse<ModifierGroup[]>> {
return this.request<ModifierGroup[]>('GET', '/menu/modifier-groups');
}
async createModifierGroup(group: Partial<ModifierGroup>): Promise<ApiResponse<ModifierGroup>> {
return this.request<ModifierGroup>('POST', '/menu/modifier-groups', group);
}
// ============================================================================
// Table & Floor Management API
// ============================================================================
async getTables(params?: ListParams): Promise<ApiResponse<Table[]>> {
return this.request<Table[]>('GET', '/tables');
}
async getTable(tableId: string): Promise<ApiResponse<Table>> {
return this.request<Table>('GET', `/tables/${tableId}`);
}
async updateTableStatus(
tableId: string,
status: string,
guestCount?: number
): Promise<ApiResponse<Table>> {
return this.request<Table>('PATCH', `/tables/${tableId}/status`, { status, guestCount });
}
async assignServer(tableId: string, serverId: string): Promise<ApiResponse<Table>> {
return this.request<Table>('PATCH', `/tables/${tableId}/assign`, { serverId });
}
async getSections(): Promise<ApiResponse<Section[]>> {
return this.request<Section[]>('GET', '/sections');
}
async createSection(section: Partial<Section>): Promise<ApiResponse<Section>> {
return this.request<Section>('POST', '/sections', section);
}
async getFloorPlans(): Promise<ApiResponse<FloorPlan[]>> {
return this.request<FloorPlan[]>('GET', '/floor-plans');
}
async getActiveFloorPlan(): Promise<ApiResponse<FloorPlan>> {
return this.request<FloorPlan>('GET', '/floor-plans/active');
}
async createTable(table: Partial<Table>): Promise<ApiResponse<Table>> {
return this.request<Table>('POST', '/tables', table);
}
async updateTable(tableId: string, updates: Partial<Table>): Promise<ApiResponse<Table>> {
return this.request<Table>('PATCH', `/tables/${tableId}`, updates);
}
// ============================================================================
// Staff Management API
// ============================================================================
async getStaff(params?: ListParams): Promise<ApiResponse<Staff[]>> {
return this.request<Staff[]>('GET', '/staff');
}
async getStaffMember(staffId: string): Promise<ApiResponse<Staff>> {
return this.request<Staff>('GET', `/staff/${staffId}`);
}
async createStaffMember(staff: Partial<Staff>): Promise<ApiResponse<Staff>> {
return this.request<Staff>('POST', '/staff', staff);
}
async updateStaffMember(staffId: string, updates: Partial<Staff>): Promise<ApiResponse<Staff>> {
return this.request<Staff>('PATCH', `/staff/${staffId}`, updates);
}
async deactivateStaffMember(staffId: string): Promise<ApiResponse<Staff>> {
return this.request<Staff>('POST', `/staff/${staffId}/deactivate`);
}
async clockIn(staffId: string): Promise<ApiResponse<TimeSheet>> {
return this.request<TimeSheet>('POST', `/staff/${staffId}/clock-in`);
}
async clockOut(staffId: string): Promise<ApiResponse<TimeSheet>> {
return this.request<TimeSheet>('POST', `/staff/${staffId}/clock-out`);
}
async getTimeSheets(params?: {
staffId?: string;
dateFrom?: string;
dateTo?: string;
}): Promise<ApiResponse<TimeSheet[]>> {
const queryParams = new URLSearchParams();
if (params?.staffId) queryParams.set('staffId', params.staffId);
if (params?.dateFrom) queryParams.set('dateFrom', params.dateFrom);
if (params?.dateTo) queryParams.set('dateTo', params.dateTo);
return this.request<TimeSheet[]>('GET', `/timesheets?${queryParams}`);
}
// ============================================================================
// Customer Management API
// ============================================================================
async getCustomers(params?: ListParams): Promise<ApiResponse<Customer[]>> {
const queryParams = new URLSearchParams();
if (params?.filter?.search) {
queryParams.set('search', params.filter.search);
}
return this.request<Customer[]>('GET', `/customers?${queryParams}`);
}
async getCustomer(customerId: string): Promise<ApiResponse<Customer>> {
return this.request<Customer>('GET', `/customers/${customerId}`);
}
async createCustomer(customer: Partial<Customer>): Promise<ApiResponse<Customer>> {
return this.request<Customer>('POST', '/customers', customer);
}
async updateCustomer(
customerId: string,
updates: Partial<Customer>
): Promise<ApiResponse<Customer>> {
return this.request<Customer>('PATCH', `/customers/${customerId}`, updates);
}
async searchCustomers(query: string): Promise<ApiResponse<Customer[]>> {
return this.request<Customer[]>('GET', `/customers/search?q=${encodeURIComponent(query)}`);
}
async getLoyaltyTransactions(customerId: string): Promise<ApiResponse<LoyaltyTransaction[]>> {
return this.request<LoyaltyTransaction[]>('GET', `/customers/${customerId}/loyalty`);
}
async addLoyaltyPoints(
customerId: string,
points: number,
description: string
): Promise<ApiResponse<LoyaltyTransaction>> {
return this.request<LoyaltyTransaction>('POST', `/customers/${customerId}/loyalty`, {
points,
description,
type: 'earned',
});
}
async redeemLoyaltyPoints(
customerId: string,
points: number,
description: string
): Promise<ApiResponse<LoyaltyTransaction>> {
return this.request<LoyaltyTransaction>('POST', `/customers/${customerId}/loyalty`, {
points: -points,
description,
type: 'redeemed',
});
}
// ============================================================================
// Reservations API
// ============================================================================
async getReservations(params?: {
dateFrom?: string;
dateTo?: string;
status?: string;
}): Promise<ApiResponse<Reservation[]>> {
const queryParams = new URLSearchParams();
if (params?.dateFrom) queryParams.set('dateFrom', params.dateFrom);
if (params?.dateTo) queryParams.set('dateTo', params.dateTo);
if (params?.status) queryParams.set('status', params.status);
return this.request<Reservation[]>('GET', `/reservations?${queryParams}`);
}
async getReservation(reservationId: string): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('GET', `/reservations/${reservationId}`);
}
async createReservation(reservation: Partial<Reservation>): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('POST', '/reservations', reservation);
}
async updateReservation(
reservationId: string,
updates: Partial<Reservation>
): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('PATCH', `/reservations/${reservationId}`, updates);
}
async confirmReservation(reservationId: string): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('POST', `/reservations/${reservationId}/confirm`);
}
async seatReservation(reservationId: string, tableId: string): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('POST', `/reservations/${reservationId}/seat`, { tableId });
}
async cancelReservation(reservationId: string, reason?: string): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('POST', `/reservations/${reservationId}/cancel`, { reason });
}
async markNoShow(reservationId: string): Promise<ApiResponse<Reservation>> {
return this.request<Reservation>('POST', `/reservations/${reservationId}/no-show`);
}
// ============================================================================
// Inventory Management API
// ============================================================================
async getInventoryItems(params?: ListParams): Promise<ApiResponse<InventoryItem[]>> {
return this.request<InventoryItem[]>('GET', '/inventory/items');
}
async getInventoryItem(itemId: string): Promise<ApiResponse<InventoryItem>> {
return this.request<InventoryItem>('GET', `/inventory/items/${itemId}`);
}
async createInventoryItem(item: Partial<InventoryItem>): Promise<ApiResponse<InventoryItem>> {
return this.request<InventoryItem>('POST', '/inventory/items', item);
}
async updateInventoryItem(
itemId: string,
updates: Partial<InventoryItem>
): Promise<ApiResponse<InventoryItem>> {
return this.request<InventoryItem>('PATCH', `/inventory/items/${itemId}`, updates);
}
async adjustStock(
itemId: string,
quantity: number,
type: 'addition' | 'subtraction' | 'correction',
reason: string
): Promise<ApiResponse<StockAdjustment>> {
return this.request<StockAdjustment>('POST', `/inventory/items/${itemId}/adjust`, {
quantity,
type,
reason,
});
}
async getLowStockItems(): Promise<ApiResponse<InventoryItem[]>> {
return this.request<InventoryItem[]>('GET', '/inventory/items/low-stock');
}
async getSuppliers(): Promise<ApiResponse<Supplier[]>> {
return this.request<Supplier[]>('GET', '/inventory/suppliers');
}
async createSupplier(supplier: Partial<Supplier>): Promise<ApiResponse<Supplier>> {
return this.request<Supplier>('POST', '/inventory/suppliers', supplier);
}
async getPurchaseOrders(params?: { status?: string }): Promise<ApiResponse<PurchaseOrder[]>> {
const queryParams = new URLSearchParams();
if (params?.status) queryParams.set('status', params.status);
return this.request<PurchaseOrder[]>('GET', `/inventory/purchase-orders?${queryParams}`);
}
async createPurchaseOrder(order: Partial<PurchaseOrder>): Promise<ApiResponse<PurchaseOrder>> {
return this.request<PurchaseOrder>('POST', '/inventory/purchase-orders', order);
}
async receivePurchaseOrder(orderId: string): Promise<ApiResponse<PurchaseOrder>> {
return this.request<PurchaseOrder>('POST', `/inventory/purchase-orders/${orderId}/receive`);
}
// ============================================================================
// Payment Processing API
// ============================================================================
async processPayment(
orderId: string,
payment: Partial<Payment>
): Promise<ApiResponse<Payment>> {
return this.request<Payment>('POST', `/orders/${orderId}/payments`, payment);
}
async refundPayment(paymentId: string, amount?: number): Promise<ApiResponse<Payment>> {
return this.request<Payment>('POST', `/payments/${paymentId}/refund`, { amount });
}
async voidPayment(paymentId: string): Promise<ApiResponse<Payment>> {
return this.request<Payment>('POST', `/payments/${paymentId}/void`);
}
async getPayments(orderId: string): Promise<ApiResponse<Payment[]>> {
return this.request<Payment[]>('GET', `/orders/${orderId}/payments`);
}
// ============================================================================
// Reporting & Analytics API
// ============================================================================
async getSalesReport(dateFrom: string, dateTo: string): Promise<ApiResponse<SalesReport>> {
return this.request<SalesReport>(
'GET',
`/reports/sales?dateFrom=${dateFrom}&dateTo=${dateTo}`
);
}
async getInventoryReport(): Promise<ApiResponse<InventoryReport>> {
return this.request<InventoryReport>('GET', '/reports/inventory');
}
async getStaffReport(dateFrom: string, dateTo: string): Promise<ApiResponse<StaffReport>> {
return this.request<StaffReport>(
'GET',
`/reports/staff?dateFrom=${dateFrom}&dateTo=${dateTo}`
);
}
async getTopSellingItems(limit: number = 10): Promise<ApiResponse<any[]>> {
return this.request<any[]>('GET', `/reports/top-items?limit=${limit}`);
}
async getRevenueByHour(date: string): Promise<ApiResponse<any[]>> {
return this.request<any[]>('GET', `/reports/revenue-by-hour?date=${date}`);
}
async getCustomerAnalytics(): Promise<ApiResponse<any>> {
return this.request<any>('GET', '/reports/customer-analytics');
}
}
export default TouchBistroApiClient;

View File

@ -0,0 +1,686 @@
/**
* TouchBistro MCP Server - Complete Type Definitions
* Full production-grade types for restaurant POS operations
*/
// ============================================================================
// Core Domain Types
// ============================================================================
export interface TouchBistroConfig {
apiKey: string;
clientId: string;
clientSecret: string;
restaurantId: string;
baseUrl?: string;
sandbox?: boolean;
}
// ============================================================================
// Menu Management
// ============================================================================
export interface MenuItem {
id: string;
name: string;
description?: string;
price: number;
cost?: number;
categoryId: string;
sku?: string;
barcode?: string;
taxable: boolean;
available: boolean;
prepTime?: number; // minutes
modifierGroupIds?: string[];
allergens?: string[];
nutritionInfo?: NutritionInfo;
image?: string;
sortOrder: number;
createdAt: string;
updatedAt: string;
}
export interface MenuCategory {
id: string;
name: string;
description?: string;
parentCategoryId?: string;
sortOrder: number;
available: boolean;
menuIds: string[];
itemCount: number;
createdAt: string;
updatedAt: string;
}
export interface Menu {
id: string;
name: string;
description?: string;
startTime?: string; // HH:mm
endTime?: string; // HH:mm
daysOfWeek?: number[]; // 0-6
available: boolean;
categoryIds: string[];
createdAt: string;
updatedAt: string;
}
export interface ModifierGroup {
id: string;
name: string;
minSelections: number;
maxSelections: number;
required: boolean;
modifiers: Modifier[];
sortOrder: number;
}
export interface Modifier {
id: string;
name: string;
price: number;
available: boolean;
sortOrder: number;
}
export interface NutritionInfo {
calories?: number;
protein?: number;
carbohydrates?: number;
fat?: number;
fiber?: number;
sodium?: number;
}
// ============================================================================
// Order Management
// ============================================================================
export interface Order {
id: string;
orderNumber: string;
status: OrderStatus;
type: OrderType;
tableId?: string;
customerId?: string;
items: OrderItem[];
subtotal: number;
tax: number;
tip: number;
discount: number;
total: number;
payments: Payment[];
serverId: string;
guestCount: number;
notes?: string;
specialInstructions?: string;
sentToKitchen: boolean;
kitchenSentAt?: string;
createdAt: string;
updatedAt: string;
completedAt?: string;
voidedAt?: string;
voidReason?: string;
}
export enum OrderStatus {
Draft = 'draft',
Pending = 'pending',
InProgress = 'in_progress',
Ready = 'ready',
Completed = 'completed',
Voided = 'voided',
Cancelled = 'cancelled'
}
export enum OrderType {
DineIn = 'dine_in',
Takeout = 'takeout',
Delivery = 'delivery',
Curbside = 'curbside',
DriveThru = 'drive_thru'
}
export interface OrderItem {
id: string;
menuItemId: string;
name: string;
quantity: number;
price: number;
modifiers: OrderModifier[];
specialInstructions?: string;
sentToKitchen: boolean;
preparedBy?: string;
preparedAt?: string;
seat?: number;
courseNumber?: number;
}
export interface OrderModifier {
id: string;
name: string;
price: number;
}
// ============================================================================
// Payment Processing
// ============================================================================
export interface Payment {
id: string;
orderId: string;
type: PaymentType;
method: PaymentMethod;
amount: number;
tip: number;
processorFee?: number;
status: PaymentStatus;
cardLast4?: string;
cardBrand?: string;
transactionId?: string;
authCode?: string;
processedAt?: string;
refundedAt?: string;
refundAmount?: number;
createdAt: string;
}
export enum PaymentType {
Full = 'full',
Partial = 'partial',
Split = 'split',
Refund = 'refund'
}
export enum PaymentMethod {
Cash = 'cash',
CreditCard = 'credit_card',
DebitCard = 'debit_card',
GiftCard = 'gift_card',
MobilePayment = 'mobile_payment',
Check = 'check',
HouseAccount = 'house_account'
}
export enum PaymentStatus {
Pending = 'pending',
Authorized = 'authorized',
Captured = 'captured',
Declined = 'declined',
Refunded = 'refunded',
Voided = 'voided'
}
// ============================================================================
// Table & Floor Management
// ============================================================================
export interface Table {
id: string;
number: string;
name?: string;
sectionId: string;
capacity: number;
shape: TableShape;
status: TableStatus;
currentOrderId?: string;
serverId?: string;
guestCount?: number;
seatedAt?: string;
positionX: number;
positionY: number;
rotation?: number;
}
export enum TableShape {
Square = 'square',
Rectangle = 'rectangle',
Circle = 'circle',
Oval = 'oval'
}
export enum TableStatus {
Available = 'available',
Occupied = 'occupied',
Reserved = 'reserved',
Cleaning = 'cleaning',
OutOfService = 'out_of_service'
}
export interface Section {
id: string;
name: string;
floorPlanId: string;
color?: string;
assignedServerIds: string[];
tableCount: number;
}
export interface FloorPlan {
id: string;
name: string;
active: boolean;
sections: Section[];
tableCount: number;
createdAt: string;
updatedAt: string;
}
// ============================================================================
// Staff Management
// ============================================================================
export interface Staff {
id: string;
employeeNumber: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
role: StaffRole;
pinCode: string;
active: boolean;
hireDate: string;
terminationDate?: string;
wage?: number;
wageType?: WageType;
permissions: StaffPermission[];
assignedSections?: string[];
createdAt: string;
updatedAt: string;
}
export enum StaffRole {
Owner = 'owner',
Manager = 'manager',
Server = 'server',
Bartender = 'bartender',
Host = 'host',
Chef = 'chef',
Cook = 'cook',
Busser = 'busser',
Cashier = 'cashier'
}
export enum WageType {
Hourly = 'hourly',
Salary = 'salary',
TippedHourly = 'tipped_hourly'
}
export interface StaffPermission {
resource: string;
actions: ('create' | 'read' | 'update' | 'delete' | 'void')[];
}
export interface TimeSheet {
id: string;
staffId: string;
clockIn: string;
clockOut?: string;
breakMinutes: number;
totalHours?: number;
wage: number;
tips: number;
date: string;
notes?: string;
}
// ============================================================================
// Customer Management
// ============================================================================
export interface Customer {
id: string;
firstName: string;
lastName: string;
email?: string;
phone?: string;
birthday?: string;
notes?: string;
loyaltyPoints: number;
loyaltyTier?: string;
totalSpent: number;
visitCount: number;
averageSpend: number;
lastVisit?: string;
preferredServer?: string;
dietaryRestrictions?: string[];
allergies?: string[];
tags?: string[];
createdAt: string;
updatedAt: string;
}
export interface LoyaltyTransaction {
id: string;
customerId: string;
orderId?: string;
points: number;
type: 'earned' | 'redeemed' | 'adjusted';
description: string;
createdAt: string;
}
// ============================================================================
// Reservations
// ============================================================================
export interface Reservation {
id: string;
customerId?: string;
customerName: string;
customerPhone?: string;
customerEmail?: string;
partySize: number;
date: string;
time: string;
duration: number; // minutes
status: ReservationStatus;
tableId?: string;
sectionId?: string;
notes?: string;
specialRequests?: string;
confirmationSent: boolean;
reminderSent: boolean;
seatedAt?: string;
completedAt?: string;
noShowAt?: string;
cancelledAt?: string;
createdAt: string;
updatedAt: string;
}
export enum ReservationStatus {
Pending = 'pending',
Confirmed = 'confirmed',
Seated = 'seated',
Completed = 'completed',
NoShow = 'no_show',
Cancelled = 'cancelled'
}
// ============================================================================
// Inventory Management
// ============================================================================
export interface InventoryItem {
id: string;
name: string;
category: string;
unit: string;
currentStock: number;
parLevel: number;
reorderPoint: number;
reorderQuantity: number;
unitCost: number;
supplierId?: string;
sku?: string;
barcode?: string;
location?: string;
expirationDate?: string;
lastRestocked?: string;
createdAt: string;
updatedAt: string;
}
export interface StockAdjustment {
id: string;
inventoryItemId: string;
quantity: number;
type: 'addition' | 'subtraction' | 'correction';
reason: string;
performedBy: string;
cost?: number;
createdAt: string;
}
export interface Supplier {
id: string;
name: string;
contactName?: string;
email?: string;
phone?: string;
address?: string;
paymentTerms?: string;
notes?: string;
active: boolean;
createdAt: string;
updatedAt: string;
}
export interface PurchaseOrder {
id: string;
supplierId: string;
orderNumber: string;
status: 'draft' | 'sent' | 'received' | 'cancelled';
items: PurchaseOrderItem[];
subtotal: number;
tax: number;
total: number;
expectedDate?: string;
receivedDate?: string;
notes?: string;
createdAt: string;
updatedAt: string;
}
export interface PurchaseOrderItem {
inventoryItemId: string;
quantity: number;
unitCost: number;
total: number;
}
// ============================================================================
// Reporting & Analytics
// ============================================================================
export interface SalesReport {
startDate: string;
endDate: string;
totalSales: number;
totalOrders: number;
averageOrderValue: number;
totalTax: number;
totalTips: number;
totalDiscounts: number;
grossSales: number;
netSales: number;
salesByHour: HourlySales[];
salesByCategory: CategorySales[];
salesByMenuItem: MenuItemSales[];
salesByPaymentMethod: PaymentMethodSales[];
salesByServer: ServerSales[];
orderTypeMix: OrderTypeMix[];
}
export interface HourlySales {
hour: number;
sales: number;
orders: number;
averageValue: number;
}
export interface CategorySales {
categoryId: string;
categoryName: string;
quantity: number;
revenue: number;
percentage: number;
}
export interface MenuItemSales {
itemId: string;
itemName: string;
quantity: number;
revenue: number;
cost?: number;
profit?: number;
margin?: number;
}
export interface PaymentMethodSales {
method: PaymentMethod;
count: number;
total: number;
percentage: number;
}
export interface ServerSales {
serverId: string;
serverName: string;
orders: number;
sales: number;
tips: number;
averageCheck: number;
}
export interface OrderTypeMix {
type: OrderType;
count: number;
revenue: number;
percentage: number;
}
export interface InventoryReport {
totalValue: number;
lowStockItems: number;
outOfStockItems: number;
expiringItems: number;
itemsByCategory: {
category: string;
count: number;
value: number;
}[];
}
export interface StaffReport {
totalHours: number;
totalWages: number;
totalTips: number;
laborCostPercentage: number;
staffPerformance: StaffPerformance[];
}
export interface StaffPerformance {
staffId: string;
staffName: string;
hours: number;
sales: number;
ordersServed: number;
salesPerHour: number;
averageCheck: number;
}
// ============================================================================
// API Response Types
// ============================================================================
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: ApiError;
metadata?: ResponseMetadata;
}
export interface ApiError {
code: string;
message: string;
details?: Record<string, any>;
}
export interface ResponseMetadata {
page?: number;
pageSize?: number;
totalCount?: number;
totalPages?: number;
timestamp: string;
}
export interface ListParams {
page?: number;
pageSize?: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
filter?: Record<string, any>;
}
// ============================================================================
// MCP App State Types
// ============================================================================
export interface AppState {
loading: boolean;
error: string | null;
lastUpdated: string | null;
}
export interface OrdersAppState extends AppState {
orders: Order[];
selectedOrder: Order | null;
filters: OrderFilters;
}
export interface OrderFilters {
status?: OrderStatus[];
type?: OrderType[];
serverId?: string;
dateFrom?: string;
dateTo?: string;
}
export interface MenuAppState extends AppState {
items: MenuItem[];
categories: MenuCategory[];
menus: Menu[];
selectedItem: MenuItem | null;
editMode: boolean;
}
export interface TablesAppState extends AppState {
tables: Table[];
sections: Section[];
selectedTable: Table | null;
floorPlan: FloorPlan | null;
}
export interface StaffAppState extends AppState {
staff: Staff[];
selectedStaff: Staff | null;
timesheets: TimeSheet[];
}
export interface CustomersAppState extends AppState {
customers: Customer[];
selectedCustomer: Customer | null;
searchQuery: string;
}
export interface ReservationsAppState extends AppState {
reservations: Reservation[];
selectedReservation: Reservation | null;
filters: ReservationFilters;
}
export interface ReservationFilters {
status?: ReservationStatus[];
dateFrom?: string;
dateTo?: string;
}
export interface InventoryAppState extends AppState {
items: InventoryItem[];
lowStockItems: InventoryItem[];
suppliers: Supplier[];
selectedItem: InventoryItem | null;
}
export interface ReportsAppState extends AppState {
salesReport: SalesReport | null;
inventoryReport: InventoryReport | null;
staffReport: StaffReport | null;
dateRange: { start: string; end: string };
}

View File

@ -0,0 +1,223 @@
/**
* Customer Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const customerTools = {
touchbistro_list_customers: {
description: 'List all customers with loyalty points, visit history, and preferences.',
parameters: z.object({
search: z.string().optional().describe('Search by name, email, or phone'),
page: z.number().optional().describe('Page number'),
pageSize: z.number().optional().describe('Items per page'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const filter: any = {};
if (params.search) filter.search = params.search;
const response = await client.getCustomers({
page: params.page,
pageSize: params.pageSize,
filter,
});
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch customers');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_customer: {
description: 'Get detailed customer information including loyalty, visit history, and preferences.',
parameters: z.object({
customerId: z.string().describe('Customer ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getCustomer(params.customerId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch customer');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_customer: {
description: 'Create a new customer profile with contact information and preferences.',
parameters: z.object({
firstName: z.string().describe('First name'),
lastName: z.string().describe('Last name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
birthday: z.string().optional().describe('Birthday (ISO date format)'),
notes: z.string().optional().describe('Customer notes'),
dietaryRestrictions: z.array(z.string()).optional().describe('Dietary restrictions'),
allergies: z.array(z.string()).optional().describe('Allergies'),
tags: z.array(z.string()).optional().describe('Customer tags/labels'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createCustomer(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create customer');
}
return {
content: [
{
type: 'text',
text: `Customer created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_customer: {
description: 'Update customer information, preferences, or notes.',
parameters: z.object({
customerId: z.string().describe('Customer ID'),
firstName: z.string().optional().describe('First name'),
lastName: z.string().optional().describe('Last name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
birthday: z.string().optional().describe('Birthday (ISO date format)'),
notes: z.string().optional().describe('Customer notes'),
dietaryRestrictions: z.array(z.string()).optional().describe('Dietary restrictions'),
allergies: z.array(z.string()).optional().describe('Allergies'),
tags: z.array(z.string()).optional().describe('Customer tags/labels'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { customerId, ...updates } = params;
const response = await client.updateCustomer(customerId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update customer');
}
return {
content: [
{
type: 'text',
text: `Customer updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_search_customers: {
description: 'Search customers by name, email, phone, or tags.',
parameters: z.object({
query: z.string().describe('Search query'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.searchCustomers(params.query);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to search customers');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_loyalty_transactions: {
description: 'Get customer loyalty point transaction history.',
parameters: z.object({
customerId: z.string().describe('Customer ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getLoyaltyTransactions(params.customerId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch loyalty transactions');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_add_loyalty_points: {
description: 'Add loyalty points to a customer account.',
parameters: z.object({
customerId: z.string().describe('Customer ID'),
points: z.number().describe('Points to add'),
description: z.string().describe('Description/reason for points'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.addLoyaltyPoints(params.customerId, params.points, params.description);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to add loyalty points');
}
return {
content: [
{
type: 'text',
text: `${params.points} loyalty points added!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_redeem_loyalty_points: {
description: 'Redeem loyalty points from a customer account.',
parameters: z.object({
customerId: z.string().describe('Customer ID'),
points: z.number().describe('Points to redeem'),
description: z.string().describe('Description of redemption'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.redeemLoyaltyPoints(params.customerId, params.points, params.description);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to redeem loyalty points');
}
return {
content: [
{
type: 'text',
text: `${params.points} loyalty points redeemed!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,290 @@
/**
* Inventory Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const inventoryTools = {
touchbistro_list_inventory_items: {
description: 'List all inventory items with stock levels, costs, and suppliers.',
parameters: z.object({
category: z.string().optional().describe('Filter by category'),
lowStock: z.boolean().optional().describe('Show only low stock items'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
if (params.lowStock) {
const response = await client.getLowStockItems();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch low stock items');
}
return {
content: [{ type: 'text', text: JSON.stringify(response.data, null, 2) }],
};
}
const filter: any = {};
if (params.category) filter.category = params.category;
const response = await client.getInventoryItems({ filter });
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch inventory items');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_inventory_item: {
description: 'Get detailed information about a specific inventory item.',
parameters: z.object({
itemId: z.string().describe('Inventory item ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getInventoryItem(params.itemId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch inventory item');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_inventory_item: {
description: 'Create a new inventory item with stock tracking.',
parameters: z.object({
name: z.string().describe('Item name'),
category: z.string().describe('Item category'),
unit: z.string().describe('Unit of measurement (e.g., lb, oz, each)'),
currentStock: z.number().describe('Current stock quantity'),
parLevel: z.number().describe('Par level (target stock)'),
reorderPoint: z.number().describe('Reorder point (minimum before restocking)'),
reorderQuantity: z.number().describe('Default reorder quantity'),
unitCost: z.number().describe('Cost per unit'),
supplierId: z.string().optional().describe('Supplier ID'),
sku: z.string().optional().describe('SKU'),
barcode: z.string().optional().describe('Barcode'),
location: z.string().optional().describe('Storage location'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createInventoryItem(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create inventory item');
}
return {
content: [
{
type: 'text',
text: `Inventory item created!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_inventory_item: {
description: 'Update inventory item details.',
parameters: z.object({
itemId: z.string().describe('Inventory item ID'),
name: z.string().optional().describe('Item name'),
category: z.string().optional().describe('Item category'),
parLevel: z.number().optional().describe('Par level'),
reorderPoint: z.number().optional().describe('Reorder point'),
reorderQuantity: z.number().optional().describe('Reorder quantity'),
unitCost: z.number().optional().describe('Cost per unit'),
supplierId: z.string().optional().describe('Supplier ID'),
location: z.string().optional().describe('Storage location'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { itemId, ...updates } = params;
const response = await client.updateInventoryItem(itemId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update inventory item');
}
return {
content: [
{
type: 'text',
text: `Inventory item updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_adjust_stock: {
description: 'Adjust inventory stock levels (add, subtract, or correct).',
parameters: z.object({
itemId: z.string().describe('Inventory item ID'),
quantity: z.number().describe('Quantity to adjust (positive or negative)'),
type: z.enum(['addition', 'subtraction', 'correction']).describe('Adjustment type'),
reason: z.string().describe('Reason for adjustment'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.adjustStock(params.itemId, params.quantity, params.type, params.reason);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to adjust stock');
}
return {
content: [
{
type: 'text',
text: `Stock adjusted!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_list_suppliers: {
description: 'List all suppliers with contact information.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getSuppliers();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch suppliers');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_supplier: {
description: 'Create a new supplier.',
parameters: z.object({
name: z.string().describe('Supplier name'),
contactName: z.string().optional().describe('Contact person name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
address: z.string().optional().describe('Address'),
paymentTerms: z.string().optional().describe('Payment terms'),
notes: z.string().optional().describe('Notes'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createSupplier(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create supplier');
}
return {
content: [
{
type: 'text',
text: `Supplier created!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_list_purchase_orders: {
description: 'List purchase orders with filters for status.',
parameters: z.object({
status: z.enum(['draft', 'sent', 'received', 'cancelled']).optional().describe('Filter by status'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getPurchaseOrders(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch purchase orders');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_purchase_order: {
description: 'Create a new purchase order for a supplier.',
parameters: z.object({
supplierId: z.string().describe('Supplier ID'),
items: z
.array(
z.object({
inventoryItemId: z.string(),
quantity: z.number(),
unitCost: z.number(),
})
)
.describe('Items to order'),
expectedDate: z.string().optional().describe('Expected delivery date (ISO format)'),
notes: z.string().optional().describe('Order notes'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createPurchaseOrder(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create purchase order');
}
return {
content: [
{
type: 'text',
text: `Purchase order created!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_receive_purchase_order: {
description: 'Mark a purchase order as received and update inventory.',
parameters: z.object({
orderId: z.string().describe('Purchase order ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.receivePurchaseOrder(params.orderId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to receive purchase order');
}
return {
content: [
{
type: 'text',
text: `Purchase order received!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,386 @@
/**
* Menu Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const menuTools = {
touchbistro_list_menu_items: {
description:
'List all menu items with optional filters (category, availability, search). Returns prices, descriptions, and modifiers.',
parameters: z.object({
categoryId: z.string().optional().describe('Filter by category ID'),
available: z.boolean().optional().describe('Filter by availability'),
search: z.string().optional().describe('Search by name or description'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const filter: any = {};
if (params.categoryId) filter.categoryId = params.categoryId;
if (params.available !== undefined) filter.available = params.available;
if (params.search) filter.search = params.search;
const response = await client.getMenuItems({ filter });
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch menu items');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_menu_item: {
description: 'Get detailed information about a specific menu item including pricing, modifiers, and nutrition.',
parameters: z.object({
itemId: z.string().describe('Menu item ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getMenuItem(params.itemId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch menu item');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_menu_item: {
description: 'Create a new menu item with pricing, category, and modifiers.',
parameters: z.object({
name: z.string().describe('Item name'),
description: z.string().optional().describe('Item description'),
price: z.number().describe('Item price'),
cost: z.number().optional().describe('Item cost'),
categoryId: z.string().describe('Category ID'),
sku: z.string().optional().describe('SKU'),
barcode: z.string().optional().describe('Barcode'),
taxable: z.boolean().default(true).describe('Is taxable'),
available: z.boolean().default(true).describe('Is available'),
prepTime: z.number().optional().describe('Preparation time in minutes'),
modifierGroupIds: z.array(z.string()).optional().describe('Modifier group IDs'),
allergens: z.array(z.string()).optional().describe('Allergen information'),
image: z.string().optional().describe('Image URL'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createMenuItem(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create menu item');
}
return {
content: [
{
type: 'text',
text: `Menu item created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_menu_item: {
description: 'Update menu item details including price, availability, and description.',
parameters: z.object({
itemId: z.string().describe('Menu item ID'),
name: z.string().optional().describe('Updated name'),
description: z.string().optional().describe('Updated description'),
price: z.number().optional().describe('Updated price'),
cost: z.number().optional().describe('Updated cost'),
available: z.boolean().optional().describe('Availability'),
prepTime: z.number().optional().describe('Preparation time in minutes'),
image: z.string().optional().describe('Image URL'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { itemId, ...updates } = params;
const response = await client.updateMenuItem(itemId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update menu item');
}
return {
content: [
{
type: 'text',
text: `Menu item updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_delete_menu_item: {
description: 'Delete a menu item permanently.',
parameters: z.object({
itemId: z.string().describe('Menu item ID to delete'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.deleteMenuItem(params.itemId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to delete menu item');
}
return {
content: [
{
type: 'text',
text: 'Menu item deleted successfully!',
},
],
};
},
},
touchbistro_list_menu_categories: {
description: 'List all menu categories with item counts and hierarchy.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getMenuCategories();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch menu categories');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_menu_category: {
description: 'Create a new menu category with name and display order.',
parameters: z.object({
name: z.string().describe('Category name'),
description: z.string().optional().describe('Category description'),
parentCategoryId: z.string().optional().describe('Parent category ID (for sub-categories)'),
sortOrder: z.number().optional().describe('Display sort order'),
available: z.boolean().default(true).describe('Is available'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createMenuCategory(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create menu category');
}
return {
content: [
{
type: 'text',
text: `Category created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_menu_category: {
description: 'Update menu category details.',
parameters: z.object({
categoryId: z.string().describe('Category ID'),
name: z.string().optional().describe('Updated name'),
description: z.string().optional().describe('Updated description'),
sortOrder: z.number().optional().describe('Display sort order'),
available: z.boolean().optional().describe('Availability'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { categoryId, ...updates } = params;
const response = await client.updateMenuCategory(categoryId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update menu category');
}
return {
content: [
{
type: 'text',
text: `Category updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_delete_menu_category: {
description: 'Delete a menu category (items in category must be reassigned first).',
parameters: z.object({
categoryId: z.string().describe('Category ID to delete'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.deleteMenuCategory(params.categoryId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to delete menu category');
}
return {
content: [
{
type: 'text',
text: 'Menu category deleted successfully!',
},
],
};
},
},
touchbistro_list_menus: {
description: 'List all menus (breakfast, lunch, dinner, etc.) with active times and categories.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getMenus();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch menus');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_menu: {
description: 'Create a new menu with time-based availability (breakfast, lunch, happy hour, etc.).',
parameters: z.object({
name: z.string().describe('Menu name'),
description: z.string().optional().describe('Menu description'),
startTime: z.string().optional().describe('Start time (HH:mm format)'),
endTime: z.string().optional().describe('End time (HH:mm format)'),
daysOfWeek: z.array(z.number()).optional().describe('Active days (0=Sunday, 6=Saturday)'),
available: z.boolean().default(true).describe('Is available'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createMenu(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create menu');
}
return {
content: [
{
type: 'text',
text: `Menu created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_menu: {
description: 'Update menu details and availability schedule.',
parameters: z.object({
menuId: z.string().describe('Menu ID'),
name: z.string().optional().describe('Updated name'),
description: z.string().optional().describe('Updated description'),
startTime: z.string().optional().describe('Start time (HH:mm format)'),
endTime: z.string().optional().describe('End time (HH:mm format)'),
daysOfWeek: z.array(z.number()).optional().describe('Active days (0=Sunday, 6=Saturday)'),
available: z.boolean().optional().describe('Availability'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { menuId, ...updates } = params;
const response = await client.updateMenu(menuId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update menu');
}
return {
content: [
{
type: 'text',
text: `Menu updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_list_modifier_groups: {
description: 'List all modifier groups (toppings, sides, cooking temps, etc.).',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getModifierGroups();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch modifier groups');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_modifier_group: {
description: 'Create a new modifier group with modifiers.',
parameters: z.object({
name: z.string().describe('Modifier group name'),
minSelections: z.number().default(0).describe('Minimum selections required'),
maxSelections: z.number().default(1).describe('Maximum selections allowed'),
required: z.boolean().default(false).describe('Is required'),
modifiers: z
.array(
z.object({
name: z.string(),
price: z.number(),
available: z.boolean().default(true),
sortOrder: z.number().optional(),
})
)
.describe('Modifiers in this group'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createModifierGroup(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create modifier group');
}
return {
content: [
{
type: 'text',
text: `Modifier group created!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,338 @@
/**
* Order Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const orderTools = {
touchbistro_list_orders: {
description:
'List all orders with optional filters (status, type, date range, server). Returns active, completed, and historical orders.',
parameters: z.object({
status: z
.enum(['draft', 'pending', 'in_progress', 'ready', 'completed', 'voided', 'cancelled'])
.optional()
.describe('Filter by order status'),
type: z
.enum(['dine_in', 'takeout', 'delivery', 'curbside', 'drive_thru'])
.optional()
.describe('Filter by order type'),
serverId: z.string().optional().describe('Filter by server ID'),
dateFrom: z.string().optional().describe('Start date (ISO format)'),
dateTo: z.string().optional().describe('End date (ISO format)'),
page: z.number().optional().describe('Page number'),
pageSize: z.number().optional().describe('Items per page'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const filter: any = {};
if (params.status) filter.status = params.status;
if (params.type) filter.type = params.type;
if (params.serverId) filter.serverId = params.serverId;
if (params.dateFrom) filter.dateFrom = params.dateFrom;
if (params.dateTo) filter.dateTo = params.dateTo;
const response = await client.getOrders({
page: params.page,
pageSize: params.pageSize,
filter,
});
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch orders');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_order: {
description: 'Get detailed information about a specific order including items, payments, and status.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getOrder(params.orderId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch order');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_order: {
description: 'Create a new order with initial items, table assignment, and server.',
parameters: z.object({
type: z.enum(['dine_in', 'takeout', 'delivery', 'curbside', 'drive_thru']).describe('Order type'),
tableId: z.string().optional().describe('Table ID (for dine-in orders)'),
customerId: z.string().optional().describe('Customer ID'),
serverId: z.string().describe('Server/staff ID'),
guestCount: z.number().optional().describe('Number of guests'),
items: z
.array(
z.object({
menuItemId: z.string(),
quantity: z.number(),
modifiers: z.array(z.object({ id: z.string(), name: z.string(), price: z.number() })).optional(),
specialInstructions: z.string().optional(),
seat: z.number().optional(),
})
)
.optional()
.describe('Initial order items'),
notes: z.string().optional().describe('Order notes'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createOrder(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create order');
}
return {
content: [
{
type: 'text',
text: `Order created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_order: {
description: 'Update order details like guest count, notes, or special instructions.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
guestCount: z.number().optional().describe('Updated guest count'),
notes: z.string().optional().describe('Updated notes'),
specialInstructions: z.string().optional().describe('Special instructions'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { orderId, ...updates } = params;
const response = await client.updateOrder(orderId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update order');
}
return {
content: [
{
type: 'text',
text: `Order updated successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_add_order_item: {
description: 'Add an item to an existing order with modifiers and special instructions.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
menuItemId: z.string().describe('Menu item ID'),
quantity: z.number().describe('Quantity'),
modifiers: z
.array(z.object({ id: z.string(), name: z.string(), price: z.number() }))
.optional()
.describe('Item modifiers'),
specialInstructions: z.string().optional().describe('Special cooking instructions'),
seat: z.number().optional().describe('Seat number'),
courseNumber: z.number().optional().describe('Course number'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { orderId, ...item } = params;
const response = await client.addOrderItem(orderId, item);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to add item to order');
}
return {
content: [
{
type: 'text',
text: `Item added to order!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_order_item: {
description: 'Update an existing order item (quantity, modifiers, instructions).',
parameters: z.object({
orderId: z.string().describe('Order ID'),
itemId: z.string().describe('Order item ID'),
quantity: z.number().optional().describe('Updated quantity'),
modifiers: z
.array(z.object({ id: z.string(), name: z.string(), price: z.number() }))
.optional()
.describe('Updated modifiers'),
specialInstructions: z.string().optional().describe('Updated special instructions'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { orderId, itemId, ...updates } = params;
const response = await client.updateOrderItem(orderId, itemId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update order item');
}
return {
content: [
{
type: 'text',
text: `Order item updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_remove_order_item: {
description: 'Remove an item from an order.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
itemId: z.string().describe('Order item ID to remove'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.removeOrderItem(params.orderId, params.itemId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to remove order item');
}
return {
content: [
{
type: 'text',
text: `Order item removed successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_send_to_kitchen: {
description: 'Send an order to the kitchen for preparation. Marks items as fired.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.sendOrderToKitchen(params.orderId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to send order to kitchen');
}
return {
content: [
{
type: 'text',
text: `Order sent to kitchen!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_complete_order: {
description: 'Mark an order as completed (all items served, payment received).',
parameters: z.object({
orderId: z.string().describe('Order ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.completeOrder(params.orderId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to complete order');
}
return {
content: [
{
type: 'text',
text: `Order completed successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_void_order: {
description: 'Void an entire order with a reason (requires manager approval).',
parameters: z.object({
orderId: z.string().describe('Order ID'),
reason: z.string().describe('Reason for voiding the order'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.voidOrder(params.orderId, params.reason);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to void order');
}
return {
content: [
{
type: 'text',
text: `Order voided!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_split_order: {
description: 'Split an order by items or amount across multiple checks.',
parameters: z.object({
orderId: z.string().describe('Order ID to split'),
splits: z
.array(
z.object({
items: z.array(z.string()).describe('Order item IDs for this split'),
amount: z.number().optional().describe('Fixed amount (alternative to items)'),
})
)
.describe('Split configurations'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.splitOrder(params.orderId, params.splits);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to split order');
}
return {
content: [
{
type: 'text',
text: `Order split into ${params.splits.length} checks!\n\n${JSON.stringify(
response.data,
null,
2
)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,110 @@
/**
* Payment Processing Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const paymentTools = {
touchbistro_process_payment: {
description: 'Process a payment for an order (full, partial, or split).',
parameters: z.object({
orderId: z.string().describe('Order ID'),
type: z.enum(['full', 'partial', 'split']).describe('Payment type'),
method: z
.enum(['cash', 'credit_card', 'debit_card', 'gift_card', 'mobile_payment', 'check', 'house_account'])
.describe('Payment method'),
amount: z.number().describe('Payment amount'),
tip: z.number().optional().describe('Tip amount'),
cardLast4: z.string().optional().describe('Last 4 digits of card'),
cardBrand: z.string().optional().describe('Card brand (Visa, Mastercard, etc.)'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { orderId, ...payment } = params;
const response = await client.processPayment(orderId, payment);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to process payment');
}
return {
content: [
{
type: 'text',
text: `Payment processed successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_get_payments: {
description: 'Get all payments for an order.',
parameters: z.object({
orderId: z.string().describe('Order ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getPayments(params.orderId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch payments');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_refund_payment: {
description: 'Refund a payment (full or partial).',
parameters: z.object({
paymentId: z.string().describe('Payment ID'),
amount: z.number().optional().describe('Refund amount (full refund if not specified)'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.refundPayment(params.paymentId, params.amount);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to refund payment');
}
return {
content: [
{
type: 'text',
text: `Payment refunded!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_void_payment: {
description: 'Void a payment transaction.',
parameters: z.object({
paymentId: z.string().describe('Payment ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.voidPayment(params.paymentId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to void payment');
}
return {
content: [
{
type: 'text',
text: `Payment voided!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,218 @@
/**
* Reservation Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const reservationTools = {
touchbistro_list_reservations: {
description: 'List reservations with filters for date range and status.',
parameters: z.object({
dateFrom: z.string().optional().describe('Start date (ISO format)'),
dateTo: z.string().optional().describe('End date (ISO format)'),
status: z
.enum(['pending', 'confirmed', 'seated', 'completed', 'no_show', 'cancelled'])
.optional()
.describe('Filter by status'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getReservations(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch reservations');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_reservation: {
description: 'Get detailed information about a specific reservation.',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getReservation(params.reservationId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch reservation');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_reservation: {
description: 'Create a new reservation for a customer.',
parameters: z.object({
customerId: z.string().optional().describe('Customer ID (if existing customer)'),
customerName: z.string().describe('Customer name'),
customerPhone: z.string().optional().describe('Customer phone number'),
customerEmail: z.string().optional().describe('Customer email'),
partySize: z.number().describe('Number of guests'),
date: z.string().describe('Reservation date (ISO format)'),
time: z.string().describe('Reservation time (HH:mm format)'),
duration: z.number().optional().describe('Estimated duration in minutes'),
tableId: z.string().optional().describe('Specific table ID'),
sectionId: z.string().optional().describe('Preferred section ID'),
notes: z.string().optional().describe('Reservation notes'),
specialRequests: z.string().optional().describe('Special requests'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createReservation(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create reservation');
}
return {
content: [
{
type: 'text',
text: `Reservation created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_reservation: {
description: 'Update reservation details (time, party size, table, etc.).',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
partySize: z.number().optional().describe('Updated party size'),
date: z.string().optional().describe('Updated date (ISO format)'),
time: z.string().optional().describe('Updated time (HH:mm format)'),
duration: z.number().optional().describe('Updated duration in minutes'),
tableId: z.string().optional().describe('Updated table ID'),
notes: z.string().optional().describe('Updated notes'),
specialRequests: z.string().optional().describe('Updated special requests'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { reservationId, ...updates } = params;
const response = await client.updateReservation(reservationId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update reservation');
}
return {
content: [
{
type: 'text',
text: `Reservation updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_confirm_reservation: {
description: 'Confirm a pending reservation.',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.confirmReservation(params.reservationId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to confirm reservation');
}
return {
content: [
{
type: 'text',
text: `Reservation confirmed!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_seat_reservation: {
description: 'Seat a reservation at a table.',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
tableId: z.string().describe('Table ID to seat at'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.seatReservation(params.reservationId, params.tableId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to seat reservation');
}
return {
content: [
{
type: 'text',
text: `Reservation seated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_cancel_reservation: {
description: 'Cancel a reservation.',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
reason: z.string().optional().describe('Cancellation reason'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.cancelReservation(params.reservationId, params.reason);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to cancel reservation');
}
return {
content: [
{
type: 'text',
text: `Reservation cancelled!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_mark_no_show: {
description: 'Mark a reservation as no-show.',
parameters: z.object({
reservationId: z.string().describe('Reservation ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.markNoShow(params.reservationId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to mark as no-show');
}
return {
content: [
{
type: 'text',
text: `Reservation marked as no-show!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
};

View File

@ -0,0 +1,226 @@
/**
* Staff Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const staffTools = {
touchbistro_list_staff: {
description: 'List all staff members with roles, active status, and assigned sections.',
parameters: z.object({
role: z
.enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier'])
.optional()
.describe('Filter by role'),
active: z.boolean().optional().describe('Filter by active status'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const filter: any = {};
if (params.role) filter.role = params.role;
if (params.active !== undefined) filter.active = params.active;
const response = await client.getStaff({ filter });
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch staff');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_staff_member: {
description: 'Get detailed information about a specific staff member.',
parameters: z.object({
staffId: z.string().describe('Staff member ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getStaffMember(params.staffId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch staff member');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_staff_member: {
description: 'Create a new staff member with role, permissions, and wage information.',
parameters: z.object({
employeeNumber: z.string().describe('Employee number'),
firstName: z.string().describe('First name'),
lastName: z.string().describe('Last name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
role: z
.enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier'])
.describe('Staff role'),
pinCode: z.string().describe('POS PIN code'),
hireDate: z.string().describe('Hire date (ISO format)'),
wage: z.number().optional().describe('Wage amount'),
wageType: z.enum(['hourly', 'salary', 'tipped_hourly']).optional().describe('Wage type'),
assignedSections: z.array(z.string()).optional().describe('Assigned section IDs'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createStaffMember(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create staff member');
}
return {
content: [
{
type: 'text',
text: `Staff member created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_staff_member: {
description: 'Update staff member details, role, or wage information.',
parameters: z.object({
staffId: z.string().describe('Staff member ID'),
firstName: z.string().optional().describe('First name'),
lastName: z.string().optional().describe('Last name'),
email: z.string().optional().describe('Email address'),
phone: z.string().optional().describe('Phone number'),
role: z
.enum(['owner', 'manager', 'server', 'bartender', 'host', 'chef', 'cook', 'busser', 'cashier'])
.optional()
.describe('Staff role'),
wage: z.number().optional().describe('Wage amount'),
wageType: z.enum(['hourly', 'salary', 'tipped_hourly']).optional().describe('Wage type'),
assignedSections: z.array(z.string()).optional().describe('Assigned section IDs'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { staffId, ...updates } = params;
const response = await client.updateStaffMember(staffId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update staff member');
}
return {
content: [
{
type: 'text',
text: `Staff member updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_deactivate_staff_member: {
description: 'Deactivate a staff member (termination/resignation).',
parameters: z.object({
staffId: z.string().describe('Staff member ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.deactivateStaffMember(params.staffId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to deactivate staff member');
}
return {
content: [
{
type: 'text',
text: `Staff member deactivated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_clock_in: {
description: 'Clock in a staff member to start their shift.',
parameters: z.object({
staffId: z.string().describe('Staff member ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.clockIn(params.staffId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to clock in');
}
return {
content: [
{
type: 'text',
text: `Staff member clocked in!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_clock_out: {
description: 'Clock out a staff member to end their shift.',
parameters: z.object({
staffId: z.string().describe('Staff member ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.clockOut(params.staffId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to clock out');
}
return {
content: [
{
type: 'text',
text: `Staff member clocked out!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_get_timesheets: {
description: 'Get staff timesheets with hours, wages, and tips for a date range.',
parameters: z.object({
staffId: z.string().optional().describe('Filter by staff member ID'),
dateFrom: z.string().optional().describe('Start date (ISO format)'),
dateTo: z.string().optional().describe('End date (ISO format)'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getTimeSheets(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch timesheets');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
};

View File

@ -0,0 +1,262 @@
/**
* Table & Floor Management Tools
*/
import { z } from 'zod';
import type { TouchBistroApiClient } from '../lib/api-client.js';
export const tableTools = {
touchbistro_list_tables: {
description: 'List all tables with current status, assigned servers, and guest counts.',
parameters: z.object({
status: z
.enum(['available', 'occupied', 'reserved', 'cleaning', 'out_of_service'])
.optional()
.describe('Filter by table status'),
sectionId: z.string().optional().describe('Filter by section ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const filter: any = {};
if (params.status) filter.status = params.status;
if (params.sectionId) filter.sectionId = params.sectionId;
const response = await client.getTables({ filter });
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch tables');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_table: {
description: 'Get detailed information about a specific table.',
parameters: z.object({
tableId: z.string().describe('Table ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getTable(params.tableId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch table');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_table: {
description: 'Create a new table in a section with position and capacity.',
parameters: z.object({
number: z.string().describe('Table number'),
name: z.string().optional().describe('Table name'),
sectionId: z.string().describe('Section ID'),
capacity: z.number().describe('Maximum guest capacity'),
shape: z.enum(['square', 'rectangle', 'circle', 'oval']).describe('Table shape'),
positionX: z.number().describe('X position on floor plan'),
positionY: z.number().describe('Y position on floor plan'),
rotation: z.number().optional().describe('Rotation in degrees'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createTable(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create table');
}
return {
content: [
{
type: 'text',
text: `Table created successfully!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_table: {
description: 'Update table details like capacity, position, or name.',
parameters: z.object({
tableId: z.string().describe('Table ID'),
number: z.string().optional().describe('Table number'),
name: z.string().optional().describe('Table name'),
capacity: z.number().optional().describe('Maximum guest capacity'),
positionX: z.number().optional().describe('X position on floor plan'),
positionY: z.number().optional().describe('Y position on floor plan'),
rotation: z.number().optional().describe('Rotation in degrees'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const { tableId, ...updates } = params;
const response = await client.updateTable(tableId, updates);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update table');
}
return {
content: [
{
type: 'text',
text: `Table updated!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_update_table_status: {
description: 'Update table status (seat guests, mark as cleaning, reserve, etc.).',
parameters: z.object({
tableId: z.string().describe('Table ID'),
status: z
.enum(['available', 'occupied', 'reserved', 'cleaning', 'out_of_service'])
.describe('New table status'),
guestCount: z.number().optional().describe('Number of guests (for occupied status)'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.updateTableStatus(params.tableId, params.status, params.guestCount);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to update table status');
}
return {
content: [
{
type: 'text',
text: `Table status updated to ${params.status}!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_assign_server_to_table: {
description: 'Assign a server to a table.',
parameters: z.object({
tableId: z.string().describe('Table ID'),
serverId: z.string().describe('Server/staff ID'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.assignServer(params.tableId, params.serverId);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to assign server');
}
return {
content: [
{
type: 'text',
text: `Server assigned to table!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_list_sections: {
description: 'List all sections/stations with assigned servers and table counts.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getSections();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch sections');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_create_section: {
description: 'Create a new section/station on a floor plan.',
parameters: z.object({
name: z.string().describe('Section name'),
floorPlanId: z.string().describe('Floor plan ID'),
color: z.string().optional().describe('Color code for display'),
assignedServerIds: z.array(z.string()).optional().describe('Assigned server IDs'),
}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.createSection(params);
if (!response.success) {
throw new Error(response.error?.message || 'Failed to create section');
}
return {
content: [
{
type: 'text',
text: `Section created!\n\n${JSON.stringify(response.data, null, 2)}`,
},
],
};
},
},
touchbistro_list_floor_plans: {
description: 'List all floor plans with sections and table layouts.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getFloorPlans();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch floor plans');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
touchbistro_get_active_floor_plan: {
description: 'Get the currently active floor plan with all tables and sections.',
parameters: z.object({}),
handler: async (params: any, client: TouchBistroApiClient) => {
const response = await client.getActiveFloorPlan();
if (!response.success) {
throw new Error(response.error?.message || 'Failed to fetch active floor plan');
}
return {
content: [
{
type: 'text',
text: JSON.stringify(response.data, null, 2),
},
],
};
},
},
};

View File

@ -0,0 +1,165 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.filter-bar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.75rem 1.5rem;
background: #1e293b;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
}
.filter-btn:hover {
background: #334155;
border-color: #475569;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.orders-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.order-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s;
}
.order-card:hover {
border-color: #3b82f6;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.1);
}
.order-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.order-number {
font-size: 1.25rem;
font-weight: 600;
color: #f8fafc;
}
.order-status {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
}
.order-details {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
}
.order-total {
font-size: 1.75rem;
font-weight: 700;
color: #10b981;
}
.order-time {
color: #94a3b8;
font-size: 0.875rem;
}
.order-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
flex: 1;
padding: 0.75rem;
background: #334155;
border: none;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s;
}
.action-btn:hover {
background: #475569;
}
.action-btn.primary {
background: #3b82f6;
color: #fff;
}
.action-btn.primary:hover {
background: #2563eb;
}
.loading {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1.1rem;
}

View File

@ -0,0 +1,104 @@
import { useState, useEffect } from 'react';
import './App.css';
interface Order {
id: string;
orderNumber: string;
status: string;
total: number;
items: any[];
createdAt: string;
}
export default function OrdersApp() {
const [orders, setOrders] = useState<Order[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
useEffect(() => {
// Simulated data for demonstration
const mockOrders: Order[] = [
{
id: '1',
orderNumber: 'ORD-001',
status: 'preparing',
total: 45.99,
items: [],
createdAt: new Date().toISOString(),
},
{
id: '2',
orderNumber: 'ORD-002',
status: 'ready',
total: 32.50,
items: [],
createdAt: new Date().toISOString(),
},
];
setOrders(mockOrders);
setLoading(false);
}, []);
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
pending: '#fbbf24',
preparing: '#3b82f6',
ready: '#10b981',
served: '#8b5cf6',
completed: '#6b7280',
cancelled: '#ef4444',
};
return colors[status] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<h1>🍽 Orders Dashboard</h1>
<p>Real-time order tracking and management</p>
</header>
<div className="filter-bar">
{['all', 'pending', 'preparing', 'ready', 'served'].map((status) => (
<button
key={status}
className={`filter-btn ${filter === status ? 'active' : ''}`}
onClick={() => setFilter(status)}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
))}
</div>
<div className="orders-grid">
{loading ? (
<div className="loading">Loading orders...</div>
) : (
orders.map((order) => (
<div key={order.id} className="order-card">
<div className="order-header">
<span className="order-number">{order.orderNumber}</span>
<span
className="order-status"
style={{ backgroundColor: getStatusColor(order.status) }}
>
{order.status}
</span>
</div>
<div className="order-details">
<div className="order-total">${order.total.toFixed(2)}</div>
<div className="order-time">
{new Date(order.createdAt).toLocaleTimeString()}
</div>
</div>
<div className="order-actions">
<button className="action-btn">View Details</button>
<button className="action-btn primary">Update Status</button>
</div>
</div>
))
)}
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TouchBistro Orders Dashboard</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,10 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
build: {
outDir: 'dist',
emptyOutDir: true,
},
});

View File

@ -0,0 +1,156 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Customer {
id: string;
name: string;
email: string;
phone: string;
visitCount: number;
totalSpent: number;
avgSpend: number;
lastVisit: string;
loyaltyPoints: number;
tags: string[];
}
export default function CustomerDirectory() {
const [customers, setCustomers] = useState<Customer[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<'name' | 'visits' | 'spent'>('name');
useEffect(() => {
// Mock data
setCustomers([
{ id: '1', name: 'Sarah Johnson', email: 'sarah.j@email.com', phone: '555-0101', visitCount: 23, totalSpent: 1847.50, avgSpend: 80.33, lastVisit: '2024-02-14', loyaltyPoints: 2340, tags: ['VIP', 'Regular'] },
{ id: '2', name: 'Mike Chen', email: 'mike.chen@email.com', phone: '555-0102', visitCount: 45, totalSpent: 3245.75, avgSpend: 72.13, lastVisit: '2024-02-15', loyaltyPoints: 4890, tags: ['VIP', 'Regular', 'Anniversary'] },
{ id: '3', name: 'Emma Davis', email: 'emma.d@email.com', phone: '555-0103', visitCount: 8, totalSpent: 567.20, avgSpend: 70.90, lastVisit: '2024-02-12', loyaltyPoints: 850, tags: [] },
{ id: '4', name: 'John Smith', email: 'john.smith@email.com', phone: '555-0104', visitCount: 12, totalSpent: 982.40, avgSpend: 81.87, lastVisit: '2024-02-10', loyaltyPoints: 1470, tags: ['Regular'] },
{ id: '5', name: 'Lisa Park', email: 'lisa.park@email.com', phone: '555-0105', visitCount: 34, totalSpent: 2456.90, avgSpend: 72.26, lastVisit: '2024-02-15', loyaltyPoints: 3685, tags: ['VIP'] },
{ id: '6', name: 'David Wilson', email: 'david.w@email.com', phone: '555-0106', visitCount: 5, totalSpent: 342.50, avgSpend: 68.50, lastVisit: '2024-02-08', loyaltyPoints: 515, tags: [] },
{ id: '7', name: 'Amy Chen', email: 'amy.chen@email.com', phone: '555-0107', visitCount: 18, totalSpent: 1456.80, avgSpend: 80.93, lastVisit: '2024-02-13', loyaltyPoints: 2185, tags: ['Regular'] },
{ id: '8', name: 'Robert Taylor', email: 'robert.t@email.com', phone: '555-0108', visitCount: 29, totalSpent: 2123.60, avgSpend: 73.23, lastVisit: '2024-02-14', loyaltyPoints: 3185, tags: ['VIP', 'Regular'] },
]);
}, []);
const filteredCustomers = customers
.filter((customer) =>
customer.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
customer.email.toLowerCase().includes(searchQuery.toLowerCase()) ||
customer.phone.includes(searchQuery)
)
.sort((a, b) => {
if (sortBy === 'name') return a.name.localeCompare(b.name);
if (sortBy === 'visits') return b.visitCount - a.visitCount;
if (sortBy === 'spent') return b.totalSpent - a.totalSpent;
return 0;
});
const totalCustomers = customers.length;
const totalRevenue = customers.reduce((sum, c) => sum + c.totalSpent, 0);
const avgVisits = customers.reduce((sum, c) => sum + c.visitCount, 0) / customers.length;
return (
<div className="app">
<header className="app-header">
<h1>👥 Customer Directory</h1>
<p>Searchable customer database</p>
</header>
<div className="stats-bar">
<div className="stat-card">
<span className="stat-label">Total Customers</span>
<span className="stat-value">{totalCustomers}</span>
</div>
<div className="stat-card">
<span className="stat-label">Total Revenue</span>
<span className="stat-value revenue">${totalRevenue.toFixed(2)}</span>
</div>
<div className="stat-card">
<span className="stat-label">Avg Visits</span>
<span className="stat-value">{avgVisits.toFixed(1)}</span>
</div>
</div>
<div className="controls">
<input
type="text"
className="search-input"
placeholder="Search by name, email, or phone..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
className="sort-select"
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
>
<option value="name">Sort by Name</option>
<option value="visits">Sort by Visits</option>
<option value="spent">Sort by Spending</option>
</select>
<button className="btn-primary">+ Add Customer</button>
</div>
<div className="customer-grid">
{filteredCustomers.map((customer) => (
<div key={customer.id} className="customer-card">
<div className="customer-header">
<h3>{customer.name}</h3>
{customer.tags.length > 0 && (
<div className="tags">
{customer.tags.map((tag) => (
<span key={tag} className={`tag ${tag.toLowerCase()}`}>
{tag}
</span>
))}
</div>
)}
</div>
<div className="customer-contact">
<div className="contact-item">
<span className="icon">📧</span>
<span>{customer.email}</span>
</div>
<div className="contact-item">
<span className="icon">📱</span>
<span>{customer.phone}</span>
</div>
</div>
<div className="customer-stats">
<div className="stat-item">
<span className="stat-value">{customer.visitCount}</span>
<span className="stat-label">Visits</span>
</div>
<div className="stat-item">
<span className="stat-value">${customer.totalSpent.toFixed(0)}</span>
<span className="stat-label">Total Spent</span>
</div>
<div className="stat-item">
<span className="stat-value">${customer.avgSpend.toFixed(0)}</span>
<span className="stat-label">Avg Spend</span>
</div>
<div className="stat-item">
<span className="stat-value">{customer.loyaltyPoints}</span>
<span className="stat-label">Points</span>
</div>
</div>
<div className="customer-footer">
<span className="last-visit">
Last visit: {new Date(customer.lastVisit).toLocaleDateString()}
</span>
<button className="view-btn">View Profile</button>
</div>
</div>
))}
</div>
{filteredCustomers.length === 0 && (
<div className="no-results">No customers found</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Customer Directory - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,274 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.25rem;
text-align: center;
}
.stat-label {
display: block;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.stat-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.revenue {
color: #10b981;
}
.controls {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 300px;
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
}
.search-input::placeholder {
color: #64748b;
}
.sort-select {
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
}
.sort-select:focus {
outline: none;
border-color: #3b82f6;
}
.btn-primary {
padding: 0.875rem 1.5rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary:hover {
background: #2563eb;
}
.customer-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 1.5rem;
}
.customer-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s;
}
.customer-card:hover {
border-color: #3b82f6;
}
.customer-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
padding-bottom: 1rem;
border-bottom: 1px solid #334155;
}
.customer-header h3 {
color: #f8fafc;
font-size: 1.25rem;
}
.tags {
display: flex;
gap: 0.5rem;
}
.tag {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
}
.tag.vip {
background: #fbbf24;
color: #78350f;
}
.tag.regular {
background: #3b82f6;
color: #fff;
}
.tag.anniversary {
background: #ec4899;
color: #fff;
}
.customer-contact {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1rem;
}
.contact-item {
display: flex;
align-items: center;
gap: 0.75rem;
color: #94a3b8;
font-size: 0.9375rem;
}
.icon {
font-size: 1.125rem;
}
.customer-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
margin-bottom: 1rem;
}
.stat-item {
background: #0f172a;
padding: 0.875rem;
border-radius: 0.5rem;
text-align: center;
}
.stat-item .stat-value {
display: block;
color: #10b981;
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.25rem;
}
.stat-item .stat-label {
display: block;
color: #64748b;
font-size: 0.75rem;
text-transform: uppercase;
}
.customer-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding-top: 1rem;
border-top: 1px solid #334155;
}
.last-visit {
color: #64748b;
font-size: 0.875rem;
}
.view-btn {
padding: 0.5rem 1rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s;
}
.view-btn:hover {
background: #2563eb;
}
.no-results {
text-align: center;
padding: 3rem;
color: #64748b;
font-size: 1.125rem;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
}

View File

@ -0,0 +1,170 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Modifier {
id: string;
name: string;
price: number;
}
interface ModifierGroup {
id: string;
name: string;
required: boolean;
modifiers: Modifier[];
}
interface MenuItem {
id: string;
name: string;
description: string;
price: number;
cost: number;
category: string;
available: boolean;
prepTime: number;
allergens: string[];
modifierGroups: ModifierGroup[];
}
export default function MenuItemDetail() {
const [item, setItem] = useState<MenuItem | null>(null);
useEffect(() => {
// Mock data
setItem({
id: '1',
name: 'Classic Burger',
description: 'Half-pound beef patty with fresh lettuce, tomato, red onion, pickles, and our signature sauce on a toasted brioche bun',
price: 14.99,
cost: 4.25,
category: 'Burgers & Sandwiches',
available: true,
prepTime: 12,
allergens: ['Gluten', 'Dairy', 'Soy'],
modifierGroups: [
{
id: '1',
name: 'Cheese Options',
required: false,
modifiers: [
{ id: '1', name: 'Cheddar', price: 1.50 },
{ id: '2', name: 'Swiss', price: 1.50 },
{ id: '3', name: 'Blue Cheese', price: 2.00 },
{ id: '4', name: 'Pepper Jack', price: 1.50 },
],
},
{
id: '2',
name: 'Toppings',
required: false,
modifiers: [
{ id: '5', name: 'Bacon', price: 2.50 },
{ id: '6', name: 'Avocado', price: 2.00 },
{ id: '7', name: 'Fried Egg', price: 2.00 },
{ id: '8', name: 'Mushrooms', price: 1.50 },
],
},
{
id: '3',
name: 'Doneness',
required: true,
modifiers: [
{ id: '9', name: 'Rare', price: 0 },
{ id: '10', name: 'Medium Rare', price: 0 },
{ id: '11', name: 'Medium', price: 0 },
{ id: '12', name: 'Medium Well', price: 0 },
{ id: '13', name: 'Well Done', price: 0 },
],
},
],
});
}, []);
if (!item) {
return <div className="loading">Loading item...</div>;
}
const margin = ((item.price - item.cost) / item.price) * 100;
return (
<div className="app">
<header className="app-header">
<div>
<h1>🍔 {item.name}</h1>
<p className="category">{item.category}</p>
</div>
<div className="header-actions">
<span className={`availability ${item.available ? 'available' : 'unavailable'}`}>
{item.available ? '✓ Available' : '✗ Unavailable'}
</span>
<button className="btn-primary">Edit Item</button>
</div>
</header>
<div className="content-grid">
<div className="main-info">
<div className="info-card">
<h2>Description</h2>
<p className="description">{item.description}</p>
</div>
<div className="info-card">
<h2>Pricing & Cost</h2>
<div className="pricing-grid">
<div className="pricing-item">
<span className="pricing-label">Menu Price</span>
<span className="pricing-value price">${item.price.toFixed(2)}</span>
</div>
<div className="pricing-item">
<span className="pricing-label">Food Cost</span>
<span className="pricing-value cost">${item.cost.toFixed(2)}</span>
</div>
<div className="pricing-item">
<span className="pricing-label">Margin</span>
<span className="pricing-value margin">{margin.toFixed(1)}%</span>
</div>
<div className="pricing-item">
<span className="pricing-label">Prep Time</span>
<span className="pricing-value">{item.prepTime} min</span>
</div>
</div>
</div>
<div className="info-card">
<h2>Allergens</h2>
<div className="allergens">
{item.allergens.map((allergen) => (
<span key={allergen} className="allergen-badge">
{allergen}
</span>
))}
</div>
</div>
</div>
<div className="modifiers-panel">
<h2>Modifier Groups</h2>
{item.modifierGroups.map((group) => (
<div key={group.id} className="modifier-group">
<div className="group-header">
<h3>{group.name}</h3>
{group.required && <span className="required-badge">Required</span>}
</div>
<div className="modifiers-list">
{group.modifiers.map((mod) => (
<div key={mod.id} className="modifier-row">
<span className="modifier-name">{mod.name}</span>
<span className="modifier-price">
{mod.price === 0 ? 'Free' : `+$${mod.price.toFixed(2)}`}
</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Menu Item Detail - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,247 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.25rem;
}
.category {
color: #94a3b8;
font-size: 1.1rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.availability {
padding: 0.625rem 1.25rem;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
}
.availability.available {
background: #10b981;
color: #fff;
}
.availability.unavailable {
background: #ef4444;
color: #fff;
}
.btn-primary {
padding: 0.875rem 1.5rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary:hover {
background: #2563eb;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
.main-info {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.info-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.info-card h2 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.description {
color: #cbd5e1;
font-size: 1.125rem;
line-height: 1.7;
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.pricing-item {
background: #0f172a;
padding: 1rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.pricing-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
}
.pricing-value {
color: #f8fafc;
font-size: 1.5rem;
font-weight: 700;
}
.pricing-value.price {
color: #10b981;
}
.pricing-value.cost {
color: #ef4444;
}
.pricing-value.margin {
color: #3b82f6;
}
.allergens {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
.allergen-badge {
padding: 0.625rem 1rem;
background: #7f1d1d;
color: #fca5a5;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.875rem;
}
.modifiers-panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
height: fit-content;
}
.modifiers-panel h2 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1.5rem;
}
.modifier-group {
margin-bottom: 1.5rem;
}
.modifier-group:last-child {
margin-bottom: 0;
}
.group-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.75rem;
}
.group-header h3 {
color: #f8fafc;
font-size: 1.125rem;
}
.required-badge {
padding: 0.25rem 0.625rem;
background: #fbbf24;
color: #78350f;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.modifiers-list {
background: #0f172a;
border-radius: 0.5rem;
overflow: hidden;
}
.modifier-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.875rem 1rem;
border-bottom: 1px solid #334155;
}
.modifier-row:last-child {
border-bottom: none;
}
.modifier-name {
color: #cbd5e1;
font-size: 0.9375rem;
}
.modifier-price {
color: #10b981;
font-weight: 600;
font-size: 0.875rem;
}
.loading {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1.1rem;
}

View File

@ -0,0 +1,140 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface MenuItem {
id: string;
name: string;
description: string;
price: number;
category: string;
available: boolean;
modifiers: number;
}
interface Category {
id: string;
name: string;
itemCount: number;
}
export default function MenuManager() {
const [categories, setCategories] = useState<Category[]>([]);
const [items, setItems] = useState<MenuItem[]>([]);
const [selectedCategory, setSelectedCategory] = useState('all');
const [searchQuery, setSearchQuery] = useState('');
useEffect(() => {
// Mock data
setCategories([
{ id: 'appetizers', name: 'Appetizers', itemCount: 8 },
{ id: 'entrees', name: 'Entrees', itemCount: 12 },
{ id: 'burgers', name: 'Burgers & Sandwiches', itemCount: 9 },
{ id: 'salads', name: 'Salads', itemCount: 6 },
{ id: 'desserts', name: 'Desserts', itemCount: 7 },
{ id: 'beverages', name: 'Beverages', itemCount: 15 },
]);
setItems([
{ id: '1', name: 'Classic Burger', description: 'Beef patty, lettuce, tomato, onion, pickles', price: 14.99, category: 'burgers', available: true, modifiers: 8 },
{ id: '2', name: 'Caesar Salad', description: 'Romaine, parmesan, croutons, Caesar dressing', price: 9.99, category: 'salads', available: true, modifiers: 4 },
{ id: '3', name: 'Chicken Wings', description: '8 pieces with choice of sauce', price: 12.99, category: 'appetizers', available: true, modifiers: 6 },
{ id: '4', name: 'Grilled Salmon', description: 'Atlantic salmon with vegetables and rice', price: 24.99, category: 'entrees', available: true, modifiers: 3 },
{ id: '5', name: 'Chocolate Cake', description: 'Rich chocolate layer cake with ganache', price: 7.99, category: 'desserts', available: true, modifiers: 2 },
{ id: '6', name: 'Craft Beer', description: 'Rotating selection of local craft beers', price: 6.99, category: 'beverages', available: true, modifiers: 0 },
{ id: '7', name: 'Margherita Pizza', description: 'Fresh mozzarella, basil, tomato sauce', price: 16.99, category: 'entrees', available: false, modifiers: 5 },
{ id: '8', name: 'French Fries', description: 'Crispy golden fries with sea salt', price: 4.99, category: 'appetizers', available: true, modifiers: 4 },
]);
}, []);
const filteredItems = items.filter((item) => {
const matchesCategory = selectedCategory === 'all' || item.category === selectedCategory;
const matchesSearch = item.name.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
return (
<div className="app">
<header className="app-header">
<h1>📖 Menu Manager</h1>
<p>Browse and edit menu items</p>
</header>
<div className="toolbar">
<input
type="text"
className="search-input"
placeholder="Search menu items..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<button className="btn-primary">+ Add Item</button>
</div>
<div className="content-grid">
<div className="categories-panel">
<h2>Categories</h2>
<div className="category-list">
<button
className={`category-btn ${selectedCategory === 'all' ? 'active' : ''}`}
onClick={() => setSelectedCategory('all')}
>
All Items
<span className="count">{items.length}</span>
</button>
{categories.map((cat) => (
<button
key={cat.id}
className={`category-btn ${selectedCategory === cat.id ? 'active' : ''}`}
onClick={() => setSelectedCategory(cat.id)}
>
{cat.name}
<span className="count">{cat.itemCount}</span>
</button>
))}
</div>
<button className="btn-secondary">+ Add Category</button>
</div>
<div className="items-panel">
<div className="panel-header">
<h2>
{selectedCategory === 'all'
? 'All Items'
: categories.find((c) => c.id === selectedCategory)?.name}
</h2>
<span className="results-count">
{filteredItems.length} {filteredItems.length === 1 ? 'item' : 'items'}
</span>
</div>
<div className="items-grid">
{filteredItems.map((item) => (
<div key={item.id} className="item-card">
<div className="item-header">
<h3>{item.name}</h3>
<span className={`availability ${item.available ? 'available' : 'unavailable'}`}>
{item.available ? '✓ Available' : '✗ Unavailable'}
</span>
</div>
<p className="item-description">{item.description}</p>
<div className="item-meta">
<span className="price">${item.price.toFixed(2)}</span>
<span className="modifiers">{item.modifiers} modifiers</span>
</div>
<div className="item-actions">
<button className="action-btn">Edit</button>
<button className="action-btn">Duplicate</button>
<button className="action-btn delete">Delete</button>
</div>
</div>
))}
</div>
{filteredItems.length === 0 && (
<div className="no-results">No items found</div>
)}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Menu Manager - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,298 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.toolbar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
}
.search-input {
flex: 1;
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
}
.search-input::placeholder {
color: #64748b;
}
.btn-primary,
.btn-secondary {
padding: 0.875rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.btn-primary {
background: #3b82f6;
color: #fff;
}
.btn-primary:hover {
background: #2563eb;
}
.btn-secondary {
background: #334155;
color: #cbd5e1;
width: 100%;
margin-top: 1rem;
}
.btn-secondary:hover {
background: #475569;
}
.content-grid {
display: grid;
grid-template-columns: 300px 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
.categories-panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
height: fit-content;
}
.categories-panel h2 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.category-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.category-btn {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.875rem 1rem;
background: #0f172a;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
text-align: left;
transition: all 0.2s;
}
.category-btn:hover {
border-color: #475569;
background: #1e293b;
}
.category-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.category-btn .count {
background: #334155;
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 600;
}
.category-btn.active .count {
background: #2563eb;
}
.items-panel {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.panel-header h2 {
color: #f8fafc;
font-size: 1.5rem;
}
.results-count {
color: #94a3b8;
font-size: 0.875rem;
}
.items-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
}
.item-card {
background: #0f172a;
border: 1px solid #334155;
border-radius: 0.5rem;
padding: 1.25rem;
transition: all 0.2s;
}
.item-card:hover {
border-color: #3b82f6;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.item-header h3 {
color: #f8fafc;
font-size: 1.125rem;
flex: 1;
}
.availability {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
white-space: nowrap;
}
.availability.available {
background: #10b981;
color: #fff;
}
.availability.unavailable {
background: #ef4444;
color: #fff;
}
.item-description {
color: #94a3b8;
font-size: 0.875rem;
margin-bottom: 1rem;
line-height: 1.5;
}
.item-meta {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding-top: 1rem;
border-top: 1px solid #334155;
}
.price {
color: #10b981;
font-size: 1.25rem;
font-weight: 700;
}
.modifiers {
color: #64748b;
font-size: 0.875rem;
}
.item-actions {
display: flex;
gap: 0.5rem;
}
.action-btn {
flex: 1;
padding: 0.625rem;
background: #334155;
border: none;
color: #cbd5e1;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s;
}
.action-btn:hover {
background: #475569;
}
.action-btn.delete {
background: #7f1d1d;
color: #fca5a5;
}
.action-btn.delete:hover {
background: #991b1b;
}
.no-results {
text-align: center;
padding: 3rem;
color: #64748b;
font-size: 1.125rem;
}

View File

@ -0,0 +1,152 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface OrderStats {
total: number;
pending: number;
inProgress: number;
completed: number;
revenue: number;
averageValue: number;
}
interface RecentOrder {
id: string;
orderNumber: string;
status: string;
total: number;
time: string;
}
export default function OrderDashboard() {
const [stats, setStats] = useState<OrderStats>({
total: 0,
pending: 0,
inProgress: 0,
completed: 0,
revenue: 0,
averageValue: 0,
});
const [recentOrders, setRecentOrders] = useState<RecentOrder[]>([]);
const [hourlyVolume, setHourlyVolume] = useState<{ hour: string; count: number }[]>([]);
useEffect(() => {
// Mock data
setStats({
total: 142,
pending: 8,
inProgress: 12,
completed: 122,
revenue: 8547.32,
averageValue: 60.19,
});
setRecentOrders([
{ id: '1', orderNumber: 'ORD-142', status: 'in_progress', total: 68.50, time: '2 min ago' },
{ id: '2', orderNumber: 'ORD-141', status: 'pending', total: 45.00, time: '5 min ago' },
{ id: '3', orderNumber: 'ORD-140', status: 'completed', total: 92.75, time: '8 min ago' },
{ id: '4', orderNumber: 'ORD-139', status: 'in_progress', total: 54.25, time: '12 min ago' },
{ id: '5', orderNumber: 'ORD-138', status: 'completed', total: 78.90, time: '15 min ago' },
]);
setHourlyVolume([
{ hour: '11am', count: 15 },
{ hour: '12pm', count: 28 },
{ hour: '1pm', count: 32 },
{ hour: '2pm', count: 18 },
{ hour: '3pm', count: 12 },
{ hour: '4pm', count: 8 },
{ hour: '5pm', count: 22 },
{ hour: '6pm', count: 35 },
]);
}, []);
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
pending: '#fbbf24',
in_progress: '#3b82f6',
completed: '#10b981',
};
return colors[status] || '#6b7280';
};
const maxVolume = Math.max(...hourlyVolume.map(h => h.count));
return (
<div className="app">
<header className="app-header">
<h1>📊 Order Dashboard</h1>
<p>Live order statistics and activity</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Total Orders Today</div>
<div className="stat-value">{stats.total}</div>
</div>
<div className="stat-card">
<div className="stat-label">Pending</div>
<div className="stat-value pending">{stats.pending}</div>
</div>
<div className="stat-card">
<div className="stat-label">In Progress</div>
<div className="stat-value in-progress">{stats.inProgress}</div>
</div>
<div className="stat-card">
<div className="stat-label">Completed</div>
<div className="stat-value completed">{stats.completed}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Revenue</div>
<div className="stat-value revenue">${stats.revenue.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Average Order Value</div>
<div className="stat-value">${stats.averageValue.toFixed(2)}</div>
</div>
</div>
<div className="dashboard-content">
<div className="section">
<h2>Hourly Order Volume</h2>
<div className="chart">
{hourlyVolume.map((item) => (
<div key={item.hour} className="chart-bar">
<div
className="bar-fill"
style={{ height: `${(item.count / maxVolume) * 100}%` }}
>
<span className="bar-label">{item.count}</span>
</div>
<div className="bar-hour">{item.hour}</div>
</div>
))}
</div>
</div>
<div className="section">
<h2>Recent Orders</h2>
<div className="recent-orders">
{recentOrders.map((order) => (
<div key={order.id} className="order-row">
<div className="order-info">
<span className="order-number">{order.orderNumber}</span>
<span className="order-time">{order.time}</span>
</div>
<div className="order-details">
<span
className="order-status"
style={{ backgroundColor: getStatusColor(order.status) }}
>
{order.status.replace('_', ' ')}
</span>
<span className="order-total">${order.total.toFixed(2)}</span>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Order Dashboard - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,197 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.pending {
color: #fbbf24;
}
.stat-value.in-progress {
color: #3b82f6;
}
.stat-value.completed {
color: #10b981;
}
.stat-value.revenue {
color: #10b981;
}
.dashboard-content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.dashboard-content {
grid-template-columns: 1fr;
}
}
.section {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.section h2 {
color: #f8fafc;
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.chart {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 0.5rem;
height: 200px;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
}
.chart-bar {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 100%;
}
.bar-fill {
width: 100%;
background: linear-gradient(to top, #3b82f6, #60a5fa);
border-radius: 0.25rem;
display: flex;
align-items: flex-start;
justify-content: center;
padding-top: 0.5rem;
min-height: 30px;
}
.bar-label {
color: #fff;
font-size: 0.875rem;
font-weight: 600;
}
.bar-hour {
color: #94a3b8;
font-size: 0.75rem;
margin-top: 0.5rem;
}
.recent-orders {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.order-row {
background: #0f172a;
border-radius: 0.5rem;
padding: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.order-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.order-number {
color: #f8fafc;
font-weight: 600;
font-size: 1rem;
}
.order-time {
color: #64748b;
font-size: 0.875rem;
}
.order-details {
display: flex;
align-items: center;
gap: 1rem;
}
.order-status {
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
}
.order-total {
color: #10b981;
font-weight: 700;
font-size: 1.125rem;
}

View File

@ -0,0 +1,211 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface OrderItem {
id: string;
name: string;
quantity: number;
price: number;
modifiers: { name: string; price: number }[];
instructions?: string;
}
interface Order {
id: string;
orderNumber: string;
status: string;
type: string;
customer: string;
server: string;
table?: string;
items: OrderItem[];
subtotal: number;
tax: number;
tip: number;
total: number;
paymentMethod: string;
createdAt: string;
completedAt?: string;
}
export default function OrderDetail() {
const [order, setOrder] = useState<Order | null>(null);
useEffect(() => {
// Mock data
setOrder({
id: '1',
orderNumber: 'ORD-142',
status: 'in_progress',
type: 'dine_in',
customer: 'Sarah Johnson',
server: 'Mike Chen',
table: 'Table 12',
items: [
{
id: '1',
name: 'Classic Burger',
quantity: 2,
price: 14.99,
modifiers: [
{ name: 'Extra Cheese', price: 1.50 },
{ name: 'No Onions', price: 0 },
],
instructions: 'Medium rare',
},
{
id: '2',
name: 'Caesar Salad',
quantity: 1,
price: 9.99,
modifiers: [
{ name: 'Add Chicken', price: 4.00 },
],
},
{
id: '3',
name: 'French Fries',
quantity: 2,
price: 4.99,
modifiers: [],
},
{
id: '4',
name: 'Soft Drink',
quantity: 2,
price: 2.99,
modifiers: [],
},
],
subtotal: 60.43,
tax: 5.44,
tip: 12.00,
total: 77.87,
paymentMethod: 'Credit Card',
createdAt: new Date(Date.now() - 15 * 60000).toISOString(),
});
}, []);
if (!order) {
return <div className="loading">Loading order...</div>;
}
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
draft: '#6b7280',
pending: '#fbbf24',
in_progress: '#3b82f6',
ready: '#10b981',
completed: '#10b981',
voided: '#ef4444',
};
return colors[status] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<div>
<h1>🧾 Order Detail</h1>
<p>{order.orderNumber}</p>
</div>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(order.status) }}
>
{order.status.replace('_', ' ').toUpperCase()}
</span>
</header>
<div className="order-grid">
<div className="info-section">
<h2>Order Information</h2>
<div className="info-grid">
<div className="info-item">
<span className="info-label">Order Type</span>
<span className="info-value">{order.type.replace('_', ' ')}</span>
</div>
<div className="info-item">
<span className="info-label">Customer</span>
<span className="info-value">{order.customer}</span>
</div>
<div className="info-item">
<span className="info-label">Server</span>
<span className="info-value">{order.server}</span>
</div>
<div className="info-item">
<span className="info-label">Table</span>
<span className="info-value">{order.table || 'N/A'}</span>
</div>
<div className="info-item">
<span className="info-label">Created</span>
<span className="info-value">
{new Date(order.createdAt).toLocaleString()}
</span>
</div>
<div className="info-item">
<span className="info-label">Payment</span>
<span className="info-value">{order.paymentMethod}</span>
</div>
</div>
</div>
<div className="items-section">
<h2>Order Items</h2>
<div className="items-list">
{order.items.map((item) => (
<div key={item.id} className="item-card">
<div className="item-header">
<span className="item-name">
{item.quantity}× {item.name}
</span>
<span className="item-price">
${(item.price * item.quantity).toFixed(2)}
</span>
</div>
{item.modifiers.length > 0 && (
<div className="modifiers">
{item.modifiers.map((mod, idx) => (
<div key={idx} className="modifier">
{mod.name}
{mod.price > 0 && ` (+$${mod.price.toFixed(2)})`}
</div>
))}
</div>
)}
{item.instructions && (
<div className="instructions">📝 {item.instructions}</div>
)}
</div>
))}
</div>
<div className="totals">
<div className="total-row">
<span>Subtotal</span>
<span>${order.subtotal.toFixed(2)}</span>
</div>
<div className="total-row">
<span>Tax</span>
<span>${order.tax.toFixed(2)}</span>
</div>
<div className="total-row">
<span>Tip</span>
<span>${order.tip.toFixed(2)}</span>
</div>
<div className="total-row total">
<span>Total</span>
<span>${order.total.toFixed(2)}</span>
</div>
</div>
<div className="actions">
<button className="btn btn-secondary">Print Receipt</button>
<button className="btn btn-secondary">Refund</button>
<button className="btn btn-primary">Update Status</button>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Order Detail - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,221 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.25rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.status-badge {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
color: #fff;
font-size: 0.875rem;
}
.order-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.order-grid {
grid-template-columns: 1fr;
}
}
.info-section,
.items-section {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
h2 {
color: #f8fafc;
font-size: 1.5rem;
margin-bottom: 1.5rem;
}
.info-grid {
display: grid;
gap: 1.25rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
}
.info-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
text-transform: uppercase;
}
.info-value {
color: #f8fafc;
font-size: 1.125rem;
font-weight: 600;
text-transform: capitalize;
}
.items-list {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
}
.item-card {
background: #0f172a;
border-radius: 0.5rem;
padding: 1rem;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.item-name {
color: #f8fafc;
font-weight: 600;
font-size: 1.125rem;
}
.item-price {
color: #10b981;
font-weight: 700;
font-size: 1.125rem;
}
.modifiers {
margin-top: 0.75rem;
padding-left: 1rem;
}
.modifier {
color: #94a3b8;
font-size: 0.875rem;
margin-bottom: 0.25rem;
}
.instructions {
margin-top: 0.75rem;
padding: 0.75rem;
background: #1e293b;
border-left: 3px solid #fbbf24;
border-radius: 0.25rem;
color: #fbbf24;
font-size: 0.875rem;
}
.totals {
background: #0f172a;
border-radius: 0.5rem;
padding: 1.5rem;
margin-bottom: 1.5rem;
}
.total-row {
display: flex;
justify-content: space-between;
padding: 0.75rem 0;
border-bottom: 1px solid #334155;
color: #cbd5e1;
font-size: 1rem;
}
.total-row:last-child {
border-bottom: none;
}
.total-row.total {
font-size: 1.5rem;
font-weight: 700;
color: #10b981;
padding-top: 1rem;
margin-top: 0.5rem;
border-top: 2px solid #334155;
}
.actions {
display: flex;
gap: 1rem;
}
.btn {
flex: 1;
padding: 1rem;
border: none;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-secondary {
background: #334155;
color: #cbd5e1;
}
.btn-secondary:hover {
background: #475569;
}
.btn-primary {
background: #3b82f6;
color: #fff;
}
.btn-primary:hover {
background: #2563eb;
}
.loading {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1.1rem;
}

View File

@ -0,0 +1,154 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Order {
id: string;
orderNumber: string;
status: string;
type: string;
customer: string;
server: string;
total: number;
createdAt: string;
}
export default function OrderGrid() {
const [orders, setOrders] = useState<Order[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState('all');
const [typeFilter, setTypeFilter] = useState('all');
useEffect(() => {
// Mock data
const mockOrders: Order[] = [
{ id: '1', orderNumber: 'ORD-142', status: 'in_progress', type: 'dine_in', customer: 'Sarah Johnson', server: 'Mike Chen', total: 77.87, createdAt: '2024-01-15T14:30:00Z' },
{ id: '2', orderNumber: 'ORD-141', status: 'completed', type: 'takeout', customer: 'John Smith', server: 'Lisa Park', total: 45.50, createdAt: '2024-01-15T14:15:00Z' },
{ id: '3', orderNumber: 'ORD-140', status: 'pending', type: 'delivery', customer: 'Emma Davis', server: 'Tom Wilson', total: 92.30, createdAt: '2024-01-15T14:00:00Z' },
{ id: '4', orderNumber: 'ORD-139', status: 'completed', type: 'dine_in', customer: 'Michael Brown', server: 'Mike Chen', total: 156.75, createdAt: '2024-01-15T13:45:00Z' },
{ id: '5', orderNumber: 'ORD-138', status: 'voided', type: 'takeout', customer: 'Jessica Lee', server: 'Lisa Park', total: 34.20, createdAt: '2024-01-15T13:30:00Z' },
{ id: '6', orderNumber: 'ORD-137', status: 'completed', type: 'dine_in', customer: 'David Wilson', server: 'Tom Wilson', total: 68.90, createdAt: '2024-01-15T13:15:00Z' },
{ id: '7', orderNumber: 'ORD-136', status: 'completed', type: 'curbside', customer: 'Amy Chen', server: 'Mike Chen', total: 52.40, createdAt: '2024-01-15T13:00:00Z' },
{ id: '8', orderNumber: 'ORD-135', status: 'completed', type: 'dine_in', customer: 'Robert Taylor', server: 'Lisa Park', total: 89.25, createdAt: '2024-01-15T12:45:00Z' },
];
setOrders(mockOrders);
}, []);
const filteredOrders = orders.filter((order) => {
const matchesSearch =
order.orderNumber.toLowerCase().includes(searchQuery.toLowerCase()) ||
order.customer.toLowerCase().includes(searchQuery.toLowerCase());
const matchesStatus = statusFilter === 'all' || order.status === statusFilter;
const matchesType = typeFilter === 'all' || order.type === typeFilter;
return matchesSearch && matchesStatus && matchesType;
});
const getStatusColor = (status: string) => {
const colors: Record<string, string> = {
draft: '#6b7280',
pending: '#fbbf24',
in_progress: '#3b82f6',
ready: '#10b981',
completed: '#10b981',
voided: '#ef4444',
};
return colors[status] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<h1>📋 Order History</h1>
<p>Search and filter all orders</p>
</header>
<div className="filters">
<input
type="text"
className="search-input"
placeholder="Search by order number or customer..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<select
className="filter-select"
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
>
<option value="all">All Statuses</option>
<option value="pending">Pending</option>
<option value="in_progress">In Progress</option>
<option value="ready">Ready</option>
<option value="completed">Completed</option>
<option value="voided">Voided</option>
</select>
<select
className="filter-select"
value={typeFilter}
onChange={(e) => setTypeFilter(e.target.value)}
>
<option value="all">All Types</option>
<option value="dine_in">Dine In</option>
<option value="takeout">Takeout</option>
<option value="delivery">Delivery</option>
<option value="curbside">Curbside</option>
</select>
</div>
<div className="results-info">
Showing {filteredOrders.length} of {orders.length} orders
</div>
<div className="table-container">
<table className="orders-table">
<thead>
<tr>
<th>Order #</th>
<th>Customer</th>
<th>Type</th>
<th>Server</th>
<th>Status</th>
<th>Total</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{filteredOrders.map((order) => (
<tr key={order.id}>
<td className="order-number">{order.orderNumber}</td>
<td>{order.customer}</td>
<td>
<span className="type-badge">
{order.type.replace('_', ' ')}
</span>
</td>
<td>{order.server}</td>
<td>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(order.status) }}
>
{order.status.replace('_', ' ')}
</span>
</td>
<td className="total">${order.total.toFixed(2)}</td>
<td className="time">
{new Date(order.createdAt).toLocaleTimeString()}
</td>
<td>
<button className="action-btn">View</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{filteredOrders.length === 0 && (
<div className="no-results">
No orders found matching your filters
</div>
)}
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Order Grid - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,197 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.search-input {
flex: 1;
min-width: 300px;
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
}
.search-input:focus {
outline: none;
border-color: #3b82f6;
}
.search-input::placeholder {
color: #64748b;
}
.filter-select {
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
min-width: 150px;
}
.filter-select:focus {
outline: none;
border-color: #3b82f6;
}
.results-info {
color: #94a3b8;
margin-bottom: 1rem;
font-size: 0.875rem;
}
.table-container {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
overflow: hidden;
}
.orders-table {
width: 100%;
border-collapse: collapse;
}
.orders-table thead {
background: #0f172a;
}
.orders-table th {
padding: 1rem;
text-align: left;
color: #94a3b8;
font-weight: 600;
font-size: 0.875rem;
text-transform: uppercase;
border-bottom: 2px solid #334155;
}
.orders-table td {
padding: 1rem;
border-bottom: 1px solid #334155;
color: #cbd5e1;
}
.orders-table tbody tr:hover {
background: #0f172a;
}
.orders-table tbody tr:last-child td {
border-bottom: none;
}
.order-number {
font-weight: 600;
color: #f8fafc;
font-family: 'Courier New', monospace;
}
.type-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
background: #334155;
color: #cbd5e1;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: capitalize;
}
.status-badge {
display: inline-block;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
}
.total {
font-weight: 700;
color: #10b981;
font-size: 1.125rem;
}
.time {
color: #94a3b8;
font-size: 0.875rem;
}
.action-btn {
padding: 0.5rem 1rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.375rem;
cursor: pointer;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s;
}
.action-btn:hover {
background: #2563eb;
}
.no-results {
text-align: center;
padding: 3rem;
color: #64748b;
font-size: 1.125rem;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
margin-top: 1rem;
}
@media (max-width: 1024px) {
.table-container {
overflow-x: auto;
}
.orders-table {
min-width: 800px;
}
}

View File

@ -0,0 +1,151 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Reservation {
id: string;
time: string;
name: string;
partySize: number;
status: 'confirmed' | 'pending' | 'seated' | 'completed';
table?: string;
phone: string;
}
export default function ReservationCalendar() {
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
const [view, setView] = useState<'day' | 'week'>('day');
const [reservations, setReservations] = useState<Reservation[]>([]);
useEffect(() => {
// Mock data
setReservations([
{ id: '1', time: '11:00', name: 'Sarah Johnson', partySize: 4, status: 'completed', table: 'Table 5', phone: '555-0101' },
{ id: '2', time: '11:30', name: 'Mike Chen', partySize: 2, status: 'completed', table: 'Table 2', phone: '555-0102' },
{ id: '3', time: '12:00', name: 'Emma Davis', partySize: 6, status: 'seated', table: 'Table 8', phone: '555-0103' },
{ id: '4', time: '12:30', name: 'John Smith', partySize: 3, status: 'seated', table: 'Table 4', phone: '555-0104' },
{ id: '5', time: '13:00', name: 'Lisa Park', partySize: 2, status: 'confirmed', phone: '555-0105' },
{ id: '6', time: '13:30', name: 'David Wilson', partySize: 5, status: 'confirmed', phone: '555-0106' },
{ id: '7', time: '14:00', name: 'Amy Chen', partySize: 4, status: 'pending', phone: '555-0107' },
{ id: '8', time: '18:00', name: 'Robert Taylor', partySize: 8, status: 'confirmed', phone: '555-0108' },
{ id: '9', time: '18:30', name: 'Jessica Lee', partySize: 2, status: 'confirmed', phone: '555-0109' },
{ id: '10', time: '19:00', name: 'Michael Brown', partySize: 6, status: 'confirmed', phone: '555-0110' },
{ id: '11', time: '19:30', name: 'Emily White', partySize: 4, status: 'confirmed', phone: '555-0111' },
{ id: '12', time: '20:00', name: 'Tom Anderson', partySize: 2, status: 'pending', phone: '555-0112' },
]);
}, []);
const timeSlots = [
'11:00', '11:30', '12:00', '12:30', '13:00', '13:30', '14:00', '14:30',
'15:00', '15:30', '16:00', '16:30', '17:00', '17:30', '18:00', '18:30',
'19:00', '19:30', '20:00', '20:30', '21:00', '21:30', '22:00'
];
const getStatusColor = (status: string) => {
const colors = {
pending: '#fbbf24',
confirmed: '#3b82f6',
seated: '#10b981',
completed: '#6b7280',
};
return colors[status as keyof typeof colors] || '#6b7280';
};
const stats = {
total: reservations.length,
pending: reservations.filter(r => r.status === 'pending').length,
confirmed: reservations.filter(r => r.status === 'confirmed').length,
seated: reservations.filter(r => r.status === 'seated').length,
};
return (
<div className="app">
<header className="app-header">
<h1>📅 Reservation Calendar</h1>
<p>Manage reservations by day or week</p>
</header>
<div className="controls">
<input
type="date"
className="date-input"
value={date}
onChange={(e) => setDate(e.target.value)}
/>
<div className="view-toggle">
<button
className={`toggle-btn ${view === 'day' ? 'active' : ''}`}
onClick={() => setView('day')}
>
Day View
</button>
<button
className={`toggle-btn ${view === 'week' ? 'active' : ''}`}
onClick={() => setView('week')}
>
Week View
</button>
</div>
<button className="btn-primary">+ New Reservation</button>
</div>
<div className="stats-bar">
<div className="stat-card">
<span className="stat-label">Total Today</span>
<span className="stat-value">{stats.total}</span>
</div>
<div className="stat-card pending">
<span className="stat-label">Pending</span>
<span className="stat-value">{stats.pending}</span>
</div>
<div className="stat-card confirmed">
<span className="stat-label">Confirmed</span>
<span className="stat-value">{stats.confirmed}</span>
</div>
<div className="stat-card seated">
<span className="stat-label">Seated</span>
<span className="stat-value">{stats.seated}</span>
</div>
</div>
<div className="calendar-container">
<div className="time-column">
{timeSlots.map((time) => (
<div key={time} className="time-slot">
{time}
</div>
))}
</div>
<div className="reservations-column">
{timeSlots.map((time) => {
const slotReservations = reservations.filter(r => r.time === time);
return (
<div key={time} className="reservation-slot">
{slotReservations.map((res) => (
<div
key={res.id}
className="reservation-card"
style={{ borderLeftColor: getStatusColor(res.status) }}
>
<div className="res-header">
<span className="res-name">{res.name}</span>
<span className="res-party">👥 {res.partySize}</span>
</div>
<div className="res-details">
<span
className="res-status"
style={{ backgroundColor: getStatusColor(res.status) }}
>
{res.status}
</span>
{res.table && <span className="res-table">🪑 {res.table}</span>}
</div>
</div>
))}
</div>
);
})}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reservation Calendar - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,242 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.controls {
display: flex;
gap: 1rem;
align-items: center;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.date-input {
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
}
.date-input:focus {
outline: none;
border-color: #3b82f6;
}
.view-toggle {
display: flex;
background: #1e293b;
border-radius: 0.5rem;
overflow: hidden;
}
.toggle-btn {
padding: 0.875rem 1.5rem;
background: transparent;
border: none;
color: #cbd5e1;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
}
.toggle-btn:hover {
background: #334155;
}
.toggle-btn.active {
background: #3b82f6;
color: #fff;
}
.btn-primary {
padding: 0.875rem 1.5rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
margin-left: auto;
}
.btn-primary:hover {
background: #2563eb;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
padding: 1rem;
text-align: center;
}
.stat-card.pending {
border-color: #fbbf24;
}
.stat-card.confirmed {
border-color: #3b82f6;
}
.stat-card.seated {
border-color: #10b981;
}
.stat-label {
display: block;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.stat-value {
display: block;
font-size: 1.75rem;
font-weight: 700;
color: #f8fafc;
}
.calendar-container {
display: grid;
grid-template-columns: 100px 1fr;
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
overflow: hidden;
}
.time-column {
background: #0f172a;
border-right: 2px solid #334155;
}
.time-slot {
height: 60px;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid #334155;
color: #94a3b8;
font-weight: 600;
font-size: 0.875rem;
}
.time-slot:last-child {
border-bottom: none;
}
.reservations-column {
overflow-y: auto;
max-height: 700px;
}
.reservation-slot {
min-height: 60px;
border-bottom: 1px solid #334155;
padding: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.reservation-slot:last-child {
border-bottom: none;
}
.reservation-card {
background: #0f172a;
border-left: 4px solid;
border-radius: 0.5rem;
padding: 0.75rem;
cursor: pointer;
transition: all 0.2s;
}
.reservation-card:hover {
background: #1e293b;
transform: translateX(4px);
}
.res-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.res-name {
color: #f8fafc;
font-weight: 600;
font-size: 0.9375rem;
}
.res-party {
color: #cbd5e1;
font-size: 0.875rem;
}
.res-details {
display: flex;
gap: 0.75rem;
align-items: center;
}
.res-status {
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
}
.res-table {
color: #94a3b8;
font-size: 0.75rem;
}

View File

@ -0,0 +1,197 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Reservation {
id: string;
confirmationNumber: string;
customerName: string;
customerEmail: string;
customerPhone: string;
partySize: number;
date: string;
time: string;
duration: number;
status: 'pending' | 'confirmed' | 'seated' | 'completed' | 'cancelled' | 'no_show';
table?: string;
notes?: string;
specialRequests?: string;
createdAt: string;
seatedAt?: string;
}
export default function ReservationDetail() {
const [reservation, setReservation] = useState<Reservation | null>(null);
useEffect(() => {
// Mock data
setReservation({
id: '1',
confirmationNumber: 'RES-2024-001',
customerName: 'Sarah Johnson',
customerEmail: 'sarah.johnson@email.com',
customerPhone: '(555) 123-4567',
partySize: 6,
date: '2024-02-15',
time: '19:00',
duration: 90,
status: 'confirmed',
table: 'Table 12',
notes: 'Anniversary celebration - please prepare champagne',
specialRequests: 'Window seating preferred, quiet area',
createdAt: '2024-02-10T14:30:00Z',
});
}, []);
if (!reservation) {
return <div className="loading">Loading reservation...</div>;
}
const getStatusColor = (status: string) => {
const colors = {
pending: '#fbbf24',
confirmed: '#3b82f6',
seated: '#10b981',
completed: '#6b7280',
cancelled: '#ef4444',
no_show: '#ef4444',
};
return colors[status as keyof typeof colors] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<div>
<h1>📋 Reservation Detail</h1>
<p>{reservation.confirmationNumber}</p>
</div>
<span
className="status-badge"
style={{ backgroundColor: getStatusColor(reservation.status) }}
>
{reservation.status.toUpperCase().replace('_', ' ')}
</span>
</header>
<div className="content-grid">
<div className="main-panel">
<div className="info-card">
<h2>Guest Information</h2>
<div className="info-grid">
<div className="info-item">
<span className="label">Guest Name</span>
<span className="value">{reservation.customerName}</span>
</div>
<div className="info-item">
<span className="label">Email</span>
<span className="value">{reservation.customerEmail}</span>
</div>
<div className="info-item">
<span className="label">Phone</span>
<span className="value">{reservation.customerPhone}</span>
</div>
<div className="info-item">
<span className="label">Party Size</span>
<span className="value party-size">
👥 {reservation.partySize} guests
</span>
</div>
</div>
</div>
<div className="info-card">
<h2>Reservation Details</h2>
<div className="info-grid">
<div className="info-item">
<span className="label">Date</span>
<span className="value">
{new Date(reservation.date).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</span>
</div>
<div className="info-item">
<span className="label">Time</span>
<span className="value">{reservation.time}</span>
</div>
<div className="info-item">
<span className="label">Duration</span>
<span className="value">{reservation.duration} minutes</span>
</div>
<div className="info-item">
<span className="label">Table Assignment</span>
<span className="value">{reservation.table || 'Not assigned'}</span>
</div>
</div>
</div>
{reservation.specialRequests && (
<div className="info-card">
<h2>Special Requests</h2>
<p className="special-requests">{reservation.specialRequests}</p>
</div>
)}
{reservation.notes && (
<div className="info-card">
<h2>Internal Notes</h2>
<p className="notes">{reservation.notes}</p>
</div>
)}
</div>
<div className="side-panel">
<div className="timeline-card">
<h2>Timeline</h2>
<div className="timeline">
<div className="timeline-item">
<div className="timeline-dot"></div>
<div className="timeline-content">
<div className="timeline-title">Reservation Created</div>
<div className="timeline-time">
{new Date(reservation.createdAt).toLocaleString()}
</div>
</div>
</div>
{reservation.status !== 'pending' && (
<div className="timeline-item active">
<div className="timeline-dot"></div>
<div className="timeline-content">
<div className="timeline-title">Confirmed</div>
<div className="timeline-time">Confirmed by guest</div>
</div>
</div>
)}
{reservation.seatedAt && (
<div className="timeline-item active">
<div className="timeline-dot"></div>
<div className="timeline-content">
<div className="timeline-title">Seated</div>
<div className="timeline-time">
{new Date(reservation.seatedAt).toLocaleString()}
</div>
</div>
</div>
)}
</div>
</div>
<div className="actions-card">
<h2>Actions</h2>
<div className="actions">
<button className="action-btn primary">Seat Guest</button>
<button className="action-btn">Send Reminder</button>
<button className="action-btn">Edit Details</button>
<button className="action-btn">Assign Table</button>
<button className="action-btn danger">Cancel Reservation</button>
<button className="action-btn danger">Mark No-Show</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reservation Detail - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,233 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.25rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
font-family: 'Courier New', monospace;
}
.status-badge {
padding: 0.75rem 1.5rem;
border-radius: 0.5rem;
font-weight: 600;
color: #fff;
font-size: 0.875rem;
}
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 2rem;
}
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
}
}
.main-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.info-card,
.timeline-card,
.actions-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
h2 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1.25rem;
}
.info-grid {
display: grid;
gap: 1.25rem;
}
.info-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
}
.label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
}
.value {
color: #f8fafc;
font-size: 1.125rem;
font-weight: 600;
}
.party-size {
color: #10b981;
font-size: 1.25rem;
}
.special-requests,
.notes {
color: #cbd5e1;
font-size: 1rem;
line-height: 1.6;
padding: 1rem;
background: #0f172a;
border-radius: 0.5rem;
border-left: 3px solid #3b82f6;
}
.notes {
border-left-color: #fbbf24;
}
.side-panel {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.timeline {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.timeline-item {
display: flex;
gap: 1rem;
position: relative;
}
.timeline-item::before {
content: '';
position: absolute;
left: 9px;
top: 30px;
width: 2px;
height: calc(100% + 1.5rem);
background: #334155;
}
.timeline-item:last-child::before {
display: none;
}
.timeline-dot {
width: 20px;
height: 20px;
border-radius: 50%;
background: #334155;
border: 3px solid #1e293b;
flex-shrink: 0;
margin-top: 2px;
}
.timeline-item.active .timeline-dot {
background: #3b82f6;
}
.timeline-content {
flex: 1;
}
.timeline-title {
color: #f8fafc;
font-weight: 600;
margin-bottom: 0.25rem;
}
.timeline-time {
color: #94a3b8;
font-size: 0.875rem;
}
.actions {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.action-btn {
padding: 0.875rem;
border: none;
border-radius: 0.5rem;
font-weight: 600;
font-size: 0.9375rem;
cursor: pointer;
transition: all 0.2s;
background: #334155;
color: #cbd5e1;
}
.action-btn:hover {
background: #475569;
}
.action-btn.primary {
background: #3b82f6;
color: #fff;
}
.action-btn.primary:hover {
background: #2563eb;
}
.action-btn.danger {
background: #7f1d1d;
color: #fca5a5;
}
.action-btn.danger:hover {
background: #991b1b;
}
.loading {
text-align: center;
padding: 3rem;
color: #94a3b8;
font-size: 1.1rem;
}

View File

@ -0,0 +1,138 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Staff {
id: string;
name: string;
role: string;
active: boolean;
shift: 'morning' | 'evening' | 'off';
sales: number;
orders: number;
avgCheck: number;
hours: number;
}
export default function StaffDashboard() {
const [staff, setStaff] = useState<Staff[]>([]);
const [filter, setFilter] = useState<string>('all');
useEffect(() => {
// Mock data
setStaff([
{ id: '1', name: 'Mike Chen', role: 'Server', active: true, shift: 'morning', sales: 2847.50, orders: 32, avgCheck: 89.00, hours: 38.5 },
{ id: '2', name: 'Lisa Park', role: 'Server', active: true, shift: 'evening', sales: 3125.75, orders: 45, avgCheck: 69.46, hours: 42.0 },
{ id: '3', name: 'Tom Wilson', role: 'Bartender', active: true, shift: 'evening', sales: 1834.25, orders: 28, avgCheck: 65.51, hours: 40.0 },
{ id: '4', name: 'Sarah Johnson', role: 'Server', active: false, shift: 'off', sales: 1245.00, orders: 18, avgCheck: 69.17, hours: 24.0 },
{ id: '5', name: 'David Kim', role: 'Host', active: true, shift: 'morning', sales: 0, orders: 0, avgCheck: 0, hours: 35.0 },
{ id: '6', name: 'Emma Davis', role: 'Chef', active: true, shift: 'morning', sales: 0, orders: 156, avgCheck: 0, hours: 44.0 },
{ id: '7', name: 'John Smith', role: 'Server', active: true, shift: 'evening', sales: 2567.80, orders: 38, avgCheck: 67.57, hours: 36.5 },
{ id: '8', name: 'Amy Chen', role: 'Busser', active: true, shift: 'evening', sales: 0, orders: 0, avgCheck: 0, hours: 32.0 },
]);
}, []);
const filteredStaff = filter === 'all' ? staff : staff.filter(s => s.shift === filter || (filter === 'off-duty' && !s.active));
const totalSales = staff.reduce((sum, s) => sum + s.sales, 0);
const activeStaff = staff.filter(s => s.active).length;
const totalOrders = staff.reduce((sum, s) => sum + s.orders, 0);
const getRoleBadgeColor = (role: string) => {
const colors: Record<string, string> = {
Server: '#3b82f6',
Bartender: '#8b5cf6',
Chef: '#ef4444',
Host: '#10b981',
Busser: '#fbbf24',
Manager: '#ec4899',
};
return colors[role] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<h1>👥 Staff Dashboard</h1>
<p>Team performance and shifts</p>
</header>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-label">Active Staff</div>
<div className="stat-value">{activeStaff}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Sales</div>
<div className="stat-value sales">${totalSales.toFixed(2)}</div>
</div>
<div className="stat-card">
<div className="stat-label">Total Orders</div>
<div className="stat-value">{totalOrders}</div>
</div>
<div className="stat-card">
<div className="stat-label">Avg Check</div>
<div className="stat-value">${(totalSales / totalOrders).toFixed(2)}</div>
</div>
</div>
<div className="filters">
{['all', 'morning', 'evening', 'off-duty'].map((shift) => (
<button
key={shift}
className={`filter-btn ${filter === shift ? 'active' : ''}`}
onClick={() => setFilter(shift)}
>
{shift.charAt(0).toUpperCase() + shift.slice(1).replace('-', ' ')}
</button>
))}
</div>
<div className="staff-grid">
{filteredStaff.map((member) => (
<div key={member.id} className={`staff-card ${!member.active ? 'inactive' : ''}`}>
<div className="staff-header">
<div>
<h3>{member.name}</h3>
<span
className="role-badge"
style={{ backgroundColor: getRoleBadgeColor(member.role) }}
>
{member.role}
</span>
</div>
<div className={`shift-indicator ${member.shift}`}>
{member.active ? member.shift : 'Off Duty'}
</div>
</div>
<div className="staff-stats">
<div className="stat">
<span className="stat-label">Sales</span>
<span className="stat-value">${member.sales.toFixed(2)}</span>
</div>
<div className="stat">
<span className="stat-label">Orders</span>
<span className="stat-value">{member.orders}</span>
</div>
<div className="stat">
<span className="stat-label">Avg Check</span>
<span className="stat-value">
{member.avgCheck > 0 ? `$${member.avgCheck.toFixed(2)}` : 'N/A'}
</span>
</div>
<div className="stat">
<span className="stat-label">Hours</span>
<span className="stat-value">{member.hours}h</span>
</div>
</div>
<div className="staff-actions">
<button className="action-btn">View Profile</button>
<button className="action-btn">Schedule</button>
</div>
</div>
))}
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Staff Dashboard - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,214 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.stat-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
}
.stat-value.sales {
color: #10b981;
}
.filters {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.75rem 1.5rem;
background: #1e293b;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
text-transform: capitalize;
}
.filter-btn:hover {
background: #334155;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.staff-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
}
.staff-card {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
transition: all 0.2s;
}
.staff-card.inactive {
opacity: 0.6;
}
.staff-card:hover {
border-color: #3b82f6;
}
.staff-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1.25rem;
padding-bottom: 1rem;
border-bottom: 1px solid #334155;
}
.staff-header h3 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 0.5rem;
}
.role-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 0.375rem;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
}
.shift-indicator {
padding: 0.5rem 1rem;
border-radius: 0.5rem;
font-size: 0.875rem;
font-weight: 600;
text-transform: capitalize;
}
.shift-indicator.morning {
background: #fef3c7;
color: #92400e;
}
.shift-indicator.evening {
background: #dbeafe;
color: #1e40af;
}
.shift-indicator.off {
background: #e5e7eb;
color: #374151;
}
.staff-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
margin-bottom: 1.25rem;
}
.stat {
background: #0f172a;
padding: 0.875rem;
border-radius: 0.5rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.stat .stat-label {
color: #94a3b8;
font-size: 0.75rem;
text-transform: uppercase;
}
.stat .stat-value {
color: #f8fafc;
font-size: 1.25rem;
font-weight: 700;
}
.staff-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
flex: 1;
padding: 0.75rem;
background: #334155;
border: none;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 0.875rem;
font-weight: 600;
transition: all 0.2s;
}
.action-btn:hover {
background: #475569;
}

View File

@ -0,0 +1,144 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Shift {
staffId: string;
staffName: string;
day: string;
startTime: string;
endTime: string;
role: string;
}
export default function StaffSchedule() {
const [shifts, setShifts] = useState<Shift[]>([]);
const [weekStart, setWeekStart] = useState(new Date().toISOString().split('T')[0]);
useEffect(() => {
// Mock data
setShifts([
{ staffId: '1', staffName: 'Mike Chen', day: 'Monday', startTime: '09:00', endTime: '17:00', role: 'Server' },
{ staffId: '1', staffName: 'Mike Chen', day: 'Tuesday', startTime: '09:00', endTime: '17:00', role: 'Server' },
{ staffId: '1', staffName: 'Mike Chen', day: 'Thursday', startTime: '09:00', endTime: '17:00', role: 'Server' },
{ staffId: '2', staffName: 'Lisa Park', day: 'Monday', startTime: '17:00', endTime: '23:00', role: 'Server' },
{ staffId: '2', staffName: 'Lisa Park', day: 'Wednesday', startTime: '17:00', endTime: '23:00', role: 'Server' },
{ staffId: '2', staffName: 'Lisa Park', day: 'Friday', startTime: '17:00', endTime: '23:00', role: 'Server' },
{ staffId: '2', staffName: 'Lisa Park', day: 'Saturday', startTime: '17:00', endTime: '23:00', role: 'Server' },
{ staffId: '3', staffName: 'Tom Wilson', day: 'Tuesday', startTime: '18:00', endTime: '02:00', role: 'Bartender' },
{ staffId: '3', staffName: 'Tom Wilson', day: 'Friday', startTime: '18:00', endTime: '02:00', role: 'Bartender' },
{ staffId: '3', staffName: 'Tom Wilson', day: 'Saturday', startTime: '18:00', endTime: '02:00', role: 'Bartender' },
{ staffId: '4', staffName: 'Emma Davis', day: 'Monday', startTime: '06:00', endTime: '14:00', role: 'Chef' },
{ staffId: '4', staffName: 'Emma Davis', day: 'Tuesday', startTime: '06:00', endTime: '14:00', role: 'Chef' },
{ staffId: '4', staffName: 'Emma Davis', day: 'Wednesday', startTime: '06:00', endTime: '14:00', role: 'Chef' },
{ staffId: '4', staffName: 'Emma Davis', day: 'Thursday', startTime: '06:00', endTime: '14:00', role: 'Chef' },
{ staffId: '4', staffName: 'Emma Davis', day: 'Friday', startTime: '06:00', endTime: '14:00', role: 'Chef' },
{ staffId: '5', staffName: 'David Kim', day: 'Wednesday', startTime: '11:00', endTime: '19:00', role: 'Host' },
{ staffId: '5', staffName: 'David Kim', day: 'Thursday', startTime: '11:00', endTime: '19:00', role: 'Host' },
{ staffId: '5', staffName: 'David Kim', day: 'Friday', startTime: '11:00', endTime: '19:00', role: 'Host' },
{ staffId: '5', staffName: 'David Kim', day: 'Saturday', startTime: '11:00', endTime: '19:00', role: 'Host' },
{ staffId: '5', staffName: 'David Kim', day: 'Sunday', startTime: '11:00', endTime: '19:00', role: 'Host' },
]);
}, []);
const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
const staff = Array.from(new Set(shifts.map(s => s.staffName))).sort();
const getRoleBadgeColor = (role: string) => {
const colors: Record<string, string> = {
Server: '#3b82f6',
Bartender: '#8b5cf6',
Chef: '#ef4444',
Host: '#10b981',
Busser: '#fbbf24',
};
return colors[role] || '#6b7280';
};
return (
<div className="app">
<header className="app-header">
<h1>📋 Staff Schedule</h1>
<p>Weekly schedule grid</p>
</header>
<div className="toolbar">
<input
type="date"
className="date-input"
value={weekStart}
onChange={(e) => setWeekStart(e.target.value)}
/>
<button className="btn-primary">+ Add Shift</button>
</div>
<div className="schedule-container">
<table className="schedule-table">
<thead>
<tr>
<th className="staff-column">Staff Member</th>
{days.map((day) => (
<th key={day}>{day}</th>
))}
</tr>
</thead>
<tbody>
{staff.map((staffName) => {
const staffShifts = shifts.filter(s => s.staffName === staffName);
const role = staffShifts[0]?.role || '';
return (
<tr key={staffName}>
<td className="staff-column">
<div className="staff-info">
<span className="staff-name">{staffName}</span>
<span
className="role-badge"
style={{ backgroundColor: getRoleBadgeColor(role) }}
>
{role}
</span>
</div>
</td>
{days.map((day) => {
const shift = staffShifts.find(s => s.day === day);
return (
<td key={day} className="shift-cell">
{shift ? (
<div className="shift-block">
<div className="shift-time">
{shift.startTime} - {shift.endTime}
</div>
</div>
) : (
<div className="no-shift"></div>
)}
</td>
);
})}
</tr>
);
})}
</tbody>
</table>
</div>
<div className="summary">
<h3>Weekly Summary</h3>
<div className="summary-stats">
<div className="summary-stat">
<span className="stat-label">Total Shifts</span>
<span className="stat-value">{shifts.length}</span>
</div>
<div className="summary-stat">
<span className="stat-label">Staff Members</span>
<span className="stat-value">{staff.length}</span>
</div>
<div className="summary-stat">
<span className="stat-label">Avg Shifts/Person</span>
<span className="stat-value">{(shifts.length / staff.length).toFixed(1)}</span>
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Staff Schedule - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,230 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1800px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.toolbar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
align-items: center;
}
.date-input {
padding: 0.875rem 1.25rem;
background: #1e293b;
border: 2px solid #334155;
color: #f8fafc;
border-radius: 0.5rem;
font-size: 1rem;
cursor: pointer;
}
.date-input:focus {
outline: none;
border-color: #3b82f6;
}
.btn-primary {
padding: 0.875rem 1.5rem;
background: #3b82f6;
border: none;
color: #fff;
border-radius: 0.5rem;
font-weight: 600;
font-size: 1rem;
cursor: pointer;
transition: all 0.2s;
margin-left: auto;
}
.btn-primary:hover {
background: #2563eb;
}
.schedule-container {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
overflow: hidden;
margin-bottom: 2rem;
}
.schedule-table {
width: 100%;
border-collapse: collapse;
}
.schedule-table thead {
background: #0f172a;
}
.schedule-table th {
padding: 1rem;
text-align: left;
color: #f8fafc;
font-weight: 600;
font-size: 0.9375rem;
border-bottom: 2px solid #334155;
}
.schedule-table th.staff-column {
width: 200px;
min-width: 200px;
}
.schedule-table td {
padding: 0.75rem;
border-bottom: 1px solid #334155;
border-right: 1px solid #334155;
}
.schedule-table td:last-child {
border-right: none;
}
.schedule-table tbody tr:hover {
background: #0f172a;
}
.schedule-table tbody tr:last-child td {
border-bottom: none;
}
.staff-column {
background: #0f172a;
}
.staff-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.staff-name {
color: #f8fafc;
font-weight: 600;
font-size: 0.9375rem;
}
.role-badge {
display: inline-block;
padding: 0.25rem 0.625rem;
border-radius: 0.375rem;
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
width: fit-content;
}
.shift-cell {
text-align: center;
vertical-align: middle;
}
.shift-block {
background: #3b82f6;
padding: 0.625rem 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s;
}
.shift-block:hover {
background: #2563eb;
transform: scale(1.02);
}
.shift-time {
color: #fff;
font-size: 0.8125rem;
font-weight: 600;
line-height: 1.4;
}
.no-shift {
color: #475569;
font-size: 1.25rem;
}
.summary {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.summary h3 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.summary-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.summary-stat {
background: #0f172a;
padding: 1.25rem;
border-radius: 0.5rem;
text-align: center;
}
.summary-stat .stat-label {
display: block;
color: #94a3b8;
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 0.5rem;
}
.summary-stat .stat-value {
display: block;
color: #f8fafc;
font-size: 2rem;
font-weight: 700;
}
@media (max-width: 1400px) {
.schedule-container {
overflow-x: auto;
}
.schedule-table {
min-width: 1200px;
}
}

View File

@ -0,0 +1,135 @@
import { useState, useEffect } from 'react';
import './styles.css';
interface Table {
id: string;
number: string;
status: 'available' | 'occupied' | 'reserved' | 'cleaning';
seats: number;
server?: string;
guestCount?: number;
seatedAt?: string;
x: number;
y: number;
}
export default function TableMap() {
const [tables, setTables] = useState<Table[]>([]);
const [filter, setFilter] = useState<string>('all');
useEffect(() => {
// Mock data - positioned in grid layout
setTables([
{ id: '1', number: '1', status: 'occupied', seats: 4, server: 'Mike', guestCount: 4, seatedAt: '12:30 PM', x: 50, y: 50 },
{ id: '2', number: '2', status: 'occupied', seats: 2, server: 'Lisa', guestCount: 2, seatedAt: '12:45 PM', x: 200, y: 50 },
{ id: '3', number: '3', status: 'available', seats: 6, x: 350, y: 50 },
{ id: '4', number: '4', status: 'reserved', seats: 4, x: 500, y: 50 },
{ id: '5', number: '5', status: 'occupied', seats: 2, server: 'Tom', guestCount: 2, seatedAt: '1:00 PM', x: 50, y: 200 },
{ id: '6', number: '6', status: 'available', seats: 4, x: 200, y: 200 },
{ id: '7', number: '7', status: 'cleaning', seats: 4, x: 350, y: 200 },
{ id: '8', number: '8', status: 'occupied', seats: 8, server: 'Mike', guestCount: 6, seatedAt: '12:15 PM', x: 500, y: 200 },
{ id: '9', number: '9', status: 'available', seats: 2, x: 50, y: 350 },
{ id: '10', number: '10', status: 'reserved', seats: 6, x: 200, y: 350 },
{ id: '11', number: '11', status: 'occupied', seats: 4, server: 'Lisa', guestCount: 3, seatedAt: '1:15 PM', x: 350, y: 350 },
{ id: '12', number: '12', status: 'available', seats: 2, x: 500, y: 350 },
]);
}, []);
const statusColors = {
available: '#10b981',
occupied: '#ef4444',
reserved: '#fbbf24',
cleaning: '#6b7280',
};
const statusCounts = {
available: tables.filter(t => t.status === 'available').length,
occupied: tables.filter(t => t.status === 'occupied').length,
reserved: tables.filter(t => t.status === 'reserved').length,
cleaning: tables.filter(t => t.status === 'cleaning').length,
};
const filteredTables = filter === 'all' ? tables : tables.filter(t => t.status === filter);
return (
<div className="app">
<header className="app-header">
<h1>🗺 Table Map</h1>
<p>Floor plan with live table status</p>
</header>
<div className="stats-bar">
<div className="stat-item available">
<span className="stat-value">{statusCounts.available}</span>
<span className="stat-label">Available</span>
</div>
<div className="stat-item occupied">
<span className="stat-value">{statusCounts.occupied}</span>
<span className="stat-label">Occupied</span>
</div>
<div className="stat-item reserved">
<span className="stat-value">{statusCounts.reserved}</span>
<span className="stat-label">Reserved</span>
</div>
<div className="stat-item cleaning">
<span className="stat-value">{statusCounts.cleaning}</span>
<span className="stat-label">Cleaning</span>
</div>
</div>
<div className="filter-bar">
{['all', 'available', 'occupied', 'reserved', 'cleaning'].map((status) => (
<button
key={status}
className={`filter-btn ${filter === status ? 'active' : ''}`}
onClick={() => setFilter(status)}
>
{status.charAt(0).toUpperCase() + status.slice(1)}
</button>
))}
</div>
<div className="floor-plan">
{filteredTables.map((table) => (
<div
key={table.id}
className="table"
style={{
left: `${table.x}px`,
top: `${table.y}px`,
borderColor: statusColors[table.status],
}}
>
<div className="table-number">{table.number}</div>
<div className="table-seats">👥 {table.seats}</div>
<div
className="table-status"
style={{ backgroundColor: statusColors[table.status] }}
>
{table.status}
</div>
{table.status === 'occupied' && (
<div className="table-info">
<div>Server: {table.server}</div>
<div>Guests: {table.guestCount}</div>
<div>{table.seatedAt}</div>
</div>
)}
</div>
))}
</div>
<div className="legend">
<h3>Legend</h3>
<div className="legend-items">
{Object.entries(statusColors).map(([status, color]) => (
<div key={status} className="legend-item">
<div className="legend-color" style={{ backgroundColor: color }}></div>
<span>{status.charAt(0).toUpperCase() + status.slice(1)}</span>
</div>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Table Map - TouchBistro MCP</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>

View File

@ -0,0 +1,9 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);

View File

@ -0,0 +1,208 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
background: #0f172a;
color: #e2e8f0;
line-height: 1.6;
}
.app {
max-width: 1600px;
margin: 0 auto;
padding: 2rem;
}
.app-header {
margin-bottom: 2rem;
}
.app-header h1 {
font-size: 2.5rem;
color: #f8fafc;
margin-bottom: 0.5rem;
}
.app-header p {
color: #94a3b8;
font-size: 1.1rem;
}
.stats-bar {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-item {
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
padding: 1.25rem;
text-align: center;
}
.stat-item.available {
border-color: #10b981;
}
.stat-item.occupied {
border-color: #ef4444;
}
.stat-item.reserved {
border-color: #fbbf24;
}
.stat-item.cleaning {
border-color: #6b7280;
}
.stat-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: #f8fafc;
margin-bottom: 0.25rem;
}
.stat-label {
color: #94a3b8;
font-size: 0.875rem;
text-transform: uppercase;
font-weight: 600;
}
.filter-bar {
display: flex;
gap: 1rem;
margin-bottom: 2rem;
flex-wrap: wrap;
}
.filter-btn {
padding: 0.75rem 1.5rem;
background: #1e293b;
border: 2px solid #334155;
color: #cbd5e1;
border-radius: 0.5rem;
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s;
text-transform: capitalize;
}
.filter-btn:hover {
background: #334155;
}
.filter-btn.active {
background: #3b82f6;
border-color: #3b82f6;
color: #fff;
}
.floor-plan {
position: relative;
background: #1e293b;
border: 2px solid #334155;
border-radius: 0.75rem;
min-height: 500px;
padding: 2rem;
margin-bottom: 2rem;
}
.table {
position: absolute;
width: 120px;
background: #0f172a;
border: 3px solid;
border-radius: 0.75rem;
padding: 1rem;
cursor: pointer;
transition: all 0.2s;
}
.table:hover {
transform: scale(1.05);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
z-index: 10;
}
.table-number {
font-size: 1.5rem;
font-weight: 700;
color: #f8fafc;
text-align: center;
margin-bottom: 0.5rem;
}
.table-seats {
text-align: center;
color: #94a3b8;
font-size: 0.875rem;
margin-bottom: 0.5rem;
}
.table-status {
padding: 0.375rem;
border-radius: 0.375rem;
text-align: center;
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
color: #fff;
margin-bottom: 0.5rem;
}
.table-info {
font-size: 0.75rem;
color: #94a3b8;
text-align: center;
line-height: 1.4;
padding-top: 0.5rem;
border-top: 1px solid #334155;
}
.legend {
background: #1e293b;
border: 1px solid #334155;
border-radius: 0.75rem;
padding: 1.5rem;
}
.legend h3 {
color: #f8fafc;
font-size: 1.25rem;
margin-bottom: 1rem;
}
.legend-items {
display: flex;
gap: 2rem;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.75rem;
}
.legend-color {
width: 24px;
height: 24px;
border-radius: 0.375rem;
}
.legend-item span {
color: #cbd5e1;
font-weight: 500;
text-transform: capitalize;
}

View File

@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"jsx": "react-jsx"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules", "dist", "src/ui/*/dist", "src/ui/*/node_modules"]
}

View File

@ -0,0 +1,245 @@
import React, { useState, useEffect } from 'react';
interface CustomerManagerProps {
onToolCall: (tool: string, args: any) => Promise<any>;
}
export function CustomerManager({ onToolCall }: CustomerManagerProps) {
const [customers, setCustomers] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [showForm, setShowForm] = useState(false);
const [formData, setFormData] = useState({
name: '',
email: '',
phone: '',
mobile: '',
website: '',
});
useEffect(() => {
loadCustomers();
}, []);
async function loadCustomers() {
setLoading(true);
try {
const data = await onToolCall('wave_list_customers', { pageSize: 100 });
setCustomers(data.customers || []);
} catch (error) {
console.error('Failed to load customers:', error);
}
setLoading(false);
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
try {
await onToolCall('wave_create_customer', formData);
setFormData({ name: '', email: '', phone: '', mobile: '', website: '' });
setShowForm(false);
await loadCustomers();
} catch (error) {
console.error('Failed to create customer:', error);
alert('Failed to create customer');
}
}
return (
<div style={{
padding: '24px',
backgroundColor: '#1a1a1a',
color: '#e0e0e0',
minHeight: '100vh',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
<div style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: '24px',
}}>
<div>
<h1 style={{ fontSize: '28px', fontWeight: '700', margin: '0 0 4px 0' }}>
Customers
</h1>
<p style={{ color: '#888', margin: 0 }}>
Manage your customer database
</p>
</div>
<button
onClick={() => setShowForm(!showForm)}
style={{
padding: '10px 20px',
backgroundColor: '#4a9eff',
border: 'none',
borderRadius: '6px',
color: '#fff',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
}}
>
{showForm ? 'Cancel' : '+ New Customer'}
</button>
</div>
{showForm && (
<div style={{
backgroundColor: '#2a2a2a',
borderRadius: '8px',
padding: '24px',
marginBottom: '24px',
border: '1px solid #404040',
}}>
<h2 style={{ fontSize: '20px', fontWeight: '600', marginBottom: '20px' }}>
Create New Customer
</h2>
<form onSubmit={handleSubmit}>
<div style={{ display: 'grid', gap: '16px' }}>
<InputField
label="Customer Name"
value={formData.name}
onChange={(v) => setFormData({ ...formData, name: v })}
required
/>
<InputField
label="Email"
type="email"
value={formData.email}
onChange={(v) => setFormData({ ...formData, email: v })}
/>
<InputField
label="Phone"
value={formData.phone}
onChange={(v) => setFormData({ ...formData, phone: v })}
/>
<InputField
label="Mobile"
value={formData.mobile}
onChange={(v) => setFormData({ ...formData, mobile: v })}
/>
<InputField
label="Website"
value={formData.website}
onChange={(v) => setFormData({ ...formData, website: v })}
/>
</div>
<button
type="submit"
style={{
marginTop: '20px',
padding: '10px 24px',
backgroundColor: '#22c55e',
border: 'none',
borderRadius: '6px',
color: '#fff',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
}}
>
Create Customer
</button>
</form>
</div>
)}
<div style={{
backgroundColor: '#2a2a2a',
borderRadius: '8px',
padding: '24px',
border: '1px solid #404040',
}}>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>Loading...</div>
) : customers.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
No customers found. Create your first customer!
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #404040' }}>
<th style={headerStyle}>Name</th>
<th style={headerStyle}>Email</th>
<th style={headerStyle}>Phone</th>
<th style={headerStyle}>Currency</th>
</tr>
</thead>
<tbody>
{customers.map(customer => (
<tr key={customer.id} style={{ borderBottom: '1px solid #333' }}>
<td style={cellStyle}>{customer.name}</td>
<td style={cellStyle}>{customer.email || '-'}</td>
<td style={cellStyle}>{customer.phone || customer.mobile || '-'}</td>
<td style={cellStyle}>{customer.currency?.code || '-'}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}
function InputField({
label,
value,
onChange,
type = 'text',
required = false,
}: {
label: string;
value: string;
onChange: (v: string) => void;
type?: string;
required?: boolean;
}) {
return (
<div>
<label style={{
display: 'block',
marginBottom: '6px',
fontSize: '14px',
fontWeight: '500',
}}>
{label} {required && <span style={{ color: '#ef4444' }}>*</span>}
</label>
<input
type={type}
value={value}
onChange={(e) => onChange(e.target.value)}
required={required}
style={{
width: '100%',
padding: '10px 12px',
backgroundColor: '#1a1a1a',
border: '1px solid #404040',
borderRadius: '6px',
color: '#e0e0e0',
fontSize: '14px',
}}
/>
</div>
);
}
const headerStyle: React.CSSProperties = {
textAlign: 'left',
padding: '12px',
fontSize: '13px',
fontWeight: '600',
color: '#888',
textTransform: 'uppercase',
};
const cellStyle: React.CSSProperties = {
padding: '12px',
fontSize: '14px',
color: '#e0e0e0',
};

View File

@ -0,0 +1,302 @@
import React, { useState, useEffect } from 'react';
interface DashboardProps {
onToolCall: (tool: string, args: any) => Promise<any>;
}
export function Dashboard({ onToolCall }: DashboardProps) {
const [businesses, setBusinesses] = useState<any[]>([]);
const [selectedBusiness, setSelectedBusiness] = useState<any>(null);
const [invoices, setInvoices] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [stats, setStats] = useState({
totalInvoices: 0,
paidInvoices: 0,
overdueInvoices: 0,
totalRevenue: 0,
});
useEffect(() => {
loadData();
}, []);
async function loadData() {
setLoading(true);
try {
const bizData = await onToolCall('wave_list_businesses', {});
setBusinesses(bizData);
if (bizData.length > 0) {
setSelectedBusiness(bizData[0]);
await loadBusinessData(bizData[0].id);
}
} catch (error) {
console.error('Failed to load data:', error);
}
setLoading(false);
}
async function loadBusinessData(businessId: string) {
try {
const invoiceData = await onToolCall('wave_list_invoices', {
businessId,
pageSize: 100
});
setInvoices(invoiceData.invoices || []);
// Calculate stats
const total = invoiceData.invoices?.length || 0;
const paid = invoiceData.invoices?.filter((i: any) => i.status === 'PAID').length || 0;
const overdue = invoiceData.invoices?.filter((i: any) => i.status === 'OVERDUE').length || 0;
const revenue = invoiceData.invoices
?.filter((i: any) => i.status === 'PAID')
.reduce((sum: number, i: any) => sum + parseFloat(i.total?.value || 0), 0) || 0;
setStats({
totalInvoices: total,
paidInvoices: paid,
overdueInvoices: overdue,
totalRevenue: revenue,
});
} catch (error) {
console.error('Failed to load business data:', error);
}
}
if (loading) {
return (
<div style={{
padding: '24px',
backgroundColor: '#1a1a1a',
color: '#e0e0e0',
minHeight: '100vh',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}>
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<div style={{ fontSize: '18px' }}>Loading Wave dashboard...</div>
</div>
</div>
);
}
return (
<div style={{
padding: '24px',
backgroundColor: '#1a1a1a',
color: '#e0e0e0',
minHeight: '100vh',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
<div style={{ marginBottom: '32px' }}>
<h1 style={{
fontSize: '32px',
fontWeight: '700',
margin: '0 0 8px 0',
color: '#ffffff',
}}>
Wave Accounting Dashboard
</h1>
<p style={{ color: '#888', margin: 0 }}>
Overview of your business financials
</p>
</div>
{/* Business Selector */}
{businesses.length > 1 && (
<div style={{ marginBottom: '24px' }}>
<label style={{
display: 'block',
marginBottom: '8px',
fontSize: '14px',
fontWeight: '500',
}}>
Select Business
</label>
<select
value={selectedBusiness?.id || ''}
onChange={(e) => {
const biz = businesses.find(b => b.id === e.target.value);
setSelectedBusiness(biz);
if (biz) loadBusinessData(biz.id);
}}
style={{
padding: '10px 12px',
backgroundColor: '#2a2a2a',
border: '1px solid #404040',
borderRadius: '6px',
color: '#e0e0e0',
fontSize: '14px',
width: '100%',
maxWidth: '400px',
}}
>
{businesses.map(biz => (
<option key={biz.id} value={biz.id}>
{biz.name} ({biz.currency.code})
</option>
))}
</select>
</div>
)}
{selectedBusiness && (
<>
{/* Stats Grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '20px',
marginBottom: '32px',
}}>
<StatCard
title="Total Invoices"
value={stats.totalInvoices.toString()}
color="#4a9eff"
/>
<StatCard
title="Paid Invoices"
value={stats.paidInvoices.toString()}
color="#22c55e"
/>
<StatCard
title="Overdue"
value={stats.overdueInvoices.toString()}
color="#ef4444"
/>
<StatCard
title="Total Revenue"
value={`${selectedBusiness.currency.symbol}${stats.totalRevenue.toLocaleString()}`}
color="#8b5cf6"
/>
</div>
{/* Recent Invoices */}
<div style={{
backgroundColor: '#2a2a2a',
borderRadius: '8px',
padding: '24px',
border: '1px solid #404040',
}}>
<h2 style={{
fontSize: '20px',
fontWeight: '600',
margin: '0 0 20px 0',
color: '#ffffff',
}}>
Recent Invoices
</h2>
{invoices.length === 0 ? (
<p style={{ color: '#888', margin: 0 }}>No invoices found</p>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #404040' }}>
<th style={tableHeaderStyle}>Invoice #</th>
<th style={tableHeaderStyle}>Customer</th>
<th style={tableHeaderStyle}>Date</th>
<th style={tableHeaderStyle}>Status</th>
<th style={tableHeaderStyle}>Amount</th>
<th style={tableHeaderStyle}>Amount Due</th>
</tr>
</thead>
<tbody>
{invoices.slice(0, 10).map(invoice => (
<tr key={invoice.id} style={{ borderBottom: '1px solid #333' }}>
<td style={tableCellStyle}>{invoice.invoiceNumber}</td>
<td style={tableCellStyle}>{invoice.customer.name}</td>
<td style={tableCellStyle}>
{new Date(invoice.invoiceDate).toLocaleDateString()}
</td>
<td style={tableCellStyle}>
<StatusBadge status={invoice.status} />
</td>
<td style={tableCellStyle}>
{invoice.total.currency.code} {parseFloat(invoice.total.value).toFixed(2)}
</td>
<td style={tableCellStyle}>
{invoice.amountDue.currency.code} {parseFloat(invoice.amountDue.value).toFixed(2)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</>
)}
</div>
</div>
);
}
function StatCard({ title, value, color }: { title: string; value: string; color: string }) {
return (
<div style={{
backgroundColor: '#2a2a2a',
borderRadius: '8px',
padding: '20px',
border: '1px solid #404040',
}}>
<div style={{
fontSize: '14px',
color: '#888',
marginBottom: '8px',
fontWeight: '500',
}}>
{title}
</div>
<div style={{
fontSize: '28px',
fontWeight: '700',
color: color,
}}>
{value}
</div>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
DRAFT: '#6b7280',
SAVED: '#3b82f6',
SENT: '#8b5cf6',
VIEWED: '#06b6d4',
PAID: '#22c55e',
PARTIAL: '#f59e0b',
OVERDUE: '#ef4444',
UNPAID: '#f59e0b',
};
return (
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '600',
backgroundColor: `${colors[status] || '#6b7280'}20`,
color: colors[status] || '#6b7280',
}}>
{status}
</span>
);
}
const tableHeaderStyle: React.CSSProperties = {
textAlign: 'left',
padding: '12px',
fontSize: '13px',
fontWeight: '600',
color: '#888',
textTransform: 'uppercase',
letterSpacing: '0.5px',
};
const tableCellStyle: React.CSSProperties = {
padding: '12px',
fontSize: '14px',
color: '#e0e0e0',
};

View File

@ -0,0 +1,190 @@
import React, { useState, useEffect } from 'react';
interface InvoiceManagerProps {
onToolCall: (tool: string, args: any) => Promise<any>;
}
export function InvoiceManager({ onToolCall }: InvoiceManagerProps) {
const [invoices, setInvoices] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState<string>('ALL');
useEffect(() => {
loadInvoices();
}, []);
async function loadInvoices() {
setLoading(true);
try {
const data = await onToolCall('wave_list_invoices', { pageSize: 100 });
setInvoices(data.invoices || []);
} catch (error) {
console.error('Failed to load invoices:', error);
}
setLoading(false);
}
const filteredInvoices = filter === 'ALL'
? invoices
: invoices.filter(inv => inv.status === filter);
const statusCounts = {
ALL: invoices.length,
DRAFT: invoices.filter(i => i.status === 'DRAFT').length,
SENT: invoices.filter(i => i.status === 'SENT').length,
PAID: invoices.filter(i => i.status === 'PAID').length,
OVERDUE: invoices.filter(i => i.status === 'OVERDUE').length,
};
return (
<div style={{
padding: '24px',
backgroundColor: '#1a1a1a',
color: '#e0e0e0',
minHeight: '100vh',
fontFamily: 'system-ui, -apple-system, sans-serif',
}}>
<div style={{ maxWidth: '1400px', margin: '0 auto' }}>
<h1 style={{ fontSize: '28px', fontWeight: '700', marginBottom: '24px' }}>
Invoices
</h1>
{/* Filter Tabs */}
<div style={{
display: 'flex',
gap: '12px',
marginBottom: '24px',
borderBottom: '1px solid #404040',
}}>
{(['ALL', 'DRAFT', 'SENT', 'PAID', 'OVERDUE'] as const).map(status => (
<button
key={status}
onClick={() => setFilter(status)}
style={{
padding: '12px 20px',
backgroundColor: 'transparent',
border: 'none',
borderBottom: filter === status ? '2px solid #4a9eff' : '2px solid transparent',
color: filter === status ? '#4a9eff' : '#888',
fontSize: '14px',
fontWeight: '600',
cursor: 'pointer',
transition: 'all 0.2s',
}}
>
{status} ({statusCounts[status]})
</button>
))}
</div>
<div style={{
backgroundColor: '#2a2a2a',
borderRadius: '8px',
padding: '24px',
border: '1px solid #404040',
}}>
{loading ? (
<div style={{ textAlign: 'center', padding: '40px' }}>Loading...</div>
) : filteredInvoices.length === 0 ? (
<div style={{ textAlign: 'center', padding: '40px', color: '#888' }}>
No {filter.toLowerCase()} invoices found
</div>
) : (
<div style={{ overflowX: 'auto' }}>
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
<thead>
<tr style={{ borderBottom: '1px solid #404040' }}>
<th style={headerStyle}>Invoice #</th>
<th style={headerStyle}>Customer</th>
<th style={headerStyle}>Date</th>
<th style={headerStyle}>Due Date</th>
<th style={headerStyle}>Status</th>
<th style={headerStyle}>Total</th>
<th style={headerStyle}>Amount Due</th>
<th style={headerStyle}>Actions</th>
</tr>
</thead>
<tbody>
{filteredInvoices.map(invoice => (
<tr key={invoice.id} style={{ borderBottom: '1px solid #333' }}>
<td style={cellStyle}>{invoice.invoiceNumber}</td>
<td style={cellStyle}>{invoice.customer.name}</td>
<td style={cellStyle}>
{new Date(invoice.invoiceDate).toLocaleDateString()}
</td>
<td style={cellStyle}>
{invoice.dueDate ? new Date(invoice.dueDate).toLocaleDateString() : '-'}
</td>
<td style={cellStyle}>
<StatusBadge status={invoice.status} />
</td>
<td style={cellStyle}>
{invoice.total.currency.code} {parseFloat(invoice.total.value).toFixed(2)}
</td>
<td style={cellStyle}>
{invoice.amountDue.currency.code} {parseFloat(invoice.amountDue.value).toFixed(2)}
</td>
<td style={cellStyle}>
{invoice.viewUrl && (
<a
href={invoice.viewUrl}
target="_blank"
rel="noopener noreferrer"
style={{ color: '#4a9eff', textDecoration: 'none' }}
>
View
</a>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const colors: Record<string, string> = {
DRAFT: '#6b7280',
SAVED: '#3b82f6',
SENT: '#8b5cf6',
VIEWED: '#06b6d4',
PAID: '#22c55e',
PARTIAL: '#f59e0b',
OVERDUE: '#ef4444',
UNPAID: '#f59e0b',
};
return (
<span style={{
padding: '4px 8px',
borderRadius: '4px',
fontSize: '12px',
fontWeight: '600',
backgroundColor: `${colors[status] || '#6b7280'}20`,
color: colors[status] || '#6b7280',
}}>
{status}
</span>
);
}
const headerStyle: React.CSSProperties = {
textAlign: 'left',
padding: '12px',
fontSize: '13px',
fontWeight: '600',
color: '#888',
textTransform: 'uppercase',
};
const cellStyle: React.CSSProperties = {
padding: '12px',
fontSize: '14px',
color: '#e0e0e0',
};

View File

@ -0,0 +1,44 @@
import { GraphQLClient } from 'graphql-request';
import type { WaveConfig } from '../types/index.js';
export class WaveClient {
private client: GraphQLClient;
private config: WaveConfig;
constructor(config: WaveConfig) {
this.config = config;
this.client = new GraphQLClient('https://gql.waveapps.com/graphql/public', {
headers: {
Authorization: `Bearer ${config.accessToken}`,
'Content-Type': 'application/json',
},
});
}
async query<T = any>(query: string, variables?: any): Promise<T> {
try {
return await this.client.request<T>(query, variables);
} catch (error: any) {
throw new Error(`Wave API Error: ${error.message}`);
}
}
async mutate<T = any>(mutation: string, variables?: any): Promise<T> {
try {
return await this.client.request<T>(mutation, variables);
} catch (error: any) {
throw new Error(`Wave API Error: ${error.message}`);
}
}
getBusinessId(): string {
if (!this.config.businessId) {
throw new Error('Business ID is required but not configured');
}
return this.config.businessId;
}
setBusinessId(businessId: string) {
this.config.businessId = businessId;
}
}

File diff suppressed because it is too large Load Diff