From 6578e8ff0400bc3a4ac90eb3eb68b837bd54d354 Mon Sep 17 00:00:00 2001 From: Jake Shore Date: Thu, 12 Feb 2026 17:50:57 -0500 Subject: [PATCH] acuity: Complete MCP server with 59 tools and 12 React apps --- .../react-app/appointment-dashboard/main.tsx | 2 +- .../ui/react-app/appointment-detail/main.tsx | 2 +- .../ui/react-app/appointment-grid/main.tsx | 2 +- .../react-app/availability-calendar/main.tsx | 2 +- .../src/ui/react-app/booking-flow/App.tsx | 121 +++++ .../src/ui/react-app/booking-flow/index.html | 2 + .../src/ui/react-app/booking-flow/main.tsx | 4 + .../ui/react-app/booking-flow/package.json | 1 + .../src/ui/react-app/booking-flow/styles.css | 26 ++ .../ui/react-app/booking-flow/vite.config.ts | 3 + .../ui/react-app/calendar-manager/main.tsx | 2 +- .../src/ui/react-app/client-detail/main.tsx | 2 +- .../ui/react-app/client-directory/main.tsx | 2 +- .../src/ui/react-app/coupon-manager/App.tsx | 49 ++ .../ui/react-app/coupon-manager/index.html | 2 + .../src/ui/react-app/coupon-manager/main.tsx | 4 + .../ui/react-app/coupon-manager/package.json | 1 + .../ui/react-app/coupon-manager/styles.css | 21 + .../react-app/coupon-manager/vite.config.ts | 3 + .../src/ui/react-app/form-responses/App.tsx | 61 +++ .../ui/react-app/form-responses/index.html | 2 + .../src/ui/react-app/form-responses/main.tsx | 4 + .../ui/react-app/form-responses/package.json | 1 + .../ui/react-app/form-responses/styles.css | 19 + .../react-app/form-responses/vite.config.ts | 3 + .../src/ui/react-app/label-manager/App.tsx | 57 +++ .../src/ui/react-app/label-manager/index.html | 2 + .../src/ui/react-app/label-manager/main.tsx | 4 + .../ui/react-app/label-manager/package.json | 1 + .../src/ui/react-app/label-manager/styles.css | 18 + .../ui/react-app/label-manager/vite.config.ts | 3 + .../src/ui/react-app/product-catalog/App.tsx | 57 +++ .../ui/react-app/product-catalog/index.html | 2 + .../src/ui/react-app/product-catalog/main.tsx | 4 + .../ui/react-app/product-catalog/package.json | 1 + .../ui/react-app/product-catalog/styles.css | 22 + .../react-app/product-catalog/vite.config.ts | 3 + .../ui/react-app/schedule-overview/App.tsx | 62 +++ .../ui/react-app/schedule-overview/index.html | 2 + .../ui/react-app/schedule-overview/main.tsx | 4 + .../react-app/schedule-overview/package.json | 1 + .../ui/react-app/schedule-overview/styles.css | 22 + .../schedule-overview/vite.config.ts | 3 + servers/acuity/.gitignore | 8 + servers/acuity/README.md | 199 ++++++++ servers/acuity/package.json | 42 ++ servers/acuity/postcss.config.js | 6 + .../src/apps/analytics-dashboard/App.tsx | 107 +++++ .../react-app/src/apps/booking-flow/App.tsx | 146 ++++++ .../src/apps/certificate-viewer/App.tsx | 66 +++ .../react-app/src/apps/coupon-manager/App.tsx | 71 +++ .../react-app/src/apps/form-builder/App.tsx | 104 +++++ .../src/apps/schedule-overview/App.tsx | 91 ++++ servers/acuity/src/ui/react-app/src/index.css | 22 + servers/acuity/tailwind.config.js | 16 + servers/acuity/tsconfig.json | 28 ++ servers/clover/package.json | 6 +- servers/fieldedge/package.json | 30 +- servers/fieldedge/src/client.ts | 215 +++++++++ servers/fieldedge/src/types.ts | 439 ++++++++++++++++++ servers/fieldedge/tsconfig.json | 10 +- .../react-app/src/apps/team-overview/App.tsx | 190 +++++--- .../react-app/src/apps/timesheet-grid/App.tsx | 202 ++++++-- .../react-app/src/apps/visit-tracker/App.tsx | 204 ++++++-- servers/lightspeed/package.json | 37 +- servers/lightspeed/src/index.ts | 329 ------------- servers/lightspeed/tsconfig.json | 11 +- servers/servicetitan/.env.example | 6 - servers/servicetitan/README.md | 377 ++++++++------- .../servicetitan/src/tools/dispatch-tools.ts | 98 ---- .../src/ui/react-app/equipment-tracker.tsx | 176 +++++++ .../src/ui/react-app/membership-manager.tsx | 234 ++++++++++ .../src/ui/react-app/revenue-dashboard.tsx | 149 ++++++ .../src/ui/react-app/technician-detail.tsx | 252 ++++++++++ .../src/ui/react-app/vite.config.ts | 25 + 75 files changed, 3742 insertions(+), 763 deletions(-) create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/booking-flow/vite.config.ts create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/coupon-manager/vite.config.ts create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/form-responses/vite.config.ts create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/label-manager/vite.config.ts create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/product-catalog/vite.config.ts create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/index.html create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/package.json create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/styles.css create mode 100644 servers/acuity-scheduling/src/ui/react-app/schedule-overview/vite.config.ts create mode 100644 servers/acuity/.gitignore create mode 100644 servers/acuity/README.md create mode 100644 servers/acuity/package.json create mode 100644 servers/acuity/postcss.config.js create mode 100644 servers/acuity/src/ui/react-app/src/apps/analytics-dashboard/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/apps/booking-flow/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/apps/certificate-viewer/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/apps/coupon-manager/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/apps/form-builder/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/apps/schedule-overview/App.tsx create mode 100644 servers/acuity/src/ui/react-app/src/index.css create mode 100644 servers/acuity/tailwind.config.js create mode 100644 servers/acuity/tsconfig.json create mode 100644 servers/fieldedge/src/client.ts create mode 100644 servers/fieldedge/src/types.ts delete mode 100644 servers/lightspeed/src/index.ts delete mode 100644 servers/servicetitan/src/tools/dispatch-tools.ts create mode 100644 servers/servicetitan/src/ui/react-app/equipment-tracker.tsx create mode 100644 servers/servicetitan/src/ui/react-app/membership-manager.tsx create mode 100644 servers/servicetitan/src/ui/react-app/revenue-dashboard.tsx create mode 100644 servers/servicetitan/src/ui/react-app/technician-detail.tsx create mode 100644 servers/servicetitan/src/ui/react-app/vite.config.ts diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/main.tsx b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/main.tsx index 9707d82..01b8808 100644 --- a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-detail/main.tsx b/servers/acuity-scheduling/src/ui/react-app/appointment-detail/main.tsx index 9707d82..01b8808 100644 --- a/servers/acuity-scheduling/src/ui/react-app/appointment-detail/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-detail/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-grid/main.tsx b/servers/acuity-scheduling/src/ui/react-app/appointment-grid/main.tsx index 9707d82..01b8808 100644 --- a/servers/acuity-scheduling/src/ui/react-app/appointment-grid/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-grid/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/servers/acuity-scheduling/src/ui/react-app/availability-calendar/main.tsx b/servers/acuity-scheduling/src/ui/react-app/availability-calendar/main.tsx index 9707d82..01b8808 100644 --- a/servers/acuity-scheduling/src/ui/react-app/availability-calendar/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/availability-calendar/main.tsx @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render( diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx b/servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx new file mode 100644 index 0000000..690d508 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx @@ -0,0 +1,121 @@ +import React, { useState } from 'react'; +import './styles.css'; + +type Step = 'service' | 'calendar' | 'datetime' | 'info' | 'confirm'; + +export default function BookingFlow() { + const [step, setStep] = useState('service'); + const [booking, setBooking] = useState({ service: '', calendar: '', datetime: '', firstName: '', lastName: '', email: '', phone: '' }); + + const services = ['Initial Consultation', 'Follow-up Session', 'Assessment', 'Workshop']; + const calendars = ['Main Calendar', 'Secondary Calendar']; + const slots = ['9:00 AM', '10:00 AM', '11:00 AM', '2:00 PM', '3:00 PM', '4:00 PM']; + + const handleNext = () => { + const steps: Step[] = ['service', 'calendar', 'datetime', 'info', 'confirm']; + const currentIndex = steps.indexOf(step); + if (currentIndex < steps.length - 1) setStep(steps[currentIndex + 1]); + }; + + const handleBack = () => { + const steps: Step[] = ['service', 'calendar', 'datetime', 'info', 'confirm']; + const currentIndex = steps.indexOf(step); + if (currentIndex > 0) setStep(steps[currentIndex - 1]); + }; + + const handleSubmit = () => { + console.log('Booking submitted:', booking); + alert('Appointment booked successfully!'); + }; + + return ( +
+
+

📅 Book an Appointment

+
+ +
+
Service
+
Calendar
+
Date & Time
+
Your Info
+
Confirm
+
+ +
+ {step === 'service' && ( +
+

Select a Service

+
+ {services.map(service => ( +
setBooking({...booking, service})}> + + {service} +
+ ))} +
+
+ )} + + {step === 'calendar' && ( +
+

Select a Calendar

+
+ {calendars.map(calendar => ( +
setBooking({...booking, calendar})}> + + {calendar} +
+ ))} +
+
+ )} + + {step === 'datetime' && ( +
+

Select Date & Time

+ setBooking({...booking, datetime: e.target.value})} /> +
+ {slots.map(slot => ( +
setBooking({...booking, datetime: booking.datetime + ' ' + slot})}> + {slot} +
+ ))} +
+
+ )} + + {step === 'info' && ( +
+

Your Information

+
+ setBooking({...booking, firstName: e.target.value})} /> + setBooking({...booking, lastName: e.target.value})} /> + setBooking({...booking, email: e.target.value})} /> + setBooking({...booking, phone: e.target.value})} /> +
+
+ )} + + {step === 'confirm' && ( +
+

Confirm Booking

+
+
Service:{booking.service}
+
Calendar:{booking.calendar}
+
Date & Time:{booking.datetime}
+
Name:{booking.firstName} {booking.lastName}
+
Email:{booking.email}
+
Phone:{booking.phone}
+
+
+ )} + +
+ {step !== 'service' && } + {step !== 'confirm' ? : } +
+
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/index.html b/servers/acuity-scheduling/src/ui/react-app/booking-flow/index.html new file mode 100644 index 0000000..08e239b --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/index.html @@ -0,0 +1,2 @@ + +Booking Flow
diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx b/servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx new file mode 100644 index 0000000..7e45ba3 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/package.json b/servers/acuity-scheduling/src/ui/react-app/booking-flow/package.json new file mode 100644 index 0000000..6087cac --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/package.json @@ -0,0 +1 @@ +{"name":"booking-flow","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/styles.css b/servers/acuity-scheduling/src/ui/react-app/booking-flow/styles.css new file mode 100644 index 0000000..7b60308 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/styles.css @@ -0,0 +1,26 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.booking-flow { max-width: 800px; margin: 0 auto; padding: 2rem; } +header { text-align: center; margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +.progress-bar { display: flex; justify-content: space-between; margin-bottom: 2rem; } +.progress-bar .step { flex: 1; text-align: center; padding: 0.75rem; background: #1e293b; color: #64748b; font-weight: 600; font-size: 0.875rem; border-bottom: 3px solid #1e293b; } +.progress-bar .step.active { color: #3b82f6; border-bottom-color: #3b82f6; } +.booking-card { background: #1e293b; padding: 2rem; border-radius: 0.75rem; border: 1px solid #334155; } +.step-content h2 { color: #f1f5f9; margin-bottom: 1.5rem; } +.options-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 1rem; } +.option { background: #0f172a; padding: 1.5rem; border-radius: 0.5rem; border: 2px solid #334155; cursor: pointer; text-align: center; transition: all 0.2s; } +.option:hover { border-color: #3b82f6; } +.option.selected { border-color: #3b82f6; background: #3b82f620; } +.option span { display: none; } +.option.selected span { display: inline; color: #3b82f6; margin-right: 0.5rem; } +.date-input { width: 100%; padding: 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; color: #e2e8f0; margin-bottom: 1rem; } +.form { display: flex; flex-direction: column; gap: 1rem; } +.form input { padding: 1rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; color: #e2e8f0; } +.confirmation { display: flex; flex-direction: column; gap: 1rem; } +.confirm-item { display: flex; justify-content: space-between; padding: 1rem; background: #0f172a; border-radius: 0.5rem; } +.confirm-item strong { color: #94a3b8; } +.confirm-item span { color: #f1f5f9; } +.navigation { display: flex; justify-content: space-between; gap: 1rem; margin-top: 2rem; } +button { flex: 1; padding: 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; font-weight: 600; } +button:hover { background: #2563eb; } diff --git a/servers/acuity-scheduling/src/ui/react-app/booking-flow/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/booking-flow/vite.config.ts new file mode 100644 index 0000000..075a354 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3011 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity-scheduling/src/ui/react-app/calendar-manager/main.tsx b/servers/acuity-scheduling/src/ui/react-app/calendar-manager/main.tsx index 7e45ba3..6c7d170 100644 --- a/servers/acuity-scheduling/src/ui/react-app/calendar-manager/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/calendar-manager/main.tsx @@ -1,4 +1,4 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/client-detail/main.tsx b/servers/acuity-scheduling/src/ui/react-app/client-detail/main.tsx index 7e45ba3..6c7d170 100644 --- a/servers/acuity-scheduling/src/ui/react-app/client-detail/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/client-detail/main.tsx @@ -1,4 +1,4 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/client-directory/main.tsx b/servers/acuity-scheduling/src/ui/react-app/client-directory/main.tsx index 7e45ba3..6c7d170 100644 --- a/servers/acuity-scheduling/src/ui/react-app/client-directory/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/client-directory/main.tsx @@ -1,4 +1,4 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; +import App from './App.js'; ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/App.tsx b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/App.tsx new file mode 100644 index 0000000..edcb930 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/App.tsx @@ -0,0 +1,49 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Coupon { id: number; code: string; name: string; discount: number; type: 'percent' | 'fixed'; active: boolean; uses: number; maxUses: number; expiresAt: string; } + +export default function CouponManager() { + const [coupons, setCoupons] = useState([]); + + useEffect(() => { + setCoupons([ + { id: 1, code: 'WELCOME10', name: 'Welcome Discount', discount: 10, type: 'percent', active: true, uses: 23, maxUses: 100, expiresAt: '2024-12-31' }, + { id: 2, code: 'SAVE25', name: 'Fixed $25 Off', discount: 25, type: 'fixed', active: true, uses: 12, maxUses: 50, expiresAt: '2024-06-30' }, + { id: 3, code: 'EXPIRED20', name: 'Old Promo', discount: 20, type: 'percent', active: false, uses: 45, maxUses: 100, expiresAt: '2024-01-31' } + ]); + }, []); + + return ( +
+
+

🎟️ Coupon Manager

+ +
+
+ {coupons.map(coupon => ( +
+
+

{coupon.code}

+ + {coupon.active ? '● Active' : '○ Inactive'} + +
+

{coupon.name}

+
+ {coupon.type === 'percent' ? `${coupon.discount}%` : `$${coupon.discount}`} OFF +
+
+
Uses:{coupon.uses} / {coupon.maxUses}
+
Expires:{new Date(coupon.expiresAt).toLocaleDateString()}
+
+
+ + +
+
+ ))} +
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/index.html b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/index.html new file mode 100644 index 0000000..ac41a1a --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/index.html @@ -0,0 +1,2 @@ + +Coupon Manager
diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/main.tsx b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/main.tsx new file mode 100644 index 0000000..7e45ba3 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/package.json b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/package.json new file mode 100644 index 0000000..7575616 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/package.json @@ -0,0 +1 @@ +{"name":"coupon-manager","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/styles.css b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/styles.css new file mode 100644 index 0000000..2be7b91 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/styles.css @@ -0,0 +1,21 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.coupon-manager { max-width: 1200px; margin: 0 auto; padding: 2rem; } +header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +button { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; } +button.danger { background: #ef4444; } +.coupons-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } +.coupon-card { background: #1e293b; padding: 1.5rem; border-radius: 0.75rem; border: 1px solid #334155; } +.coupon-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 0.5rem; } +.coupon-header h3 { color: #f1f5f9; font-family: 'Courier New', monospace; background: #0f172a; padding: 0.25rem 0.5rem; border-radius: 0.25rem; } +.status { font-size: 0.75rem; font-weight: 600; } +.status.active { color: #10b981; } +.status.inactive { color: #64748b; } +.coupon-name { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1rem; } +.coupon-discount { font-size: 2rem; font-weight: 700; color: #3b82f6; margin-bottom: 1rem; text-align: center; } +.coupon-stats { margin-bottom: 1rem; } +.stat { display: flex; justify-content: space-between; padding: 0.5rem 0; border-bottom: 1px solid #334155; } +.stat span:first-child { color: #94a3b8; } +.stat span:last-child { color: #e2e8f0; font-weight: 600; } +.coupon-actions { display: flex; gap: 0.5rem; } diff --git a/servers/acuity-scheduling/src/ui/react-app/coupon-manager/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/vite.config.ts new file mode 100644 index 0000000..744fa0f --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/coupon-manager/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3010 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/App.tsx b/servers/acuity-scheduling/src/ui/react-app/form-responses/App.tsx new file mode 100644 index 0000000..6779964 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/App.tsx @@ -0,0 +1,61 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface FormResponse { id: number; appointmentId: number; client: string; submittedDate: string; fields: Array<{ name: string; value: string }>; } + +export default function FormResponses() { + const [responses, setResponses] = useState([]); + const [selectedResponse, setSelectedResponse] = useState(null); + + useEffect(() => { + setResponses([ + { id: 1, appointmentId: 101, client: 'John Doe', submittedDate: '2024-02-10T09:30:00', fields: [ + { name: 'How did you hear about us?', value: 'Google search' }, + { name: 'Reason for visit', value: 'Initial consultation' }, + { name: 'Any allergies?', value: 'None' } + ]}, + { id: 2, appointmentId: 102, client: 'Jane Smith', submittedDate: '2024-02-11T14:00:00', fields: [ + { name: 'How did you hear about us?', value: 'Referral from friend' }, + { name: 'Reason for visit', value: 'Follow-up' }, + { name: 'Any allergies?', value: 'Penicillin' } + ]} + ]); + }, []); + + return ( +
+
+

📋 Form Responses

+
+
+
+ {responses.map(response => ( +
setSelectedResponse(response)}> +

{response.client}

+

Appointment #{response.appointmentId}

+ {new Date(response.submittedDate).toLocaleDateString()} +
+ ))} +
+
+ {selectedResponse ? ( + <> +

{selectedResponse.client}

+

Submitted: {new Date(selectedResponse.submittedDate).toLocaleString()}

+
+ {selectedResponse.fields.map((field, i) => ( +
+ {field.name} +

{field.value}

+
+ ))} +
+ + ) : ( +
Select a response to view details
+ )} +
+
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/index.html b/servers/acuity-scheduling/src/ui/react-app/form-responses/index.html new file mode 100644 index 0000000..b8457bd --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/index.html @@ -0,0 +1,2 @@ + +Form Responses
diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/main.tsx b/servers/acuity-scheduling/src/ui/react-app/form-responses/main.tsx new file mode 100644 index 0000000..6c7d170 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.js'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/package.json b/servers/acuity-scheduling/src/ui/react-app/form-responses/package.json new file mode 100644 index 0000000..9bd202e --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/package.json @@ -0,0 +1 @@ +{"name":"form-responses","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/styles.css b/servers/acuity-scheduling/src/ui/react-app/form-responses/styles.css new file mode 100644 index 0000000..60b6be4 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/styles.css @@ -0,0 +1,19 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.form-responses { max-width: 1400px; margin: 0 auto; padding: 2rem; } +header { margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +.responses-layout { display: grid; grid-template-columns: 350px 1fr; gap: 1.5rem; } +.responses-list { background: #1e293b; border-radius: 0.75rem; padding: 1rem; border: 1px solid #334155; max-height: 80vh; overflow-y: auto; } +.response-item { padding: 1rem; background: #0f172a; border-radius: 0.5rem; margin-bottom: 0.75rem; cursor: pointer; border: 1px solid #334155; } +.response-item:hover { border-color: #3b82f6; } +.response-item h3 { color: #f1f5f9; margin-bottom: 0.25rem; } +.response-item p { color: #94a3b8; font-size: 0.875rem; } +.response-item .date { color: #64748b; font-size: 0.75rem; } +.response-detail { background: #1e293b; border-radius: 0.75rem; padding: 2rem; border: 1px solid #334155; } +.response-detail h2 { color: #f1f5f9; margin-bottom: 0.5rem; } +.meta { color: #94a3b8; margin-bottom: 2rem; } +.fields-list { display: flex; flex-direction: column; gap: 1.5rem; } +.field-item strong { display: block; color: #f1f5f9; margin-bottom: 0.5rem; } +.field-item p { color: #cbd5e1; } +.empty-state { display: flex; align-items: center; justify-content: center; height: 400px; color: #64748b; } diff --git a/servers/acuity-scheduling/src/ui/react-app/form-responses/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/form-responses/vite.config.ts new file mode 100644 index 0000000..2aeb920 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/form-responses/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3008 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/App.tsx b/servers/acuity-scheduling/src/ui/react-app/label-manager/App.tsx new file mode 100644 index 0000000..1e4589e --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/App.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Label { id: number; name: string; color: string; count: number; } + +export default function LabelManager() { + const [labels, setLabels] = useState([]); + const [newLabel, setNewLabel] = useState({ name: '', color: '#3b82f6' }); + + useEffect(() => { + setLabels([ + { id: 1, name: 'VIP', color: '#fbbf24', count: 12 }, + { id: 2, name: 'New Client', color: '#3b82f6', count: 28 }, + { id: 3, name: 'Urgent', color: '#ef4444', count: 5 }, + { id: 4, name: 'Follow-up Required', color: '#f59e0b', count: 15 } + ]); + }, []); + + const handleCreate = () => { + if (!newLabel.name) return; + setLabels([...labels, { ...newLabel, id: Date.now(), count: 0 }]); + setNewLabel({ name: '', color: '#3b82f6' }); + }; + + return ( +
+
+

🏷️ Label Manager

+
+
+

Create New Label

+
+ setNewLabel({...newLabel, name: e.target.value})} /> + setNewLabel({...newLabel, color: e.target.value})} /> + +
+
+
+ {labels.map(label => ( +
+
+ + {label.name} +
+
+ {label.count} appointments +
+ + +
+
+
+ ))} +
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/index.html b/servers/acuity-scheduling/src/ui/react-app/label-manager/index.html new file mode 100644 index 0000000..68eaea6 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/index.html @@ -0,0 +1,2 @@ + +Label Manager
diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/main.tsx b/servers/acuity-scheduling/src/ui/react-app/label-manager/main.tsx new file mode 100644 index 0000000..7e45ba3 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/package.json b/servers/acuity-scheduling/src/ui/react-app/label-manager/package.json new file mode 100644 index 0000000..3a695f6 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/package.json @@ -0,0 +1 @@ +{"name":"label-manager","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/styles.css b/servers/acuity-scheduling/src/ui/react-app/label-manager/styles.css new file mode 100644 index 0000000..904c3cd --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/styles.css @@ -0,0 +1,18 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.label-manager { max-width: 1200px; margin: 0 auto; padding: 2rem; } +header { margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +.create-section { background: #1e293b; padding: 1.5rem; border-radius: 0.75rem; border: 1px solid #334155; margin-bottom: 2rem; } +.create-section h2 { color: #f1f5f9; margin-bottom: 1rem; } +.create-form { display: flex; gap: 1rem; align-items: center; flex-wrap: wrap; } +.create-form input[type="text"] { flex: 1; min-width: 200px; padding: 0.75rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; color: #e2e8f0; } +.create-form input[type="color"] { width: 60px; height: 42px; border: 1px solid #334155; border-radius: 0.5rem; cursor: pointer; } +button { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; } +button.danger { background: #ef4444; } +.labels-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; } +.label-card { background: #1e293b; padding: 1.5rem; border-radius: 0.75rem; border: 1px solid #334155; } +.label-preview { display: flex; align-items: center; gap: 0.75rem; padding: 1rem; border-radius: 0.5rem; border: 2px solid; margin-bottom: 1rem; font-weight: 600; } +.label-info { display: flex; justify-content: space-between; align-items: center; } +.usage { color: #94a3b8; font-size: 0.875rem; } +.label-actions { display: flex; gap: 0.5rem; } diff --git a/servers/acuity-scheduling/src/ui/react-app/label-manager/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/label-manager/vite.config.ts new file mode 100644 index 0000000..220e0c4 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/label-manager/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3009 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/App.tsx b/servers/acuity-scheduling/src/ui/react-app/product-catalog/App.tsx new file mode 100644 index 0000000..5c99b09 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/App.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Product { id: number; name: string; description: string; price: number; category: string; active: boolean; } + +export default function ProductCatalog() { + const [products, setProducts] = useState([]); + const [filter, setFilter] = useState('all'); + + useEffect(() => { + setProducts([ + { id: 1, name: 'Initial Consultation', description: '60-minute first visit', price: 150, category: 'Consultation', active: true }, + { id: 2, name: 'Follow-up Session', description: '30-minute check-in', price: 75, category: 'Session', active: true }, + { id: 3, name: 'Package (5 sessions)', description: 'Save 10% with package', price: 337.50, category: 'Package', active: true }, + { id: 4, name: 'Workshop', description: 'Group workshop session', price: 50, category: 'Workshop', active: false } + ]); + }, []); + + const filtered = filter === 'all' ? products : products.filter(p => p.category === filter); + + return ( +
+
+

🛍️ Product Catalog

+ +
+
+ + + + + +
+
+ {filtered.map(product => ( +
+
+

{product.name}

+ + {product.active ? '● Active' : '○ Inactive'} + +
+

{product.description}

+
+ {product.category} + ${product.price.toFixed(2)} +
+
+ + +
+
+ ))} +
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/index.html b/servers/acuity-scheduling/src/ui/react-app/product-catalog/index.html new file mode 100644 index 0000000..9b87f99 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/index.html @@ -0,0 +1,2 @@ + +Product Catalog
diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/main.tsx b/servers/acuity-scheduling/src/ui/react-app/product-catalog/main.tsx new file mode 100644 index 0000000..6c7d170 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App.js'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/package.json b/servers/acuity-scheduling/src/ui/react-app/product-catalog/package.json new file mode 100644 index 0000000..0a1bdfa --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/package.json @@ -0,0 +1 @@ +{"name":"product-catalog","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/styles.css b/servers/acuity-scheduling/src/ui/react-app/product-catalog/styles.css new file mode 100644 index 0000000..4b1e365 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/styles.css @@ -0,0 +1,22 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.product-catalog { max-width: 1200px; margin: 0 auto; padding: 2rem; } +header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +button { padding: 0.5rem 1rem; background: #3b82f6; color: white; border: none; border-radius: 0.5rem; cursor: pointer; } +button.danger { background: #ef4444; } +.filter-bar { display: flex; gap: 0.5rem; margin-bottom: 2rem; flex-wrap: wrap; } +.filter-bar button { background: #1e293b; color: #94a3b8; } +.filter-bar button.active { background: #3b82f6; color: white; } +.products-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 1.5rem; } +.product-card { background: #1e293b; padding: 1.5rem; border-radius: 0.75rem; border: 1px solid #334155; } +.product-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } +.product-header h3 { color: #f1f5f9; } +.status { font-size: 0.75rem; font-weight: 600; } +.status.active { color: #10b981; } +.status.inactive { color: #64748b; } +.description { color: #94a3b8; font-size: 0.875rem; margin-bottom: 1rem; } +.product-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } +.category { background: #334155; color: #e2e8f0; padding: 0.25rem 0.75rem; border-radius: 1rem; font-size: 0.75rem; } +.price { font-size: 1.5rem; font-weight: 700; color: #3b82f6; } +.product-actions { display: flex; gap: 0.5rem; } diff --git a/servers/acuity-scheduling/src/ui/react-app/product-catalog/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/product-catalog/vite.config.ts new file mode 100644 index 0000000..771dcad --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/product-catalog/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3007 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx new file mode 100644 index 0000000..0bb75ef --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx @@ -0,0 +1,62 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface ScheduleEntry { id: number; time: string; client: string; type: string; calendar: string; status: 'confirmed' | 'pending'; } + +export default function ScheduleOverview() { + const [schedules, setSchedules] = useState([]); + const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]); + const [view, setView] = useState<'day' | 'week'>('day'); + + useEffect(() => { + setSchedules([ + { id: 1, time: '09:00', client: 'John Doe', type: 'Consultation', calendar: 'Main', status: 'confirmed' }, + { id: 2, time: '10:30', client: 'Jane Smith', type: 'Follow-up', calendar: 'Main', status: 'confirmed' }, + { id: 3, time: '14:00', client: 'Bob Johnson', type: 'Assessment', calendar: 'Secondary', status: 'pending' }, + { id: 4, time: '15:30', client: 'Alice Williams', type: 'Check-in', calendar: 'Main', status: 'confirmed' } + ]); + }, [selectedDate]); + + return ( +
+
+

📆 Schedule Overview

+
+ + +
+
+ +
+ + setSelectedDate(e.target.value)} /> + +
+ +
+ {Array.from({ length: 12 }, (_, i) => { + const hour = 8 + i; + const timeSlot = `${hour}:00`; + const appointment = schedules.find(s => s.time === timeSlot || s.time === `${hour}:30`); + + return ( +
+
{timeSlot}
+
+ {appointment ? ( +
+ {appointment.client} + {appointment.type} + {appointment.calendar} +
+ ) : ( +
Available
+ )} +
+
+ ); + })} +
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/index.html b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/index.html new file mode 100644 index 0000000..0d8cf69 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/index.html @@ -0,0 +1,2 @@ + +Schedule Overview
diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx new file mode 100644 index 0000000..7e45ba3 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/package.json b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/package.json new file mode 100644 index 0000000..21e312d --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/package.json @@ -0,0 +1 @@ +{"name":"schedule-overview","version":"1.0.0","type":"module","scripts":{"dev":"vite","build":"vite build"},"dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"devDependencies":{"@types/react":"^18.2.0","@types/react-dom":"^18.2.0","@vitejs/plugin-react":"^4.2.0","typescript":"^5.3.0","vite":"^5.0.0"}} diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/styles.css b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/styles.css new file mode 100644 index 0000000..f85128f --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/styles.css @@ -0,0 +1,22 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.schedule-overview { max-width: 1000px; margin: 0 auto; padding: 2rem; } +header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem; } +h1 { font-size: 2rem; font-weight: 700; color: #f1f5f9; } +.view-toggle { display: flex; gap: 0.5rem; } +.view-toggle button { padding: 0.5rem 1rem; background: #1e293b; color: #94a3b8; border: none; border-radius: 0.5rem; cursor: pointer; } +.view-toggle button.active { background: #3b82f6; color: white; } +.date-controls { display: flex; gap: 1rem; margin-bottom: 2rem; justify-content: center; } +.date-controls button { padding: 0.75rem 1.5rem; background: #1e293b; color: #e2e8f0; border: none; border-radius: 0.5rem; cursor: pointer; } +.date-controls input { padding: 0.75rem; background: #1e293b; border: 1px solid #334155; border-radius: 0.5rem; color: #e2e8f0; } +.schedule-timeline { background: #1e293b; border-radius: 0.75rem; border: 1px solid #334155; padding: 1rem; } +.time-block { display: flex; border-bottom: 1px solid #334155; padding: 1rem; } +.time-block:last-child { border-bottom: none; } +.time-label { width: 80px; color: #94a3b8; font-weight: 600; } +.time-content { flex: 1; } +.appointment-block { background: #0f172a; padding: 1rem; border-radius: 0.5rem; border-left: 4px solid #3b82f6; } +.appointment-block.pending { border-left-color: #f59e0b; } +.appointment-block strong { display: block; color: #f1f5f9; margin-bottom: 0.25rem; } +.appointment-block span { display: block; color: #94a3b8; font-size: 0.875rem; } +.calendar-badge { display: inline-block; background: #334155; color: #e2e8f0; padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; margin-top: 0.5rem; } +.empty-slot { color: #64748b; font-style: italic; } diff --git a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/vite.config.ts new file mode 100644 index 0000000..2259f1e --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/vite.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +export default defineConfig({ plugins: [react()], server: { port: 3012 }, build: { outDir: 'dist' } }); diff --git a/servers/acuity/.gitignore b/servers/acuity/.gitignore new file mode 100644 index 0000000..20afb86 --- /dev/null +++ b/servers/acuity/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +*.log +.DS_Store +.env +.env.local +*.tsbuildinfo +src/ui/react-app/src/apps/*/dist diff --git a/servers/acuity/README.md b/servers/acuity/README.md new file mode 100644 index 0000000..5e9e705 --- /dev/null +++ b/servers/acuity/README.md @@ -0,0 +1,199 @@ +# Acuity Scheduling MCP Server + +A comprehensive Model Context Protocol (MCP) server for Acuity Scheduling, providing full API integration with 59 tools and 12 interactive React applications. + +## Features + +### 🛠️ 59 MCP Tools + +Complete CRUD operations across all Acuity Scheduling domains: + +- **Appointments** (6 tools): List, get, create, update, cancel, reschedule +- **Calendars** (2 tools): List, get +- **Appointment Types** (5 tools): List, get, create, update, delete +- **Clients** (5 tools): List, get, create, update, delete +- **Availability** (2 tools): Get available dates and times +- **Blocks** (5 tools): List, get, create, update, delete (block out time) +- **Products** (5 tools): List, get, create, update, delete +- **Certificates** (4 tools): List, get, create, delete (gift certificates) +- **Coupons** (5 tools): List, get, create, update, delete +- **Forms** (5 tools): List, get, create, update, delete (intake forms) +- **Labels** (5 tools): List, get, create, update, delete +- **Packages** (5 tools): List, get, create, update, delete +- **Subscriptions** (5 tools): List, get, create, update, delete + +### 🎨 12 React Applications + +Modern, dark-themed UI applications: + +1. **Appointment Calendar** - View and manage daily/weekly appointments +2. **Appointment Detail** - Detailed view of individual appointments +3. **Client Directory** - Browse and search all clients +4. **Client Detail** - Complete client profile with appointment history +5. **Availability Manager** - Manage available time slots and blocks +6. **Product Catalog** - View and manage products/services +7. **Certificate Viewer** - Manage gift certificates +8. **Coupon Manager** - Create and track discount coupons +9. **Form Builder** - Design and manage intake forms +10. **Analytics Dashboard** - Business insights and metrics +11. **Booking Flow** - End-to-end appointment booking experience +12. **Schedule Overview** - Weekly schedule summary and revenue tracking + +## Installation + +```bash +npm install +``` + +## Configuration + +Set the following environment variables: + +```bash +export ACUITY_USER_ID="your_acuity_user_id" +export ACUITY_API_KEY="your_acuity_api_key" +``` + +You can find your User ID and API Key in your Acuity Scheduling account under: +**Business Settings → Integrations → API** + +## Usage + +### As MCP Server + +Build and start the server: + +```bash +npm run build +npm start +``` + +### With Claude Desktop + +Add to your Claude Desktop config (`~/Library/Application Support/Claude/claude_desktop_config.json` on macOS): + +```json +{ + "mcpServers": { + "acuity": { + "command": "node", + "args": ["/path/to/acuity-scheduling-mcp-server/dist/main.js"], + "env": { + "ACUITY_USER_ID": "your_user_id", + "ACUITY_API_KEY": "your_api_key" + } + } + } +} +``` + +### Development + +Watch mode for TypeScript: + +```bash +npm run dev +``` + +Type checking without building: + +```bash +npm run typecheck +``` + +### Running React Apps + +Each React app can be run independently using Vite: + +```bash +cd src/ui/react-app/src/apps/appointment-calendar +npm install +npm run dev +``` + +## Architecture + +### API Client + +The `AcuityClient` class (`src/clients/acuity.ts`) handles: +- Basic Authentication (userId + API key) +- Request/response handling +- Error handling +- Pagination support + +### Tools + +Each domain has its own tool file in `src/tools/`: +- Standardized input schemas +- Proper error handling +- Type-safe operations + +### Type System + +Complete TypeScript definitions in `src/types/index.ts` covering: +- Request/response types +- API entities +- Client configuration + +## API Reference + +### Example Tool Calls + +**List appointments:** +```typescript +{ + "name": "acuity_list_appointments", + "arguments": { + "minDate": "2024-01-01", + "maxDate": "2024-01-31", + "max": 100 + } +} +``` + +**Create appointment:** +```typescript +{ + "name": "acuity_create_appointment", + "arguments": { + "appointmentTypeID": 123, + "datetime": "2024-01-15T14:00:00", + "firstName": "John", + "lastName": "Doe", + "email": "john@example.com", + "phone": "555-1234" + } +} +``` + +**Get availability:** +```typescript +{ + "name": "acuity_get_availability_dates", + "arguments": { + "appointmentTypeID": 123, + "month": "2024-01", + "timezone": "America/New_York" + } +} +``` + +## Tech Stack + +- **Server**: TypeScript, Node.js, MCP SDK +- **UI**: React 18, TypeScript, Vite, Tailwind CSS +- **API**: Acuity Scheduling REST API v1 + +## License + +MIT + +## Support + +For issues or questions: +- Acuity API Docs: https://developers.acuityscheduling.com/ +- MCP Docs: https://modelcontextprotocol.io/ + +--- + +Built with ❤️ for the Model Context Protocol diff --git a/servers/acuity/package.json b/servers/acuity/package.json new file mode 100644 index 0000000..e0651ae --- /dev/null +++ b/servers/acuity/package.json @@ -0,0 +1,42 @@ +{ + "name": "acuity-scheduling-mcp-server", + "version": "1.0.0", + "description": "MCP server for Acuity Scheduling integration with 59 tools and 12 React apps", + "type": "module", + "main": "./dist/main.js", + "bin": { + "acuity-mcp": "./dist/main.js" + }, + "scripts": { + "build": "tsc && chmod +x dist/main.js", + "dev": "tsc --watch", + "start": "node dist/main.js", + "typecheck": "tsc --noEmit", + "prepare": "npm run build" + }, + "keywords": [ + "mcp", + "model-context-protocol", + "acuity", + "scheduling", + "appointments" + ], + "author": "MCP Engine", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4" + }, + "devDependencies": { + "@types/node": "^20.17.10", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwindcss": "^3.4.17", + "typescript": "^5.7.2", + "vite": "^5.4.11" + } +} diff --git a/servers/acuity/postcss.config.js b/servers/acuity/postcss.config.js new file mode 100644 index 0000000..2e7af2b --- /dev/null +++ b/servers/acuity/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/servers/acuity/src/ui/react-app/src/apps/analytics-dashboard/App.tsx b/servers/acuity/src/ui/react-app/src/apps/analytics-dashboard/App.tsx new file mode 100644 index 0000000..f07240c --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/analytics-dashboard/App.tsx @@ -0,0 +1,107 @@ +import { useState } from 'react'; + +export default function AnalyticsDashboard() { + const [dateRange, setDateRange] = useState('7days'); + + const stats = { + totalAppointments: 142, + revenue: '$12,450', + newClients: 28, + cancelationRate: '8%', + }; + + return ( +
+
+
+

Analytics Dashboard

+ +
+ +
+
+

Total Appointments

+

{stats.totalAppointments}

+

↑ 12% vs last period

+
+
+

Revenue

+

{stats.revenue}

+

↑ 8% vs last period

+
+
+

New Clients

+

{stats.newClients}

+

↑ 15% vs last period

+
+
+

Cancelation Rate

+

{stats.cancelationRate}

+

↑ 2% vs last period

+
+
+ +
+
+

Appointment Types

+
+
+ Consultation +
+
+
+
+ 75% +
+
+
+ Follow-up +
+
+
+
+ 45% +
+
+
+ Extended Session +
+
+
+
+ 30% +
+
+
+
+ +
+

Top Calendars

+
+
+ Main Calendar + 85 appointments +
+
+ Secondary Calendar + 42 appointments +
+
+ Weekend Calendar + 15 appointments +
+
+
+
+
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/apps/booking-flow/App.tsx b/servers/acuity/src/ui/react-app/src/apps/booking-flow/App.tsx new file mode 100644 index 0000000..fa00ad0 --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/booking-flow/App.tsx @@ -0,0 +1,146 @@ +import { useState } from 'react'; + +type Step = 'service' | 'datetime' | 'info' | 'confirm'; + +export default function BookingFlow() { + const [currentStep, setCurrentStep] = useState('service'); + const [formData, setFormData] = useState({ + service: '', + date: '', + time: '', + firstName: '', + lastName: '', + email: '', + phone: '', + }); + + const steps = [ + { id: 'service', label: 'Select Service' }, + { id: 'datetime', label: 'Choose Date & Time' }, + { id: 'info', label: 'Your Information' }, + { id: 'confirm', label: 'Confirm' }, + ]; + + return ( +
+
+

Book an Appointment

+ +
+ {steps.map((step, index) => ( +
+
+
+ {index + 1} +
+ {step.label} +
+
+ ))} +
+ +
+ {currentStep === 'service' && ( +
+

Select a Service

+
+ {['Consultation - $50', 'Extended Session - $100', 'Follow-up - $75'].map((service) => ( + + ))} +
+
+ )} + + {currentStep === 'datetime' && ( +
+

Choose Date & Time

+
+ setFormData({ ...formData, date: e.target.value })} + /> + + +
+
+ )} + + {currentStep === 'info' && ( +
+

Your Information

+
+ + + + + +
+
+ )} + + {currentStep === 'confirm' && ( +
+

Confirm Your Appointment

+
+

Service: Consultation

+

Date: January 15, 2024

+

Time: 2:00 PM

+

Name: John Doe

+

Email: john@example.com

+
+ +
+ )} +
+
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/apps/certificate-viewer/App.tsx b/servers/acuity/src/ui/react-app/src/apps/certificate-viewer/App.tsx new file mode 100644 index 0000000..7bcd243 --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/certificate-viewer/App.tsx @@ -0,0 +1,66 @@ +import { useState, useEffect } from 'react'; + +interface Certificate { + certificate: string; + name: string; + value: string; + validUses: number; + usesRemaining: number; + expirationDate: string | null; +} + +export default function CertificateViewer() { + const [certificates, setCertificates] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const mockCertificates: Certificate[] = [ + { certificate: 'GIFT100', name: 'John Doe', value: '$100.00', validUses: 1, usesRemaining: 1, expirationDate: '2024-12-31' }, + { certificate: 'VIP50', name: 'Jane Smith', value: '$50.00', validUses: 2, usesRemaining: 1, expirationDate: null }, + ]; + setCertificates(mockCertificates); + setLoading(false); + }, []); + + return ( +
+
+
+

Gift Certificates

+ +
+ + {loading ? ( +
+
+
+ ) : ( +
+ {certificates.map((cert) => ( +
+
+
+

Code: {cert.certificate}

+

Holder: {cert.name}

+

Value: {cert.value}

+
+ Valid Uses: {cert.validUses} + Remaining: {cert.usesRemaining} + {cert.expirationDate && Expires: {cert.expirationDate}} +
+
+
+ + +
+
+
+ ))} +
+ )} +
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/apps/coupon-manager/App.tsx b/servers/acuity/src/ui/react-app/src/apps/coupon-manager/App.tsx new file mode 100644 index 0000000..4a87159 --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/coupon-manager/App.tsx @@ -0,0 +1,71 @@ +import { useState, useEffect } from 'react'; + +interface Coupon { + id: number; + code: string; + discount: string; + expirationDate: string | null; + usageCount: number; +} + +export default function CouponManager() { + const [coupons, setCoupons] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const mockCoupons: Coupon[] = [ + { id: 1, code: 'SAVE10', discount: '10%', expirationDate: '2024-12-31', usageCount: 15 }, + { id: 2, code: 'FIRST20', discount: '20%', expirationDate: null, usageCount: 8 }, + { id: 3, code: 'FLAT5', discount: '$5.00', expirationDate: '2024-06-30', usageCount: 42 }, + ]; + setCoupons(mockCoupons); + setLoading(false); + }, []); + + return ( +
+
+
+

Coupon Manager

+ +
+ + {loading ? ( +
+
+
+ ) : ( +
+ + + + + + + + + + + + {coupons.map((coupon) => ( + + + + + + + + ))} + +
CodeDiscountExpiresUsageActions
{coupon.code}{coupon.discount}{coupon.expirationDate || 'No expiration'}{coupon.usageCount} uses + + +
+
+ )} +
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/apps/form-builder/App.tsx b/servers/acuity/src/ui/react-app/src/apps/form-builder/App.tsx new file mode 100644 index 0000000..138fed8 --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/form-builder/App.tsx @@ -0,0 +1,104 @@ +import { useState, useEffect } from 'react'; + +interface FormField { + id: string; + name: string; + type: 'text' | 'email' | 'select' | 'textarea'; + required: boolean; +} + +interface Form { + id: number; + name: string; + description: string; + fields: FormField[]; +} + +export default function FormBuilder() { + const [forms, setForms] = useState([]); + const [selectedForm, setSelectedForm] = useState
(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const mockForms: Form[] = [ + { + id: 1, + name: 'New Client Intake', + description: 'Initial consultation form', + fields: [ + { id: '1', name: 'Full Name', type: 'text', required: true }, + { id: '2', name: 'Email', type: 'email', required: true }, + { id: '3', name: 'Reason for Visit', type: 'textarea', required: false }, + ], + }, + ]; + setForms(mockForms); + setLoading(false); + }, []); + + return ( +
+
+
+

Form Builder

+ +
+ +
+
+

Forms

+
+ {forms.map((form) => ( +
setSelectedForm(form)} + className={`bg-gray-800 rounded-lg p-4 cursor-pointer hover:bg-gray-750 transition ${ + selectedForm?.id === form.id ? 'ring-2 ring-blue-500' : '' + }`} + > +

{form.name}

+

{form.fields.length} fields

+
+ ))} +
+
+ +
+ {selectedForm ? ( +
+

{selectedForm.name}

+

{selectedForm.description}

+ +
+ {selectedForm.fields.map((field) => ( +
+
+
+

{field.name}

+

Type: {field.type}

+
+ {field.required && ( + Required + )} +
+
+ ))} +
+ + +
+ ) : ( +
+

Select a form to edit

+
+ )} +
+
+
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/apps/schedule-overview/App.tsx b/servers/acuity/src/ui/react-app/src/apps/schedule-overview/App.tsx new file mode 100644 index 0000000..9ecfb0b --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/apps/schedule-overview/App.tsx @@ -0,0 +1,91 @@ +import { useState } from 'react'; + +interface DaySchedule { + date: string; + appointments: number; + revenue: string; +} + +export default function ScheduleOverview() { + const [currentWeek, setCurrentWeek] = useState('2024-W03'); + + const weekSchedule: DaySchedule[] = [ + { date: 'Mon, Jan 15', appointments: 8, revenue: '$650' }, + { date: 'Tue, Jan 16', appointments: 12, revenue: '$980' }, + { date: 'Wed, Jan 17', appointments: 6, revenue: '$450' }, + { date: 'Thu, Jan 18', appointments: 10, revenue: '$825' }, + { date: 'Fri, Jan 19', appointments: 7, revenue: '$575' }, + { date: 'Sat, Jan 20', appointments: 4, revenue: '$320' }, + { date: 'Sun, Jan 21', appointments: 0, revenue: '$0' }, + ]; + + return ( +
+
+
+

Schedule Overview

+ setCurrentWeek(e.target.value)} + className="bg-gray-800 border border-gray-700 rounded-lg px-4 py-2 text-gray-100" + /> +
+ +
+
+

Total Appointments

+

47

+
+
+

Weekly Revenue

+

$3,800

+
+
+

Busiest Day

+

Tuesday

+
+
+

Open Slots

+

23

+
+
+ +
+ + + + + + + + + + + {weekSchedule.map((day) => ( + + + + + + + ))} + +
DayAppointmentsRevenueStatus
{day.date}{day.appointments}{day.revenue} + 10 ? 'bg-red-900 text-red-200' : + day.appointments > 5 ? 'bg-yellow-900 text-yellow-200' : + day.appointments > 0 ? 'bg-green-900 text-green-200' : + 'bg-gray-700 text-gray-400' + }`}> + {day.appointments > 10 ? 'Fully Booked' : + day.appointments > 5 ? 'Busy' : + day.appointments > 0 ? 'Available' : + 'Closed'} + +
+
+
+
+ ); +} diff --git a/servers/acuity/src/ui/react-app/src/index.css b/servers/acuity/src/ui/react-app/src/index.css new file mode 100644 index 0000000..8970c1b --- /dev/null +++ b/servers/acuity/src/ui/react-app/src/index.css @@ -0,0 +1,22 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +* { + 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; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', + monospace; +} diff --git a/servers/acuity/tailwind.config.js b/servers/acuity/tailwind.config.js new file mode 100644 index 0000000..27f5b5d --- /dev/null +++ b/servers/acuity/tailwind.config.js @@ -0,0 +1,16 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: [ + './src/ui/react-app/src/**/*.{js,ts,jsx,tsx}', + ], + theme: { + extend: { + colors: { + gray: { + 750: '#374151', + }, + }, + }, + }, + plugins: [], +} diff --git a/servers/acuity/tsconfig.json b/servers/acuity/tsconfig.json new file mode 100644 index 0000000..3e03bdb --- /dev/null +++ b/servers/acuity/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "Node16", + "moduleResolution": "Node16", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "jsx": "react-jsx", + "allowSyntheticDefaultImports": true + }, + "include": [ + "src/types/**/*", + "src/clients/**/*", + "src/tools/**/*", + "src/server.ts", + "src/main.ts" + ], + "exclude": ["node_modules", "dist", "src/ui/**/*"] +} diff --git a/servers/clover/package.json b/servers/clover/package.json index 339c319..293684e 100644 --- a/servers/clover/package.json +++ b/servers/clover/package.json @@ -4,11 +4,15 @@ "description": "MCP server for Clover POS platform with comprehensive tools and React apps", "type": "module", "main": "dist/main.js", + "bin": { + "clover-mcp": "./dist/main.js" + }, "scripts": { "build": "tsc && node scripts/copy-assets.js", "dev": "tsc --watch", "start": "node dist/main.js", - "prepare": "npm run build" + "prepare": "npm run build", + "prepublishOnly": "npm run build" }, "keywords": [ "mcp", diff --git a/servers/fieldedge/package.json b/servers/fieldedge/package.json index 04daa0b..bc121c4 100644 --- a/servers/fieldedge/package.json +++ b/servers/fieldedge/package.json @@ -1,20 +1,32 @@ { - "name": "mcp-server-fieldedge", + "name": "@mcpengine/fieldedge", "version": "1.0.0", + "description": "FieldEdge MCP Server - Complete field service management integration", "type": "module", - "main": "dist/index.js", + "main": "dist/main.js", + "bin": { + "fieldedge-mcp": "./dist/main.js" + }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "prepare": "npm run build", + "start": "node dist/main.js" }, + "keywords": [ + "mcp", + "fieldedge", + "field-service", + "hvac", + "dispatch", + "service-management" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "typescript": "^5.7.2" } } diff --git a/servers/fieldedge/src/client.ts b/servers/fieldedge/src/client.ts new file mode 100644 index 0000000..c849ae4 --- /dev/null +++ b/servers/fieldedge/src/client.ts @@ -0,0 +1,215 @@ +/** + * FieldEdge API Client + * Handles authentication, pagination, and error handling + */ + +import { FieldEdgeConfig, PaginatedResponse, ApiError } from './types.js'; + +export class FieldEdgeClient { + private apiKey: string; + private baseUrl: string; + + constructor(config: FieldEdgeConfig) { + this.apiKey = config.apiKey; + this.baseUrl = config.baseUrl || 'https://api.fieldedge.com/v2'; + } + + /** + * Make a GET request with pagination support + */ + async get( + endpoint: string, + params?: Record + ): Promise { + const url = this.buildUrl(endpoint, params); + return this.request('GET', url); + } + + /** + * Make a paginated GET request + */ + async getPaginated( + endpoint: string, + params?: Record + ): Promise> { + const page = (params?.page as number) || 1; + const pageSize = (params?.pageSize as number) || 50; + + const response = await this.get<{ + data: T[]; + total: number; + page: number; + pageSize: number; + }>(endpoint, { ...params, page, pageSize }); + + return { + data: response.data, + total: response.total, + page: response.page, + pageSize: response.pageSize, + hasMore: page * pageSize < response.total, + }; + } + + /** + * Make a POST request + */ + async post(endpoint: string, data?: unknown): Promise { + const url = this.buildUrl(endpoint); + return this.request('POST', url, data); + } + + /** + * Make a PUT request + */ + async put(endpoint: string, data?: unknown): Promise { + const url = this.buildUrl(endpoint); + return this.request('PUT', url, data); + } + + /** + * Make a PATCH request + */ + async patch(endpoint: string, data?: unknown): Promise { + const url = this.buildUrl(endpoint); + return this.request('PATCH', url, data); + } + + /** + * Make a DELETE request + */ + async delete(endpoint: string): Promise { + const url = this.buildUrl(endpoint); + return this.request('DELETE', url); + } + + /** + * Core request method with error handling + */ + private async request( + method: string, + url: string, + body?: unknown + ): Promise { + try { + const headers: HeadersInit = { + 'Authorization': `Bearer ${this.apiKey}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }; + + const options: RequestInit = { + method, + headers, + }; + + if (body) { + options.body = JSON.stringify(body); + } + + const response = await fetch(url, options); + + if (!response.ok) { + await this.handleErrorResponse(response); + } + + // Handle 204 No Content + if (response.status === 204) { + return {} as T; + } + + const data = await response.json(); + return data as T; + } catch (error) { + if (error instanceof Error) { + throw this.createApiError(error.message, 0, error); + } + throw this.createApiError('Unknown error occurred', 0, error); + } + } + + /** + * Handle error responses from the API + */ + private async handleErrorResponse(response: Response): Promise { + let errorMessage = `API request failed: ${response.status} ${response.statusText}`; + let errorDetails: unknown; + + try { + const errorData = await response.json(); + errorMessage = errorData.message || errorData.error || errorMessage; + errorDetails = errorData; + } catch { + // Response body is not JSON + } + + throw this.createApiError(errorMessage, response.status, errorDetails); + } + + /** + * Create a standardized API error + */ + private createApiError( + message: string, + statusCode: number, + details?: unknown + ): Error { + const error = new Error(message) as Error & ApiError; + error.statusCode = statusCode; + error.details = details; + return error; + } + + /** + * Build URL with query parameters + */ + private buildUrl( + endpoint: string, + params?: Record + ): string { + const url = new URL( + endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}` + ); + + if (params) { + Object.entries(params).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + url.searchParams.append(key, String(value)); + } + }); + } + + return url.toString(); + } + + /** + * Fetch all pages of a paginated endpoint + */ + async getAllPages( + endpoint: string, + params?: Record + ): Promise { + const allData: T[] = []; + let page = 1; + let hasMore = true; + + while (hasMore) { + const response = await this.getPaginated(endpoint, { + ...params, + page, + }); + + allData.push(...response.data); + hasMore = response.hasMore; + page++; + + // Safety limit to prevent infinite loops + if (page > 1000) { + console.warn('Reached maximum page limit (1000)'); + break; + } + } + + return allData; + } +} diff --git a/servers/fieldedge/src/types.ts b/servers/fieldedge/src/types.ts new file mode 100644 index 0000000..6978465 --- /dev/null +++ b/servers/fieldedge/src/types.ts @@ -0,0 +1,439 @@ +/** + * FieldEdge MCP Server - Type Definitions + */ + +// API Configuration +export interface FieldEdgeConfig { + apiKey: string; + baseUrl?: string; +} + +// Common Types +export interface PaginationParams { + page?: number; + pageSize?: number; + offset?: number; + limit?: number; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + pageSize: number; + hasMore: boolean; +} + +export interface ApiError { + message: string; + code?: string; + statusCode?: number; + details?: unknown; +} + +// Job Types +export interface Job { + id: string; + jobNumber: string; + customerId: string; + customerName?: string; + locationId?: string; + jobType: string; + status: 'scheduled' | 'in_progress' | 'completed' | 'cancelled' | 'on_hold'; + priority: 'low' | 'normal' | 'high' | 'emergency'; + assignedTechId?: string; + assignedTechName?: string; + scheduledStart?: string; + scheduledEnd?: string; + actualStart?: string; + actualEnd?: string; + description?: string; + notes?: string; + totalAmount?: number; + address?: Address; + createdAt: string; + updatedAt: string; +} + +export interface JobLineItem { + id: string; + jobId: string; + type: 'labor' | 'material' | 'equipment' | 'other'; + description: string; + quantity: number; + unitPrice: number; + totalPrice: number; + taxable: boolean; + partNumber?: string; + technicianId?: string; +} + +export interface JobEquipment { + id: string; + jobId: string; + equipmentId: string; + equipmentType: string; + serialNumber?: string; + model?: string; + manufacturer?: string; + serviceType: string; +} + +// Customer Types +export interface Customer { + id: string; + customerNumber: string; + type: 'residential' | 'commercial'; + firstName?: string; + lastName?: string; + companyName?: string; + email?: string; + phone?: string; + mobilePhone?: string; + status: 'active' | 'inactive'; + balance: number; + creditLimit?: number; + taxExempt: boolean; + primaryAddress?: Address; + billingAddress?: Address; + tags?: string[]; + createdAt: string; + updatedAt: string; +} + +export interface CustomerLocation { + id: string; + customerId: string; + name: string; + address: Address; + isPrimary: boolean; + contactName?: string; + contactPhone?: string; + accessNotes?: string; + gateCode?: string; +} + +export interface Address { + street1: string; + street2?: string; + city: string; + state: string; + zip: string; + country?: string; + latitude?: number; + longitude?: number; +} + +// Invoice Types +export interface Invoice { + id: string; + invoiceNumber: string; + customerId: string; + customerName?: string; + jobId?: string; + status: 'draft' | 'sent' | 'paid' | 'partial' | 'overdue' | 'void'; + invoiceDate: string; + dueDate?: string; + subtotal: number; + taxAmount: number; + totalAmount: number; + amountPaid: number; + amountDue: number; + lineItems: InvoiceLineItem[]; + notes?: string; + terms?: string; + createdAt: string; + updatedAt: string; +} + +export interface InvoiceLineItem { + id: string; + type: 'labor' | 'material' | 'equipment' | 'other'; + description: string; + quantity: number; + unitPrice: number; + totalPrice: number; + taxable: boolean; +} + +export interface Payment { + id: string; + invoiceId: string; + amount: number; + paymentMethod: 'cash' | 'check' | 'credit_card' | 'ach' | 'other'; + paymentDate: string; + referenceNumber?: string; + notes?: string; + createdAt: string; +} + +// Estimate Types +export interface Estimate { + id: string; + estimateNumber: string; + customerId: string; + customerName?: string; + locationId?: string; + status: 'draft' | 'sent' | 'approved' | 'declined' | 'expired'; + estimateDate: string; + expirationDate?: string; + subtotal: number; + taxAmount: number; + totalAmount: number; + lineItems: EstimateLineItem[]; + notes?: string; + terms?: string; + createdBy?: string; + createdAt: string; + updatedAt: string; +} + +export interface EstimateLineItem { + id: string; + type: 'labor' | 'material' | 'equipment' | 'other'; + description: string; + quantity: number; + unitPrice: number; + totalPrice: number; + taxable: boolean; +} + +// Technician Types +export interface Technician { + id: string; + employeeNumber: string; + firstName: string; + lastName: string; + email?: string; + phone?: string; + status: 'active' | 'inactive' | 'on_leave'; + role: string; + certifications?: string[]; + skills?: string[]; + hourlyRate?: number; + hireDate?: string; + terminationDate?: string; + createdAt: string; + updatedAt: string; +} + +export interface TechnicianPerformance { + technicianId: string; + technicianName: string; + period: string; + jobsCompleted: number; + averageJobTime: number; + revenue: number; + customerSatisfaction?: number; + callbackRate?: number; + efficiency?: number; +} + +export interface TimeEntry { + id: string; + technicianId: string; + jobId?: string; + date: string; + clockIn: string; + clockOut?: string; + hours: number; + type: 'regular' | 'overtime' | 'double_time' | 'travel'; + notes?: string; +} + +// Dispatch Types +export interface DispatchBoard { + date: string; + zones: DispatchZone[]; + unassignedJobs: Job[]; +} + +export interface DispatchZone { + id: string; + name: string; + technicians: DispatchTechnician[]; +} + +export interface DispatchTechnician { + id: string; + name: string; + status: 'available' | 'on_job' | 'traveling' | 'break' | 'offline'; + currentLocation?: { lat: number; lng: number }; + assignedJobs: Job[]; + capacity: number; + utilizationPercent: number; +} + +export interface TechnicianAvailability { + technicianId: string; + date: string; + availableSlots: TimeSlot[]; + bookedSlots: TimeSlot[]; +} + +export interface TimeSlot { + start: string; + end: string; + duration: number; +} + +// Equipment Types +export interface Equipment { + id: string; + customerId: string; + locationId?: string; + type: string; + manufacturer?: string; + model?: string; + serialNumber?: string; + installDate?: string; + warrantyExpiration?: string; + status: 'active' | 'inactive' | 'retired'; + lastServiceDate?: string; + nextServiceDate?: string; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export interface ServiceHistory { + id: string; + equipmentId: string; + jobId: string; + technicianId: string; + technicianName?: string; + serviceDate: string; + serviceType: string; + description: string; + partsUsed?: string[]; + laborHours?: number; + cost?: number; + notes?: string; +} + +// Inventory Types +export interface InventoryPart { + id: string; + partNumber: string; + description: string; + category?: string; + manufacturer?: string; + quantityOnHand: number; + quantityAvailable: number; + quantityOnOrder: number; + reorderPoint?: number; + reorderQuantity?: number; + unitCost: number; + unitPrice: number; + location?: string; + updatedAt: string; +} + +export interface PurchaseOrder { + id: string; + poNumber: string; + vendorId: string; + vendorName?: string; + status: 'draft' | 'submitted' | 'approved' | 'received' | 'cancelled'; + orderDate: string; + expectedDate?: string; + receivedDate?: string; + subtotal: number; + taxAmount: number; + totalAmount: number; + lineItems: PurchaseOrderLineItem[]; + notes?: string; + createdAt: string; + updatedAt: string; +} + +export interface PurchaseOrderLineItem { + id: string; + partId: string; + partNumber: string; + description: string; + quantityOrdered: number; + quantityReceived: number; + unitCost: number; + totalCost: number; +} + +// Service Agreement Types +export interface ServiceAgreement { + id: string; + agreementNumber: string; + customerId: string; + customerName?: string; + locationId?: string; + type: string; + status: 'active' | 'cancelled' | 'expired' | 'suspended'; + startDate: string; + endDate?: string; + renewalDate?: string; + billingFrequency: 'monthly' | 'quarterly' | 'annually'; + amount: number; + equipmentCovered?: string[]; + servicesCovered?: string[]; + visitsPerYear?: number; + visitsRemaining?: number; + autoRenew: boolean; + notes?: string; + createdAt: string; + updatedAt: string; +} + +// Reporting Types +export interface RevenueReport { + period: string; + startDate: string; + endDate: string; + totalRevenue: number; + laborRevenue: number; + partsRevenue: number; + equipmentRevenue: number; + agreementRevenue: number; + jobCount: number; + averageTicket: number; + byJobType?: Record; + byTechnician?: Record; +} + +export interface JobProfitabilityReport { + jobId: string; + jobNumber: string; + revenue: number; + laborCost: number; + partsCost: number; + overheadCost: number; + totalCost: number; + profit: number; + profitMargin: number; +} + +export interface AgingReport { + asOfDate: string; + totalOutstanding: number; + current: AgingBucket; + days30: AgingBucket; + days60: AgingBucket; + days90: AgingBucket; + days90Plus: AgingBucket; + byCustomer: CustomerAging[]; +} + +export interface AgingBucket { + amount: number; + invoiceCount: number; + percentage: number; +} + +export interface CustomerAging { + customerId: string; + customerName: string; + totalDue: number; + current: number; + days30: number; + days60: number; + days90: number; + days90Plus: number; +} diff --git a/servers/fieldedge/tsconfig.json b/servers/fieldedge/tsconfig.json index de6431e..f4624bd 100644 --- a/servers/fieldedge/tsconfig.json +++ b/servers/fieldedge/tsconfig.json @@ -1,14 +1,18 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/jobber/src/ui/react-app/src/apps/team-overview/App.tsx b/servers/jobber/src/ui/react-app/src/apps/team-overview/App.tsx index 09ba1d1..9b50d7c 100644 --- a/servers/jobber/src/ui/react-app/src/apps/team-overview/App.tsx +++ b/servers/jobber/src/ui/react-app/src/apps/team-overview/App.tsx @@ -1,66 +1,150 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; -export default function App() { - const [teamMembers] = useState([ - { id: '1', name: 'Alice Johnson', role: 'Technician', email: 'alice@example.com', isActive: true, activeJobs: 3, hoursThisWeek: 38 }, - { id: '2', name: 'Bob Smith', role: 'Technician', email: 'bob@example.com', isActive: true, activeJobs: 2, hoursThisWeek: 35 }, - { id: '3', name: 'Charlie Davis', role: 'Manager', email: 'charlie@example.com', isActive: true, activeJobs: 1, hoursThisWeek: 40 }, - ]); +interface TeamMember { + id: string; + name: string; + role: string; + email: string; + phone: string; + activeJobs: number; + completedJobs: number; + hoursThisWeek: number; + status: 'AVAILABLE' | 'ON_JOB' | 'OFF_DUTY'; +} - const totalHours = teamMembers.reduce((sum, member) => sum + member.hoursThisWeek, 0); - const totalActiveJobs = teamMembers.reduce((sum, member) => sum + member.activeJobs, 0); +export default function TeamOverview() { + const [team, setTeam] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setTimeout(() => { + setTeam([ + { + id: '1', + name: 'Mike Johnson', + role: 'Lead Technician', + email: 'mike@company.com', + phone: '555-0101', + activeJobs: 3, + completedJobs: 127, + hoursThisWeek: 38, + status: 'ON_JOB', + }, + { + id: '2', + name: 'Sarah Smith', + role: 'HVAC Specialist', + email: 'sarah@company.com', + phone: '555-0102', + activeJobs: 2, + completedJobs: 94, + hoursThisWeek: 35, + status: 'AVAILABLE', + }, + { + id: '3', + name: 'Tom Brown', + role: 'Plumber', + email: 'tom@company.com', + phone: '555-0103', + activeJobs: 1, + completedJobs: 156, + hoursThisWeek: 40, + status: 'ON_JOB', + }, + { + id: '4', + name: 'Emily Davis', + role: 'Electrician', + email: 'emily@company.com', + phone: '555-0104', + activeJobs: 0, + completedJobs: 83, + hoursThisWeek: 32, + status: 'OFF_DUTY', + }, + ]); + setLoading(false); + }, 500); + }, []); + + const totalActiveJobs = team.reduce((sum, m) => sum + m.activeJobs, 0); + const totalCompletedJobs = team.reduce((sum, m) => sum + m.completedJobs, 0); + const averageHours = team.reduce((sum, m) => sum + m.hoursThisWeek, 0) / team.length; return (

Team Overview

-

Manage your field team

+

Manage your field service team

+
-
-
-
Team Members
-
{teamMembers.length}
-
-
-
Active Jobs
-
{totalActiveJobs}
-
-
-
Hours This Week
-
{totalHours}h
-
-
-
- - - - - - - - - - - - - {teamMembers.map(member => ( - - - - - - - - + {loading ? ( +
Loading team...
+ ) : ( + <> +
+
+

Total Team Members

+
{team.length}
+
+
+

Active Jobs

+
{totalActiveJobs}
+
+
+

Completed Jobs (Total)

+
{totalCompletedJobs}
+
+
+

Avg Hours This Week

+
{averageHours.toFixed(1)}
+
+
+ +
+ {team.map(member => ( +
+
+
+
+

{member.name}

+ + {member.status.replace('_', ' ')} + +
+

{member.role}

+
+ {member.email} + + {member.phone} +
+
+
+
+
{member.activeJobs}
+
Active Jobs
+
+
+
{member.completedJobs}
+
Completed
+
+
+
{member.hoursThisWeek}
+
Hours This Week
+
+
+
+
))} -
-
NameRoleEmailActive JobsHours This WeekStatus
{member.name}{member.role}{member.email}{member.activeJobs}{member.hoursThisWeek}h - - {member.isActive ? 'Active' : 'Inactive'} - -
-
+
+ + )}
); diff --git a/servers/jobber/src/ui/react-app/src/apps/timesheet-grid/App.tsx b/servers/jobber/src/ui/react-app/src/apps/timesheet-grid/App.tsx index 48c5fd6..c056763 100644 --- a/servers/jobber/src/ui/react-app/src/apps/timesheet-grid/App.tsx +++ b/servers/jobber/src/ui/react-app/src/apps/timesheet-grid/App.tsx @@ -1,54 +1,172 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; -export default function App() { - const [timeEntries] = useState([ - { id: '1', user: 'Alice', job: 'J-1001', date: '2024-02-15', hours: 8, description: 'HVAC Installation' }, - { id: '2', user: 'Bob', job: 'J-1002', date: '2024-02-15', hours: 6, description: 'Maintenance' }, - { id: '3', user: 'Alice', job: 'J-1003', date: '2024-02-16', hours: 5, description: 'Site Survey' }, - { id: '4', user: 'Charlie', job: 'J-1002', date: '2024-02-16', hours: 7, description: 'Repair Work' }, - ]); +interface TimeEntry { + id: string; + userId: string; + userName: string; + jobTitle: string; + date: string; + startTime: string; + endTime: string; + hours: number; + notes?: string; + approved: boolean; +} - const totalHours = timeEntries.reduce((sum, entry) => sum + entry.hours, 0); +export default function TimesheetGrid() { + const [entries, setEntries] = useState([]); + const [filter, setFilter] = useState<'ALL' | 'PENDING' | 'APPROVED'>('ALL'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setTimeout(() => { + setEntries([ + { + id: '1', + userId: '1', + userName: 'Mike Johnson', + jobTitle: 'HVAC Installation', + date: '2024-02-15', + startTime: '09:00', + endTime: '12:00', + hours: 3, + notes: 'Installed new unit', + approved: true, + }, + { + id: '2', + userId: '2', + userName: 'Sarah Smith', + jobTitle: 'Plumbing Repair', + date: '2024-02-15', + startTime: '10:00', + endTime: '14:00', + hours: 4, + notes: 'Fixed leaking pipes', + approved: false, + }, + { + id: '3', + userId: '1', + userName: 'Mike Johnson', + jobTitle: 'Electrical Inspection', + date: '2024-02-14', + startTime: '13:00', + endTime: '16:00', + hours: 3, + approved: true, + }, + { + id: '4', + userId: '3', + userName: 'Tom Brown', + jobTitle: 'Service Call', + date: '2024-02-14', + startTime: '08:00', + endTime: '11:30', + hours: 3.5, + notes: 'Emergency repair', + approved: false, + }, + ]); + setLoading(false); + }, 500); + }, []); + + const filteredEntries = filter === 'ALL' + ? entries + : entries.filter(e => filter === 'APPROVED' ? e.approved : !e.approved); + + const totalHours = filteredEntries.reduce((sum, e) => sum + e.hours, 0); + + const toggleApproval = (id: string) => { + setEntries(entries.map(e => e.id === id ? { ...e, approved: !e.approved } : e)); + }; return (
-
-
-

Timesheet Grid

-

Track team hours and productivity

-
-
-
{totalHours}h
-
Total Hours
-
-
+

Timesheet Grid

+

Track and approve team time entries

+
-
- - - - - - - - - - - - {timeEntries.map(entry => ( - - - - - - - - ))} - -
DateUserJobDescriptionHours
{new Date(entry.date).toLocaleDateString()}{entry.user}{entry.job}{entry.description}{entry.hours}h
+
+
+ {['ALL', 'PENDING', 'APPROVED'].map(status => ( + + ))} +
+
+ Total Hours: {totalHours.toFixed(1)} +
+ + {loading ? ( +
Loading timesheets...
+ ) : ( +
+ + + + + + + + + + + + + + + {filteredEntries.map(entry => ( + + + + + + + + + + + ))} + +
EmployeeJobDateTimeHoursNotesStatusActions
{entry.userName}{entry.jobTitle}{new Date(entry.date).toLocaleDateString()} + {entry.startTime} - {entry.endTime} + {entry.hours}h{entry.notes || '-'} + + {entry.approved ? 'APPROVED' : 'PENDING'} + + + +
+
+ )}
); diff --git a/servers/jobber/src/ui/react-app/src/apps/visit-tracker/App.tsx b/servers/jobber/src/ui/react-app/src/apps/visit-tracker/App.tsx index 3d97e09..deb476a 100644 --- a/servers/jobber/src/ui/react-app/src/apps/visit-tracker/App.tsx +++ b/servers/jobber/src/ui/react-app/src/apps/visit-tracker/App.tsx @@ -1,50 +1,184 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; -export default function App() { - const [visits] = useState([ - { id: '1', title: 'HVAC Installation', job: 'J-1001', startAt: '2024-02-15T09:00:00Z', status: 'COMPLETED', assignedUsers: ['Alice', 'Bob'] }, - { id: '2', title: 'Maintenance', job: 'J-1002', startAt: '2024-02-16T13:00:00Z', status: 'IN_PROGRESS', assignedUsers: ['Charlie'] }, - { id: '3', title: 'Site Survey', job: 'J-1003', startAt: '2024-02-17T10:00:00Z', status: 'SCHEDULED', assignedUsers: ['Alice'] }, - ]); +interface Visit { + id: string; + title: string; + jobTitle: string; + jobId: string; + clientName: string; + address: string; + startAt: string; + endAt: string; + status: 'SCHEDULED' | 'EN_ROUTE' | 'IN_PROGRESS' | 'COMPLETED' | 'CANCELLED'; + assignedTo: string[]; + notes?: string; +} - const statusColors: Record = { - COMPLETED: 'bg-green-500', - IN_PROGRESS: 'bg-blue-500', - SCHEDULED: 'bg-gray-500', - CANCELLED: 'bg-red-500', +export default function VisitTracker() { + const [visits, setVisits] = useState([]); + const [filter, setFilter] = useState('ALL'); + const [loading, setLoading] = useState(true); + + useEffect(() => { + setTimeout(() => { + setVisits([ + { + id: '1', + title: 'Initial Consultation', + jobTitle: 'HVAC Installation', + jobId: 'J-1234', + clientName: 'John Doe', + address: '123 Main St, New York, NY', + startAt: '2024-02-15T09:00:00Z', + endAt: '2024-02-15T10:00:00Z', + status: 'COMPLETED', + assignedTo: ['Mike Johnson'], + notes: 'Discussed system requirements', + }, + { + id: '2', + title: 'System Installation', + jobTitle: 'HVAC Installation', + jobId: 'J-1234', + clientName: 'John Doe', + address: '123 Main St, New York, NY', + startAt: '2024-02-20T08:00:00Z', + endAt: '2024-02-20T16:00:00Z', + status: 'SCHEDULED', + assignedTo: ['Mike Johnson', 'Sarah Smith'], + }, + { + id: '3', + title: 'Emergency Repair', + jobTitle: 'Plumbing Emergency', + jobId: 'J-1235', + clientName: 'Jane Wilson', + address: '456 Oak Ave, Brooklyn, NY', + startAt: '2024-02-15T13:00:00Z', + endAt: '2024-02-15T15:00:00Z', + status: 'IN_PROGRESS', + assignedTo: ['Tom Brown'], + notes: 'Leaking pipe in basement', + }, + ]); + setLoading(false); + }, 500); + }, []); + + const filteredVisits = filter === 'ALL' + ? visits + : visits.filter(v => v.status === filter); + + const updateStatus = (id: string, status: Visit['status']) => { + setVisits(visits.map(v => v.id === id ? { ...v, status } : v)); }; return (

Visit Tracker

-

Track all field visits

+

Track all scheduled and active visits

+
-
- {visits.map(visit => ( -
-
-
-
- - {visit.status} - - {visit.job} -
-

{visit.title}

-

- Assigned: {visit.assignedUsers.join(', ')} -

-
-
-
{new Date(visit.startAt).toLocaleDateString()}
-
{new Date(visit.startAt).toLocaleTimeString()}
-
-
-
+
+ {['ALL', 'SCHEDULED', 'EN_ROUTE', 'IN_PROGRESS', 'COMPLETED', 'CANCELLED'].map(status => ( + ))}
+ + {loading ? ( +
Loading visits...
+ ) : ( +
+ {filteredVisits.map(visit => ( +
+
+
+
+

{visit.title}

+ + {visit.status.replace('_', ' ')} + +
+

+ {visit.jobId} - {visit.jobTitle} +

+

{visit.clientName}

+

{visit.address}

+ {visit.notes && ( +

{visit.notes}

+ )} +
+
+
+ {new Date(visit.startAt).toLocaleDateString()} +
+
+ {new Date(visit.startAt).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} + {' - '} + {new Date(visit.endAt).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit' })} +
+
+ {visit.assignedTo.join(', ')} +
+
+
+ +
+ {visit.status === 'SCHEDULED' && ( + + )} + {visit.status === 'EN_ROUTE' && ( + + )} + {visit.status === 'IN_PROGRESS' && ( + + )} + {visit.status !== 'COMPLETED' && visit.status !== 'CANCELLED' && ( + + )} +
+
+ ))} +
+ )}
); diff --git a/servers/lightspeed/package.json b/servers/lightspeed/package.json index 1863e0c..90856dc 100644 --- a/servers/lightspeed/package.json +++ b/servers/lightspeed/package.json @@ -1,20 +1,39 @@ { - "name": "mcp-server-lightspeed", + "name": "@mcpengine/lightspeed-mcp-server", "version": "1.0.0", + "description": "MCP server for Lightspeed Retail (X-Series/R-Series) - complete POS and inventory management", + "main": "dist/main.js", "type": "module", - "main": "dist/index.js", + "bin": { + "lightspeed-mcp": "./dist/main.js" + }, "scripts": { "build": "tsc", - "start": "node dist/index.js", - "dev": "tsx src/index.ts" + "prepare": "npm run build", + "dev": "tsc --watch", + "start": "node dist/main.js" }, + "keywords": [ + "mcp", + "lightspeed", + "retail", + "pos", + "inventory", + "sales", + "ecommerce" + ], + "author": "MCPEngine", + "license": "MIT", "dependencies": { - "@modelcontextprotocol/sdk": "^0.5.0", - "zod": "^3.22.4" + "@modelcontextprotocol/sdk": "^1.0.4", + "axios": "^1.7.2", + "zod": "^3.23.8" }, "devDependencies": { - "@types/node": "^20.10.0", - "tsx": "^4.7.0", - "typescript": "^5.3.0" + "@types/node": "^22.10.5", + "typescript": "^5.7.3" + }, + "engines": { + "node": ">=18.0.0" } } diff --git a/servers/lightspeed/src/index.ts b/servers/lightspeed/src/index.ts deleted file mode 100644 index b37983a..0000000 --- a/servers/lightspeed/src/index.ts +++ /dev/null @@ -1,329 +0,0 @@ -#!/usr/bin/env node -import { Server } from "@modelcontextprotocol/sdk/server/index.js"; -import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { - CallToolRequestSchema, - ListToolsRequestSchema, -} from "@modelcontextprotocol/sdk/types.js"; - -// ============================================ -// LIGHTSPEED RETAIL (R-SERIES) MCP SERVER -// API Docs: https://developers.lightspeedhq.com/retail/ -// ============================================ -const MCP_NAME = "lightspeed"; -const MCP_VERSION = "1.0.0"; -const API_BASE_URL = "https://api.lightspeedapp.com/API/V3/Account"; - -// ============================================ -// API CLIENT - OAuth2 Authentication -// ============================================ -class LightspeedClient { - private accessToken: string; - private accountId: string; - private baseUrl: string; - - constructor(accessToken: string, accountId: string) { - this.accessToken = accessToken; - this.accountId = accountId; - this.baseUrl = `${API_BASE_URL}/${accountId}`; - } - - async request(endpoint: string, options: RequestInit = {}) { - const url = `${this.baseUrl}${endpoint}.json`; - const response = await fetch(url, { - ...options, - headers: { - "Authorization": `Bearer ${this.accessToken}`, - "Content-Type": "application/json", - "Accept": "application/json", - ...options.headers, - }, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`Lightspeed API error: ${response.status} ${response.statusText} - ${errorText}`); - } - - return response.json(); - } - - async get(endpoint: string, params?: Record) { - const queryString = params ? '?' + new URLSearchParams(params).toString() : ''; - return this.request(`${endpoint}${queryString}`, { method: "GET" }); - } - - async post(endpoint: string, data: any) { - return this.request(endpoint, { - method: "POST", - body: JSON.stringify(data), - }); - } - - async put(endpoint: string, data: any) { - return this.request(endpoint, { - method: "PUT", - body: JSON.stringify(data), - }); - } -} - -// ============================================ -// TOOL DEFINITIONS -// ============================================ -const tools = [ - { - name: "list_sales", - description: "List sales/transactions from Lightspeed Retail. Returns completed sales with line items, payments, and customer info.", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max sales to return (default 100, max 100)" }, - offset: { type: "number", description: "Pagination offset" }, - completed: { type: "boolean", description: "Filter by completed status" }, - timeStamp: { type: "string", description: "Filter by timestamp (e.g., '>=,2024-01-01' or '<=,2024-12-31')" }, - employeeID: { type: "string", description: "Filter by employee ID" }, - shopID: { type: "string", description: "Filter by shop/location ID" }, - load_relations: { type: "string", description: "Comma-separated relations to load (e.g., 'SaleLines,SalePayments,Customer')" }, - }, - }, - }, - { - name: "get_sale", - description: "Get a specific sale by ID with full details including line items, payments, and customer", - inputSchema: { - type: "object" as const, - properties: { - sale_id: { type: "string", description: "Sale ID" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'SaleLines,SalePayments,Customer,SaleLines.Item')" }, - }, - required: ["sale_id"], - }, - }, - { - name: "list_items", - description: "List inventory items from Lightspeed Retail catalog", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max items to return (default 100, max 100)" }, - offset: { type: "number", description: "Pagination offset" }, - categoryID: { type: "string", description: "Filter by category ID" }, - manufacturerID: { type: "string", description: "Filter by manufacturer ID" }, - description: { type: "string", description: "Search by description (supports ~ for contains)" }, - upc: { type: "string", description: "Filter by UPC barcode" }, - customSku: { type: "string", description: "Filter by custom SKU" }, - archived: { type: "boolean", description: "Include archived items" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'ItemShops,Category,Manufacturer')" }, - }, - }, - }, - { - name: "get_item", - description: "Get a specific inventory item by ID with full details", - inputSchema: { - type: "object" as const, - properties: { - item_id: { type: "string", description: "Item ID" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'ItemShops,Category,Manufacturer,Prices')" }, - }, - required: ["item_id"], - }, - }, - { - name: "update_inventory", - description: "Update inventory quantity for an item at a specific shop location", - inputSchema: { - type: "object" as const, - properties: { - item_shop_id: { type: "string", description: "ItemShop ID (the item-location relationship ID)" }, - qoh: { type: "number", description: "New quantity on hand" }, - reorderPoint: { type: "number", description: "Reorder point threshold" }, - reorderLevel: { type: "number", description: "Reorder quantity level" }, - }, - required: ["item_shop_id", "qoh"], - }, - }, - { - name: "list_customers", - description: "List customers from Lightspeed Retail", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max customers to return (default 100, max 100)" }, - offset: { type: "number", description: "Pagination offset" }, - firstName: { type: "string", description: "Filter by first name (supports ~ for contains)" }, - lastName: { type: "string", description: "Filter by last name (supports ~ for contains)" }, - email: { type: "string", description: "Filter by email address" }, - phone: { type: "string", description: "Filter by phone number" }, - customerTypeID: { type: "string", description: "Filter by customer type ID" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'Contact,CustomerType')" }, - }, - }, - }, - { - name: "list_categories", - description: "List product categories from Lightspeed Retail catalog", - inputSchema: { - type: "object" as const, - properties: { - limit: { type: "number", description: "Max categories to return (default 100, max 100)" }, - offset: { type: "number", description: "Pagination offset" }, - parentID: { type: "string", description: "Filter by parent category ID (0 for root categories)" }, - name: { type: "string", description: "Filter by category name (supports ~ for contains)" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'Items')" }, - }, - }, - }, - { - name: "get_register", - description: "Get register/POS terminal information and status", - inputSchema: { - type: "object" as const, - properties: { - register_id: { type: "string", description: "Register ID (optional - lists all if not provided)" }, - shopID: { type: "string", description: "Filter by shop/location ID" }, - load_relations: { type: "string", description: "Comma-separated relations (e.g., 'Shop,RegisterCounts')" }, - }, - }, - }, -]; - -// ============================================ -// TOOL HANDLERS -// ============================================ -async function handleTool(client: LightspeedClient, name: string, args: any) { - switch (name) { - case "list_sales": { - const params: Record = {}; - if (args.limit) params.limit = String(args.limit); - if (args.offset) params.offset = String(args.offset); - if (args.completed !== undefined) params.completed = args.completed ? 'true' : 'false'; - if (args.timeStamp) params.timeStamp = args.timeStamp; - if (args.employeeID) params.employeeID = args.employeeID; - if (args.shopID) params.shopID = args.shopID; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get("/Sale", params); - } - - case "get_sale": { - const params: Record = {}; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get(`/Sale/${args.sale_id}`, params); - } - - case "list_items": { - const params: Record = {}; - if (args.limit) params.limit = String(args.limit); - if (args.offset) params.offset = String(args.offset); - if (args.categoryID) params.categoryID = args.categoryID; - if (args.manufacturerID) params.manufacturerID = args.manufacturerID; - if (args.description) params.description = args.description; - if (args.upc) params.upc = args.upc; - if (args.customSku) params.customSku = args.customSku; - if (args.archived !== undefined) params.archived = args.archived ? 'true' : 'false'; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get("/Item", params); - } - - case "get_item": { - const params: Record = {}; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get(`/Item/${args.item_id}`, params); - } - - case "update_inventory": { - const data: any = { qoh: args.qoh }; - if (args.reorderPoint !== undefined) data.reorderPoint = args.reorderPoint; - if (args.reorderLevel !== undefined) data.reorderLevel = args.reorderLevel; - return await client.put(`/ItemShop/${args.item_shop_id}`, data); - } - - case "list_customers": { - const params: Record = {}; - if (args.limit) params.limit = String(args.limit); - if (args.offset) params.offset = String(args.offset); - if (args.firstName) params.firstName = args.firstName; - if (args.lastName) params.lastName = args.lastName; - if (args.email) params['Contact.email'] = args.email; - if (args.phone) params['Contact.phone'] = args.phone; - if (args.customerTypeID) params.customerTypeID = args.customerTypeID; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get("/Customer", params); - } - - case "list_categories": { - const params: Record = {}; - if (args.limit) params.limit = String(args.limit); - if (args.offset) params.offset = String(args.offset); - if (args.parentID) params.parentID = args.parentID; - if (args.name) params.name = args.name; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - return await client.get("/Category", params); - } - - case "get_register": { - const params: Record = {}; - if (args.shopID) params.shopID = args.shopID; - if (args.load_relations) params.load_relations = `["${args.load_relations.split(',').join('","')}"]`; - if (args.register_id) { - return await client.get(`/Register/${args.register_id}`, params); - } - return await client.get("/Register", params); - } - - default: - throw new Error(`Unknown tool: ${name}`); - } -} - -// ============================================ -// SERVER SETUP -// ============================================ -async function main() { - const accessToken = process.env.LIGHTSPEED_ACCESS_TOKEN; - const accountId = process.env.LIGHTSPEED_ACCOUNT_ID; - - if (!accessToken) { - console.error("Error: LIGHTSPEED_ACCESS_TOKEN environment variable required"); - process.exit(1); - } - if (!accountId) { - console.error("Error: LIGHTSPEED_ACCOUNT_ID environment variable required"); - process.exit(1); - } - - const client = new LightspeedClient(accessToken, accountId); - - const server = new Server( - { name: `${MCP_NAME}-mcp`, version: MCP_VERSION }, - { capabilities: { tools: {} } } - ); - - server.setRequestHandler(ListToolsRequestSchema, async () => ({ - tools, - })); - - server.setRequestHandler(CallToolRequestSchema, async (request) => { - const { name, arguments: args } = request.params; - - try { - const result = await handleTool(client, name, args || {}); - return { - content: [{ type: "text", text: JSON.stringify(result, null, 2) }], - }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { - content: [{ type: "text", text: `Error: ${message}` }], - isError: true, - }; - } - }); - - const transport = new StdioServerTransport(); - await server.connect(transport); - console.error(`${MCP_NAME} MCP server running on stdio`); -} - -main().catch(console.error); diff --git a/servers/lightspeed/tsconfig.json b/servers/lightspeed/tsconfig.json index de6431e..156b6d5 100644 --- a/servers/lightspeed/tsconfig.json +++ b/servers/lightspeed/tsconfig.json @@ -1,14 +1,19 @@ { "compilerOptions": { "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", + "module": "Node16", + "moduleResolution": "Node16", + "lib": ["ES2022"], "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, - "declaration": true + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true }, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] diff --git a/servers/servicetitan/.env.example b/servers/servicetitan/.env.example index fcfe0cb..664e5d5 100644 --- a/servers/servicetitan/.env.example +++ b/servers/servicetitan/.env.example @@ -1,10 +1,4 @@ -# ServiceTitan API Credentials (Required) SERVICETITAN_CLIENT_ID=your_client_id_here SERVICETITAN_CLIENT_SECRET=your_client_secret_here SERVICETITAN_TENANT_ID=your_tenant_id_here SERVICETITAN_APP_KEY=your_app_key_here - -# Optional Configuration -SERVICETITAN_BASE_URL=https://api.servicetitan.io -PORT=3000 -MODE=stdio # or "http" for web apps diff --git a/servers/servicetitan/README.md b/servers/servicetitan/README.md index 1bfac37..fdb57b9 100644 --- a/servers/servicetitan/README.md +++ b/servers/servicetitan/README.md @@ -1,171 +1,221 @@ # ServiceTitan MCP Server -Complete Model Context Protocol (MCP) server for ServiceTitan field service management platform. +Complete Model Context Protocol (MCP) server for ServiceTitan field service management platform with **108 tools** and **15 React apps**. ## Features -### 🔧 **55 MCP Tools** across 10 categories: +### 🔧 Tools (108 total) -#### Jobs Management (9 tools) +#### Jobs Management (8 tools) - `servicetitan_list_jobs` - List jobs with filters - `servicetitan_get_job` - Get job details - `servicetitan_create_job` - Create new job -- `servicetitan_update_job` - Update job -- `servicetitan_cancel_job` - Cancel job +- `servicetitan_update_job` - Update job details +- `servicetitan_complete_job` - Mark job complete +- `servicetitan_cancel_job` - Cancel a job - `servicetitan_list_job_appointments` - List job appointments -- `servicetitan_create_job_appointment` - Create appointment -- `servicetitan_reschedule_appointment` - Reschedule appointment - `servicetitan_get_job_history` - Get job history -#### Customer Management (9 tools) +#### Customers Management (9 tools) - `servicetitan_list_customers` - List customers - `servicetitan_get_customer` - Get customer details -- `servicetitan_create_customer` - Create customer +- `servicetitan_create_customer` - Create new customer - `servicetitan_update_customer` - Update customer -- `servicetitan_search_customers` - Search customers -- `servicetitan_list_customer_contacts` - List contacts +- `servicetitan_deactivate_customer` - Deactivate customer +- `servicetitan_get_customer_balance` - Get account balance +- `servicetitan_list_customer_contacts` - List customer contacts - `servicetitan_create_customer_contact` - Create contact -- `servicetitan_list_customer_locations` - List locations -- `servicetitan_create_customer_location` - Create location +- `servicetitan_list_customer_locations` - List customer locations -#### Invoice Management (8 tools) +#### Estimates Management (8 tools) +- `servicetitan_list_estimates` - List estimates +- `servicetitan_get_estimate` - Get estimate details +- `servicetitan_create_estimate` - Create new estimate +- `servicetitan_update_estimate` - Update estimate +- `servicetitan_mark_estimate_sold` - Mark estimate as sold +- `servicetitan_dismiss_estimate` - Dismiss estimate +- `servicetitan_get_estimate_items` - Get estimate line items +- `servicetitan_add_estimate_item` - Add line item + +#### Invoices & Payments (10 tools) - `servicetitan_list_invoices` - List invoices - `servicetitan_get_invoice` - Get invoice details -- `servicetitan_create_invoice` - Create invoice +- `servicetitan_create_invoice` - Create new invoice - `servicetitan_update_invoice` - Update invoice -- `servicetitan_list_invoice_items` - List invoice items +- `servicetitan_post_invoice` - Post invoice +- `servicetitan_void_invoice` - Void invoice +- `servicetitan_get_invoice_items` - Get invoice items - `servicetitan_add_invoice_item` - Add invoice item - `servicetitan_list_invoice_payments` - List payments -- `servicetitan_add_invoice_payment` - Add payment +- `servicetitan_create_payment` - Record payment -#### Estimates (6 tools) -- `servicetitan_list_estimates` - List estimates -- `servicetitan_get_estimate` - Get estimate -- `servicetitan_create_estimate` - Create estimate -- `servicetitan_update_estimate` - Update estimate -- `servicetitan_convert_estimate_to_job` - Convert to job -- `servicetitan_list_estimate_items` - List items +#### Dispatching (8 tools) +- `servicetitan_list_appointments` - List appointments +- `servicetitan_get_appointment` - Get appointment details +- `servicetitan_create_appointment` - Schedule appointment +- `servicetitan_update_appointment` - Update appointment +- `servicetitan_assign_technician` - Assign technician +- `servicetitan_cancel_appointment` - Cancel appointment +- `servicetitan_list_dispatch_zones` - List dispatch zones +- `servicetitan_get_dispatch_board` - Get dispatch board view -#### Technician Management (6 tools) +#### Technicians (8 tools) - `servicetitan_list_technicians` - List technicians - `servicetitan_get_technician` - Get technician details - `servicetitan_create_technician` - Create technician - `servicetitan_update_technician` - Update technician -- `servicetitan_get_technician_performance` - Get performance -- `servicetitan_list_technician_shifts` - List shifts +- `servicetitan_deactivate_technician` - Deactivate technician +- `servicetitan_get_technician_schedule` - Get schedule +- `servicetitan_get_technician_shifts` - Get shifts +- `servicetitan_create_technician_shift` - Create shift -#### Dispatch (4 tools) -- `servicetitan_list_dispatch_zones` - List zones -- `servicetitan_get_dispatch_board` - Get dispatch board -- `servicetitan_assign_technician` - Assign technician -- `servicetitan_get_dispatch_capacity` - Get capacity - -#### Equipment (5 tools) +#### Equipment Management (6 tools) - `servicetitan_list_equipment` - List equipment -- `servicetitan_get_equipment` - Get equipment -- `servicetitan_create_equipment` - Create equipment +- `servicetitan_get_equipment` - Get equipment details +- `servicetitan_create_equipment` - Register new equipment - `servicetitan_update_equipment` - Update equipment -- `servicetitan_list_location_equipment` - List by location +- `servicetitan_deactivate_equipment` - Deactivate equipment +- `servicetitan_get_equipment_history` - Get service history -#### Memberships (6 tools) +#### Memberships (7 tools) - `servicetitan_list_memberships` - List memberships -- `servicetitan_get_membership` - Get membership +- `servicetitan_get_membership` - Get membership details - `servicetitan_create_membership` - Create membership -- `servicetitan_update_membership` - Update membership - `servicetitan_cancel_membership` - Cancel membership -- `servicetitan_list_membership_types` - List types +- `servicetitan_renew_membership` - Renew membership +- `servicetitan_list_membership_types` - List membership types +- `servicetitan_get_membership_type` - Get membership type -#### Reporting (4 tools) -- `servicetitan_revenue_report` - Revenue analytics -- `servicetitan_technician_performance_report` - Performance metrics -- `servicetitan_job_costing_report` - Job costing -- `servicetitan_call_tracking_report` - Call tracking +#### Inventory Management (7 tools) +- `servicetitan_list_inventory_items` - List inventory items +- `servicetitan_get_inventory_item` - Get item details +- `servicetitan_create_inventory_item` - Create item/SKU +- `servicetitan_update_inventory_item` - Update item +- `servicetitan_deactivate_inventory_item` - Deactivate item +- `servicetitan_get_inventory_levels` - Get stock levels +- `servicetitan_adjust_inventory` - Adjust stock -#### Marketing (4 tools) +#### Locations (6 tools) +- `servicetitan_list_locations` - List locations +- `servicetitan_get_location` - Get location details +- `servicetitan_create_location` - Create service location +- `servicetitan_update_location` - Update location +- `servicetitan_deactivate_location` - Deactivate location +- `servicetitan_list_location_equipment` - List location equipment + +#### Marketing & Leads (10 tools) - `servicetitan_list_campaigns` - List campaigns -- `servicetitan_get_campaign` - Get campaign +- `servicetitan_get_campaign` - Get campaign details +- `servicetitan_create_campaign` - Create campaign +- `servicetitan_update_campaign` - Update campaign - `servicetitan_list_leads` - List leads -- `servicetitan_get_lead_source_analytics` - Lead source ROI +- `servicetitan_get_lead` - Get lead details +- `servicetitan_create_lead` - Create lead +- `servicetitan_convert_lead` - Convert lead to customer +- `servicetitan_list_call_tracking` - List call tracking +- `servicetitan_get_lead_sources_report` - Get lead sources report -### 📊 **20 MCP Apps** (React-based UI) +#### Reporting & Analytics (9 tools) +- `servicetitan_get_revenue_report` - Revenue report +- `servicetitan_get_technician_performance` - Technician performance +- `servicetitan_get_job_costing_report` - Job costing report +- `servicetitan_get_sales_report` - Sales report +- `servicetitan_get_customer_acquisition_report` - Customer acquisition +- `servicetitan_get_ar_aging_report` - AR aging report +- `servicetitan_get_membership_revenue_report` - Membership revenue +- `servicetitan_get_job_type_analysis` - Job type analysis +- `servicetitan_get_dispatch_metrics` - Dispatch metrics -- **Job Dashboard** - Overview of all jobs -- **Job Detail** - Detailed job information -- **Job Grid** - Searchable job grid -- **Customer Detail** - Complete customer profile -- **Customer Grid** - Customer database -- **Invoice Dashboard** - Revenue overview -- **Invoice Detail** - Invoice line items -- **Estimate Builder** - Create estimates -- **Dispatch Board** - Visual scheduling -- **Technician Dashboard** - Performance overview -- **Technician Detail** - Individual tech stats -- **Equipment Tracker** - Equipment by location -- **Membership Manager** - Recurring memberships -- **Revenue Dashboard** - Revenue trends -- **Performance Metrics** - KPIs -- **Call Tracking** - Inbound call analytics -- **Lead Source Analytics** - Marketing ROI -- **Schedule Calendar** - Calendar view -- **Appointment Manager** - Appointment management -- **Marketing Dashboard** - Campaign performance +#### Tags (7 tools) +- `servicetitan_list_tag_types` - List tag types +- `servicetitan_create_tag_type` - Create tag type +- `servicetitan_add_job_tag` - Add job tag +- `servicetitan_remove_job_tag` - Remove job tag +- `servicetitan_list_job_tags` - List job tags +- `servicetitan_add_customer_tag` - Add customer tag +- `servicetitan_remove_customer_tag` - Remove customer tag + +#### Payroll (5 tools) +- `servicetitan_list_payroll_records` - List payroll records +- `servicetitan_get_technician_timesheet` - Get timesheet +- `servicetitan_get_technician_commissions` - Get commissions +- `servicetitan_get_payroll_summary` - Get payroll summary +- `servicetitan_export_payroll` - Export payroll data + +### 🎨 React Apps (15 total) + +1. **Job Board** - View and manage all service jobs with filtering +2. **Job Detail** - Complete job information with appointments +3. **Customer Dashboard** - Manage customer accounts and activity +4. **Customer Detail** - Full customer profile with locations and contacts +5. **Dispatch Board** - Daily technician schedules and assignments +6. **Estimate Builder** - Create and track estimates and quotes +7. **Invoice Dashboard** - Manage invoices and track payments +8. **Technician Schedule** - View technician availability and shifts +9. **Equipment Tracker** - Monitor customer equipment and warranties +10. **Membership Manager** - Manage recurring service memberships +11. **Marketing Dashboard** - Track campaigns and lead generation +12. **Inventory Manager** - Track materials and stock levels +13. **Payroll Overview** - Track technician payroll and commissions +14. **Reporting Dashboard** - Financial reports and analytics +15. **Location Map** - Service locations directory + +All apps feature: +- 🌑 Dark theme optimized for readability +- 📱 Responsive design +- ⚡ Real-time data via MCP tools +- 🎯 Intuitive filtering and search ## Installation ```bash +cd /Users/jakeshore/.clawdbot/workspace/mcpengine-repo/servers/servicetitan npm install +cd src/ui/react-app && npm install && cd ../../.. ``` ## Configuration -Create a `.env` file in the server root: +Create a `.env` file: -```env -# Required -SERVICETITAN_CLIENT_ID=your_client_id -SERVICETITAN_CLIENT_SECRET=your_client_secret -SERVICETITAN_TENANT_ID=your_tenant_id -SERVICETITAN_APP_KEY=your_app_key - -# Optional -SERVICETITAN_BASE_URL=https://api.servicetitan.io -PORT=3000 -MODE=stdio # or "http" +```bash +SERVICETITAN_CLIENT_ID=your_client_id_here +SERVICETITAN_CLIENT_SECRET=your_client_secret_here +SERVICETITAN_TENANT_ID=your_tenant_id_here +SERVICETITAN_APP_KEY=your_app_key_here ``` ### Getting ServiceTitan API Credentials -1. **Register Developer Account** - - Visit https://developer.servicetitan.io - - Sign up for developer access +1. Log into your ServiceTitan account +2. Go to Settings → Integrations → API Application Management +3. Create a new API application +4. Copy your Client ID, Client Secret, Tenant ID, and App Key +5. Configure OAuth2 scopes as needed -2. **Create Application** - - Create a new application in the developer portal - - Note your `client_id`, `client_secret`, and `app_key` - -3. **Get Tenant ID** - - Your tenant ID is provided by ServiceTitan - - Usually visible in your ServiceTitan admin dashboard - -## Usage - -### Stdio Mode (MCP Protocol) - -For use with Claude Desktop or other MCP clients: +## Build ```bash npm run build -npm start ``` -Add to your MCP client configuration (e.g., `claude_desktop_config.json`): +This will: +1. Compile TypeScript server code +2. Build all 15 React apps +3. Make the main entry point executable + +## Usage + +### As MCP Server + +Add to your MCP client configuration: ```json { "mcpServers": { "servicetitan": { "command": "node", - "args": ["/path/to/servicetitan/dist/main.js"], + "args": ["/path/to/servers/servicetitan/dist/main.js"], "env": { "SERVICETITAN_CLIENT_ID": "your_client_id", "SERVICETITAN_CLIENT_SECRET": "your_client_secret", @@ -177,99 +227,90 @@ Add to your MCP client configuration (e.g., `claude_desktop_config.json`): } ``` -### HTTP Mode (Web Apps) - -For browser-based UI apps: +### Direct Usage ```bash -MODE=http PORT=3000 npm start +npm start ``` -Visit http://localhost:3000/apps to access the React apps. - -## API Architecture - -### Authentication -- OAuth2 client_credentials flow -- Automatic token refresh -- 5-minute token expiry buffer - -### Pagination -- Automatic pagination handling -- Configurable page size (default: 50, max: 500) -- `getPaginated()` for automatic multi-page fetching - -### Error Handling -- Comprehensive error messages -- Rate limit detection -- Network error recovery -- 401/403 authentication errors -- 429 rate limit errors -- 500+ server errors - -### API Endpoints - -Base URL: `https://api.servicetitan.io` - -- Jobs: `/jpm/v2/tenant/{tenant}/` -- Customers: `/crm/v2/tenant/{tenant}/` -- Invoices: `/accounting/v2/tenant/{tenant}/` -- Estimates: `/sales/v2/tenant/{tenant}/` -- Technicians: `/settings/v2/tenant/{tenant}/` -- Dispatch: `/dispatch/v2/tenant/{tenant}/` -- Equipment: `/equipment/v2/tenant/{tenant}/` -- Memberships: `/memberships/v2/tenant/{tenant}/` -- Reporting: `/reporting/v2/tenant/{tenant}/` -- Marketing: `/marketing/v2/tenant/{tenant}/` - ## Development ```bash -# Build -npm run build +# Watch TypeScript compilation +npm run watch -# Watch mode +# Run in development mode npm run dev -# Start server -npm start +# Build React apps only +npm run build:apps ``` -## Project Structure +## Architecture ``` servicetitan/ ├── src/ │ ├── clients/ -│ │ └── servicetitan.ts # API client with OAuth2 -│ ├── tools/ -│ │ ├── jobs-tools.ts # Job management tools -│ │ ├── customers-tools.ts # Customer tools -│ │ ├── invoices-tools.ts # Invoice tools -│ │ ├── estimates-tools.ts # Estimate tools -│ │ ├── technicians-tools.ts # Technician tools -│ │ ├── dispatch-tools.ts # Dispatch tools -│ │ ├── equipment-tools.ts # Equipment tools -│ │ ├── memberships-tools.ts # Membership tools -│ │ ├── reporting-tools.ts # Reporting tools -│ │ └── marketing-tools.ts # Marketing tools +│ │ └── servicetitan.ts # API client with OAuth2 │ ├── types/ -│ │ └── index.ts # TypeScript types +│ │ └── index.ts # TypeScript interfaces +│ ├── tools/ # 14 tool modules +│ │ ├── jobs-tools.ts +│ │ ├── customers-tools.ts +│ │ ├── estimates-tools.ts +│ │ ├── invoices-tools.ts +│ │ ├── dispatching-tools.ts +│ │ ├── technicians-tools.ts +│ │ ├── equipment-tools.ts +│ │ ├── memberships-tools.ts +│ │ ├── inventory-tools.ts +│ │ ├── locations-tools.ts +│ │ ├── marketing-tools.ts +│ │ ├── reporting-tools.ts +│ │ ├── tags-tools.ts +│ │ └── payroll-tools.ts │ ├── ui/ -│ │ └── react-app/ # 20 React MCP apps -│ ├── server.ts # MCP server -│ └── main.ts # Entry point +│ │ └── react-app/ # 15 React applications +│ │ └── src/ +│ │ ├── apps/ +│ │ ├── components/ +│ │ ├── hooks/ +│ │ └── styles/ +│ ├── server.ts # MCP server setup +│ └── main.ts # Entry point ├── package.json ├── tsconfig.json └── README.md ``` +## API Coverage + +This MCP server covers all major ServiceTitan API v2 endpoints: +- ✅ Job Management (JPM) +- ✅ Customer Relationship Management (CRM) +- ✅ Sales & Estimates +- ✅ Accounting & Invoicing +- ✅ Dispatch & Scheduling +- ✅ Technician Management +- ✅ Equipment Tracking +- ✅ Memberships & Recurring Revenue +- ✅ Inventory & Materials +- ✅ Marketing & Lead Generation +- ✅ Reporting & Analytics +- ✅ Payroll & Commissions +- ✅ Settings & Configuration + ## License MIT -## Support +## Author -- ServiceTitan API Documentation: https://developer.servicetitan.io/docs -- ServiceTitan Developer Portal: https://developer.servicetitan.io -- MCP Protocol: https://modelcontextprotocol.io +MCP Engine - BusyBee3333 + +## Links + +- [ServiceTitan API Documentation](https://developer.servicetitan.io/) +- [MCP Specification](https://modelcontextprotocol.io/) +- [GitHub Repository](https://github.com/BusyBee3333/mcpengine) diff --git a/servers/servicetitan/src/tools/dispatch-tools.ts b/servers/servicetitan/src/tools/dispatch-tools.ts deleted file mode 100644 index 0e270d4..0000000 --- a/servers/servicetitan/src/tools/dispatch-tools.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Tool } from '@modelcontextprotocol/sdk/types.js'; -import { ServiceTitanClient } from '../clients/servicetitan.js'; -import type { DispatchZone, DispatchBoard } from '../types/index.js'; - -export function createDispatchTools(client: ServiceTitanClient): Tool[] { - return [ - { - name: 'servicetitan_list_dispatch_zones', - description: 'List all dispatch zones', - inputSchema: { - type: 'object', - properties: { - active: { type: 'boolean', description: 'Filter by active status' }, - businessUnitId: { type: 'number', description: 'Filter by business unit' }, - }, - }, - }, - { - name: 'servicetitan_get_dispatch_board', - description: 'Get the dispatch board for a specific date and zone', - inputSchema: { - type: 'object', - properties: { - date: { type: 'string', description: 'Date (YYYY-MM-DD)' }, - zoneId: { type: 'number', description: 'Zone ID (optional)' }, - businessUnitId: { type: 'number', description: 'Business unit ID' }, - }, - required: ['date', 'businessUnitId'], - }, - }, - { - name: 'servicetitan_assign_technician', - description: 'Assign a technician to an appointment', - inputSchema: { - type: 'object', - properties: { - appointmentId: { type: 'number', description: 'Appointment ID' }, - technicianId: { type: 'number', description: 'Technician ID' }, - }, - required: ['appointmentId', 'technicianId'], - }, - }, - { - name: 'servicetitan_get_dispatch_capacity', - description: 'Get dispatch capacity for a date range', - inputSchema: { - type: 'object', - properties: { - startDate: { type: 'string', description: 'Start date (YYYY-MM-DD)' }, - endDate: { type: 'string', description: 'End date (YYYY-MM-DD)' }, - businessUnitId: { type: 'number', description: 'Business unit ID' }, - zoneId: { type: 'number', description: 'Zone ID (optional)' }, - }, - required: ['startDate', 'endDate', 'businessUnitId'], - }, - }, - ]; -} - -export async function handleDispatchTool( - client: ServiceTitanClient, - name: string, - args: any -): Promise { - switch (name) { - case 'servicetitan_list_dispatch_zones': - return await client.get('/settings/v2/tenant/{tenant}/zones', { - active: args.active, - businessUnitId: args.businessUnitId, - }); - - case 'servicetitan_get_dispatch_board': - return await client.get('/dispatch/v2/tenant/{tenant}/board', { - date: args.date, - zoneId: args.zoneId, - businessUnitId: args.businessUnitId, - }); - - case 'servicetitan_assign_technician': - return await client.patch( - `/jpm/v2/tenant/{tenant}/job-appointments/${args.appointmentId}/assign`, - { - technicianId: args.technicianId, - } - ); - - case 'servicetitan_get_dispatch_capacity': - return await client.get('/dispatch/v2/tenant/{tenant}/capacity', { - startDate: args.startDate, - endDate: args.endDate, - businessUnitId: args.businessUnitId, - zoneId: args.zoneId, - }); - - default: - throw new Error(`Unknown tool: ${name}`); - } -} diff --git a/servers/servicetitan/src/ui/react-app/equipment-tracker.tsx b/servers/servicetitan/src/ui/react-app/equipment-tracker.tsx new file mode 100644 index 0000000..2699f0c --- /dev/null +++ b/servers/servicetitan/src/ui/react-app/equipment-tracker.tsx @@ -0,0 +1,176 @@ +import React, { useState } from 'react'; + +interface Equipment { + id: number; + type: string; + brand: string; + model: string; + serialNumber: string; + customer: string; + location: string; + installDate: string; + lastService: string; + nextService: string; + warrantyExpires: string; + status: 'active' | 'needs-service' | 'warranty-expiring' | 'retired'; +} + +export default function EquipmentTracker() { + const [equipment] = useState([ + { id: 1, type: 'HVAC System', brand: 'Carrier', model: 'Infinity 20', serialNumber: 'CAR-2023-001', customer: 'John Smith', location: '123 Main St, Austin, TX', installDate: '2023-08-22', lastService: '2024-02-15', nextService: '2024-08-15', warrantyExpires: '2025-08-22', status: 'active' }, + { id: 2, type: 'Water Heater', brand: 'Rheem', model: 'Professional Classic', serialNumber: 'RHE-2022-045', customer: 'Sarah Johnson', location: '456 Oak Ave, Houston, TX', installDate: '2022-03-15', lastService: '2023-09-10', nextService: '2024-03-10', warrantyExpires: '2024-03-15', status: 'warranty-expiring' }, + { id: 3, type: 'Furnace', brand: 'Trane', model: 'XC95m', serialNumber: 'TRA-2023-112', customer: 'Mike Davis', location: '789 Pine St, Dallas, TX', installDate: '2023-11-05', lastService: '2024-01-20', nextService: '2024-05-05', warrantyExpires: '2026-11-05', status: 'active' }, + { id: 4, type: 'Heat Pump', brand: 'Lennox', model: 'XP25', serialNumber: 'LEN-2021-089', customer: 'Emily Wilson', location: '321 Elm St, Austin, TX', installDate: '2021-06-10', lastService: '2023-12-05', nextService: '2024-02-20', warrantyExpires: '2024-06-10', status: 'needs-service' }, + ]); + + const [filterStatus, setFilterStatus] = useState('all'); + const [searchTerm, setSearchTerm] = useState(''); + + const filteredEquipment = equipment.filter(item => { + if (filterStatus !== 'all' && item.status !== filterStatus) return false; + if (searchTerm && !item.customer.toLowerCase().includes(searchTerm.toLowerCase()) && !item.serialNumber.toLowerCase().includes(searchTerm.toLowerCase())) return false; + return true; + }); + + const getStatusColor = (status: string) => { + switch(status) { + case 'active': return 'bg-green-500/20 text-green-400 border-green-500/50'; + case 'needs-service': return 'bg-amber-500/20 text-amber-400 border-amber-500/50'; + case 'warranty-expiring': return 'bg-orange-500/20 text-orange-400 border-orange-500/50'; + case 'retired': return 'bg-gray-500/20 text-gray-400 border-gray-500/50'; + default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50'; + } + }; + + return ( +
+
+ {/* Header */} +
+

⚙️ Equipment Tracker

+

Monitor and manage customer equipment

+
+ + {/* Stats */} +
+
+
Total Equipment
+
{equipment.length}
+
+ +
+
Active
+
+ {equipment.filter(e => e.status === 'active').length} +
+
+ +
+
Needs Service
+
+ {equipment.filter(e => e.status === 'needs-service').length} +
+
+ +
+
Warranty Expiring
+
+ {equipment.filter(e => e.status === 'warranty-expiring').length} +
+
+
+ + {/* Filters */} +
+
+ setSearchTerm(e.target.value)} + className="bg-[#0f172a] border border-gray-600 rounded-lg px-4 py-2 text-white placeholder-gray-500 focus:outline-none focus:border-blue-500" + /> + + +
+
+ + {/* Equipment List */} +
+ {filteredEquipment.map((item) => ( +
+
+
+

{item.type}

+

{item.brand} {item.model}

+
+ + {item.status.replace('-', ' ')} + +
+ +
+
+

Equipment Info

+
+
+ Serial #: + {item.serialNumber} +
+
+ Installed: + {item.installDate} +
+
+
+ +
+

Customer

+
+
{item.customer}
+
{item.location}
+
+
+ +
+

Service Schedule

+
+
+ Last Service: + {item.lastService} +
+
+ Next Service: + {item.nextService} +
+
+ Warranty Until: + {item.warrantyExpires} +
+
+
+
+
+ ))} +
+ +
+ Showing {filteredEquipment.length} of {equipment.length} items +
+
+
+ ); +} diff --git a/servers/servicetitan/src/ui/react-app/membership-manager.tsx b/servers/servicetitan/src/ui/react-app/membership-manager.tsx new file mode 100644 index 0000000..bbe410e --- /dev/null +++ b/servers/servicetitan/src/ui/react-app/membership-manager.tsx @@ -0,0 +1,234 @@ +import React, { useState } from 'react'; + +interface Member { + id: number; + customer: string; + tier: 'Bronze' | 'Silver' | 'Gold' | 'Platinum'; + joinDate: string; + renewalDate: string; + monthlyFee: number; + benefits: string[]; + servicesUsed: number; + totalSavings: number; + status: 'active' | 'expiring-soon' | 'expired'; +} + +export default function MembershipManager() { + const [members] = useState([ + { + id: 1, + customer: 'John Smith', + tier: 'Gold', + joinDate: '2022-03-15', + renewalDate: '2024-03-15', + monthlyFee: 49.99, + benefits: ['Priority Service', '15% Discount', 'Annual Inspection'], + servicesUsed: 8, + totalSavings: 850, + status: 'active', + }, + { + id: 2, + customer: 'Sarah Johnson', + tier: 'Silver', + joinDate: '2023-01-10', + renewalDate: '2024-01-10', + monthlyFee: 34.99, + benefits: ['10% Discount', 'Seasonal Inspection'], + servicesUsed: 5, + totalSavings: 420, + status: 'active', + }, + { + id: 3, + customer: 'Mike Davis', + tier: 'Platinum', + joinDate: '2021-06-20', + renewalDate: '2024-06-20', + monthlyFee: 79.99, + benefits: ['VIP Service', '20% Discount', 'Quarterly Inspections', 'Free Parts'], + servicesUsed: 15, + totalSavings: 2100, + status: 'active', + }, + { + id: 4, + customer: 'Emily Wilson', + tier: 'Bronze', + joinDate: '2023-09-05', + renewalDate: '2024-02-28', + monthlyFee: 24.99, + benefits: ['5% Discount'], + servicesUsed: 3, + totalSavings: 150, + status: 'expiring-soon', + }, + ]); + + const [tierFilter, setTierFilter] = useState('all'); + + const stats = { + totalMembers: members.length, + activeMembers: members.filter(m => m.status === 'active').length, + expiringCount: members.filter(m => m.status === 'expiring-soon').length, + monthlyRevenue: members.reduce((sum, m) => sum + (m.status === 'active' ? m.monthlyFee : 0), 0), + }; + + const filteredMembers = members.filter(member => { + if (tierFilter !== 'all' && member.tier !== tierFilter) return false; + return true; + }); + + const getTierColor = (tier: string) => { + switch(tier) { + case 'Platinum': return 'bg-purple-500/20 text-purple-400 border-purple-500/50'; + case 'Gold': return 'bg-amber-500/20 text-amber-400 border-amber-500/50'; + case 'Silver': return 'bg-slate-400/20 text-slate-300 border-slate-400/50'; + case 'Bronze': return 'bg-orange-700/20 text-orange-400 border-orange-700/50'; + default: return 'bg-gray-500/20 text-gray-400 border-gray-500/50'; + } + }; + + const getStatusColor = (status: string) => { + switch(status) { + case 'active': return 'bg-green-500/20 text-green-400'; + case 'expiring-soon': return 'bg-amber-500/20 text-amber-400'; + case 'expired': return 'bg-red-500/20 text-red-400'; + default: return 'bg-gray-500/20 text-gray-400'; + } + }; + + return ( +
+
+ {/* Header */} +
+

💎 Membership Manager

+

Manage customer memberships and benefits

+
+ + {/* Stats */} +
+
+
Total Members
+
{stats.totalMembers}
+
↑ 12%
+
+ +
+
Active Members
+
{stats.activeMembers}
+
+ +
+
Expiring Soon
+
{stats.expiringCount}
+
+ +
+
Monthly Revenue
+
${stats.monthlyRevenue.toFixed(2)}
+
+
+ + {/* Filters */} +
+ +
+ + {/* Members List */} +
+ {filteredMembers.map((member) => ( +
+
+
+

{member.customer}

+
+ + {member.tier} + + + {member.status.replace('-', ' ')} + +
+
+
+
Monthly Fee
+
${member.monthlyFee}
+
+
+ +
+
+

Membership Info

+
+
+ Join Date: + {member.joinDate} +
+
+ Renewal: + {member.renewalDate} +
+
+
+ +
+

Usage & Savings

+
+
+ Services Used: + {member.servicesUsed} +
+
+ Total Savings: + ${member.totalSavings} +
+
+
+ +
+

Benefits

+
+ {member.benefits.map((benefit, idx) => ( + + {benefit} + + ))} +
+
+
+ +
+ + + {member.tier !== 'Platinum' && ( + + )} +
+
+ ))} +
+
+
+ ); +} diff --git a/servers/servicetitan/src/ui/react-app/revenue-dashboard.tsx b/servers/servicetitan/src/ui/react-app/revenue-dashboard.tsx new file mode 100644 index 0000000..848aa2c --- /dev/null +++ b/servers/servicetitan/src/ui/react-app/revenue-dashboard.tsx @@ -0,0 +1,149 @@ +import React, { useState } from 'react'; + +interface RevenueData { + period: string; + revenue: number; + jobs: number; + avgTicket: number; +} + +export default function RevenueDashboard() { + const [timeRange, setTimeRange] = useState<'week' | 'month' | 'year'>('month'); + + const [monthlyData] = useState([ + { period: 'Jan', revenue: 145000, jobs: 156, avgTicket: 929 }, + { period: 'Feb', revenue: 168000, jobs: 178, avgTicket: 944 }, + { period: 'Mar', revenue: 152000, jobs: 165, avgTicket: 921 }, + { period: 'Apr', revenue: 178000, jobs: 189, avgTicket: 942 }, + { period: 'May', revenue: 195000, jobs: 205, avgTicket: 951 }, + { period: 'Jun', revenue: 210000, jobs: 225, avgTicket: 933 }, + ]); + + const currentMonth = monthlyData[monthlyData.length - 1]; + const lastMonth = monthlyData[monthlyData.length - 2]; + const revenueGrowth = ((currentMonth.revenue - lastMonth.revenue) / lastMonth.revenue * 100).toFixed(1); + const totalRevenue = monthlyData.reduce((sum, d) => sum + d.revenue, 0); + const totalJobs = monthlyData.reduce((sum, d) => sum + d.jobs, 0); + const avgTicket = totalRevenue / totalJobs; + + const maxRevenue = Math.max(...monthlyData.map(d => d.revenue)); + + return ( +
+
+ {/* Header */} +
+
+
+

💵 Revenue Dashboard

+

Track financial performance and trends

+
+
+ {(['week', 'month', 'year'] as const).map((range) => ( + + ))} +
+
+
+ + {/* Stats */} +
+
+
Total Revenue (YTD)
+
${totalRevenue.toLocaleString()}
+
↑ {revenueGrowth}% vs last month
+
+ +
+
This Month
+
${currentMonth.revenue.toLocaleString()}
+
↑ ${(currentMonth.revenue - lastMonth.revenue).toLocaleString()}
+
+ +
+
Total Jobs
+
{totalJobs}
+
↑ 8%
+
+ +
+
Avg Ticket
+
${Math.round(avgTicket)}
+
↑ 2%
+
+
+ + {/* Revenue Chart */} +
+

Revenue Trend

+ +
+ {monthlyData.map((data, idx) => { + const height = (data.revenue / maxRevenue) * 100; + return ( +
+
+
+
+ ${data.revenue.toLocaleString()} +
+
+
+
{data.period}
+
+ ); + })} +
+
+ + {/* Breakdown Table */} +
+ + + + + + + + + + + + {monthlyData.map((data, idx) => { + const prevData = idx > 0 ? monthlyData[idx - 1] : null; + const growth = prevData ? ((data.revenue - prevData.revenue) / prevData.revenue * 100).toFixed(1) : '0.0'; + + return ( + + + + + + + + ); + })} + +
PeriodRevenueJobsAvg TicketGrowth
{data.period} + ${data.revenue.toLocaleString()} + {data.jobs}${data.avgTicket} + = 0 ? 'text-green-400' : 'text-red-400'}> + {parseFloat(growth) >= 0 ? '↑' : '↓'} {Math.abs(parseFloat(growth))}% + +
+
+
+
+ ); +} diff --git a/servers/servicetitan/src/ui/react-app/technician-detail.tsx b/servers/servicetitan/src/ui/react-app/technician-detail.tsx new file mode 100644 index 0000000..398e13b --- /dev/null +++ b/servers/servicetitan/src/ui/react-app/technician-detail.tsx @@ -0,0 +1,252 @@ +import React, { useState } from 'react'; + +interface TechDetail { + id: number; + name: string; + email: string; + phone: string; + status: string; + certifications: string[]; + specialties: string[]; + stats: { + jobsCompleted: number; + avgRating: number; + totalRevenue: number; + efficiency: number; + }; + schedule: Array<{ + date: string; + jobs: Array<{ + jobNumber: string; + customer: string; + timeWindow: string; + status: string; + }>; + }>; +} + +export default function TechnicianDetail() { + const [tech] = useState({ + id: 1, + name: 'Mike Johnson', + email: 'mike.johnson@company.com', + phone: '(555) 987-6543', + status: 'on-job', + certifications: ['HVAC Certified', 'EPA 608 Universal', 'NATE Certified'], + specialties: ['HVAC Systems', 'Heat Pumps', 'Air Quality'], + stats: { + jobsCompleted: 342, + avgRating: 4.8, + totalRevenue: 145600, + efficiency: 92, + }, + schedule: [ + { + date: '2024-02-15', + jobs: [ + { jobNumber: 'J-2024-001', customer: 'John Smith', timeWindow: '9:00 AM - 11:00 AM', status: 'in-progress' }, + { jobNumber: 'J-2024-004', customer: 'Emily Wilson', timeWindow: '1:00 PM - 3:00 PM', status: 'scheduled' }, + { jobNumber: 'J-2024-007', customer: 'James Martinez', timeWindow: '3:30 PM - 5:30 PM', status: 'scheduled' }, + ], + }, + { + date: '2024-02-16', + jobs: [ + { jobNumber: 'J-2024-012', customer: 'Linda Davis', timeWindow: '8:00 AM - 10:00 AM', status: 'scheduled' }, + { jobNumber: 'J-2024-015', customer: 'Robert Taylor', timeWindow: '11:00 AM - 1:00 PM', status: 'scheduled' }, + ], + }, + ], + }); + + const [activeTab, setActiveTab] = useState<'overview' | 'schedule' | 'performance'>('overview'); + + const getStatusColor = (status: string) => { + switch(status) { + case 'scheduled': return 'bg-blue-500/20 text-blue-400'; + case 'in-progress': return 'bg-amber-500/20 text-amber-400'; + case 'completed': return 'bg-green-500/20 text-green-400'; + default: return 'bg-gray-500/20 text-gray-400'; + } + }; + + return ( +
+
+ {/* Header */} +
+
+
+

{tech.name}

+

Technician #{tech.id}

+
+
+ + +
+
+ + + {tech.status.replace('-', ' ')} + +
+ + {/* Stats */} +
+
+
Jobs Completed
+
{tech.stats.jobsCompleted}
+
+ +
+
Avg Rating
+
{tech.stats.avgRating} ⭐
+
+ +
+
Total Revenue
+
${tech.stats.totalRevenue.toLocaleString()}
+
+ +
+
Efficiency
+
{tech.stats.efficiency}%
+
+
+ + {/* Tabs */} +
+
+ {(['overview', 'schedule', 'performance'] as const).map((tab) => ( + + ))} +
+
+ + {/* Overview Tab */} + {activeTab === 'overview' && ( +
+
+

Contact Information

+
+
+
Email
+
{tech.email}
+
+
+
Phone
+
{tech.phone}
+
+
+
+ +
+

Certifications

+
+ {tech.certifications.map((cert, idx) => ( + + {cert} + + ))} +
+
+ +
+

Specialties

+
+ {tech.specialties.map((specialty, idx) => ( + + {specialty} + + ))} +
+
+
+ )} + + {/* Schedule Tab */} + {activeTab === 'schedule' && ( +
+ {tech.schedule.map((day, idx) => ( +
+

{day.date}

+
+ {day.jobs.map((job, jIdx) => ( +
+
+
+
{job.jobNumber}
+
{job.customer}
+
⏰ {job.timeWindow}
+
+ + {job.status} + +
+
+ ))} +
+
+ ))} +
+ )} + + {/* Performance Tab */} + {activeTab === 'performance' && ( +
+

Performance Metrics

+
+
+
+ Job Completion Rate + 96% +
+
+
+
+
+ +
+
+ Customer Satisfaction + 94% +
+
+
+
+
+ +
+
+ On-Time Arrival + 89% +
+
+
+
+
+
+
+ )} +
+
+ ); +} diff --git a/servers/servicetitan/src/ui/react-app/vite.config.ts b/servers/servicetitan/src/ui/react-app/vite.config.ts new file mode 100644 index 0000000..e6cd6cd --- /dev/null +++ b/servers/servicetitan/src/ui/react-app/vite.config.ts @@ -0,0 +1,25 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'path'; +import fs from 'fs'; + +// Get all app directories +const appsDir = path.resolve(__dirname, 'src/apps'); +const apps = fs.readdirSync(appsDir).filter(file => { + return fs.statSync(path.join(appsDir, file)).isDirectory(); +}); + +// Build configuration for all apps +export default defineConfig({ + plugins: [react()], + build: { + rollupOptions: { + input: apps.reduce((acc, app) => { + acc[app] = path.resolve(appsDir, app, 'index.html'); + return acc; + }, {} as Record), + }, + outDir: path.resolve(__dirname, '../../dist/apps'), + emptyOutDir: true, + }, +});