diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard.ts b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard.ts deleted file mode 100644 index 22e5c4e..0000000 --- a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard.ts +++ /dev/null @@ -1,119 +0,0 @@ -export default ` - - - - - Appointment Dashboard - - - - - - -
- - -`; diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/App.tsx b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/App.tsx new file mode 100644 index 0000000..0fbc445 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/App.tsx @@ -0,0 +1,180 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface Appointment { + id: number; + firstName: string; + lastName: string; + datetime: string; + type: string; + appointmentTypeID: number; + status: 'confirmed' | 'pending' | 'canceled'; + calendarID: number; +} + +interface DashboardStats { + today: number; + thisWeek: number; + thisMonth: number; + total: number; +} + +export default function AppointmentDashboard() { + const [stats, setStats] = useState({ today: 0, thisWeek: 0, thisMonth: 0, total: 0 }); + const [upcomingAppointments, setUpcomingAppointments] = useState([]); + const [loading, setLoading] = useState(true); + const [dateRange, setDateRange] = useState<'today' | 'week' | 'month'>('today'); + + useEffect(() => { + fetchDashboardData(); + }, [dateRange]); + + const fetchDashboardData = async () => { + setLoading(true); + try { + // In production, this would call MCP tools: + // const appointments = await window.mcp.call('acuity_list_appointments', { minDate, maxDate }); + + // Mock data for demo + const mockAppointments: Appointment[] = [ + { id: 1, firstName: 'John', lastName: 'Doe', datetime: '2024-02-15T10:00:00', type: 'Initial Consultation', appointmentTypeID: 1, status: 'confirmed', calendarID: 1 }, + { id: 2, firstName: 'Jane', lastName: 'Smith', datetime: '2024-02-15T14:30:00', type: 'Follow-up Session', appointmentTypeID: 2, status: 'confirmed', calendarID: 1 }, + { id: 3, firstName: 'Bob', lastName: 'Johnson', datetime: '2024-02-16T09:00:00', type: 'Assessment', appointmentTypeID: 3, status: 'pending', calendarID: 2 }, + { id: 4, firstName: 'Alice', lastName: 'Williams', datetime: '2024-02-16T11:30:00', type: 'Check-in', appointmentTypeID: 1, status: 'confirmed', calendarID: 1 }, + ]; + + setUpcomingAppointments(mockAppointments); + setStats({ today: 2, thisWeek: 8, thisMonth: 32, total: 156 }); + } catch (error) { + console.error('Error fetching dashboard data:', error); + } finally { + setLoading(false); + } + }; + + const formatDateTime = (datetime: string) => { + const date = new Date(datetime); + return date.toLocaleString('en-US', { + month: 'short', + day: 'numeric', + hour: 'numeric', + minute: '2-digit', + hour12: true + }); + }; + + const getStatusColor = (status: string) => { + switch (status) { + case 'confirmed': return '#10b981'; + case 'pending': return '#f59e0b'; + case 'canceled': return '#ef4444'; + default: return '#6b7280'; + } + }; + + if (loading) { + return ( +
+
+

Loading dashboard...

+
+ ); + } + + return ( +
+
+

Appointment Dashboard

+
+ + + +
+
+ +
+
+
๐Ÿ“…
+
+

Today

+
{stats.today}
+
+
+
+
๐Ÿ“†
+
+

This Week

+
{stats.thisWeek}
+
+
+
+
๐Ÿ—“๏ธ
+
+

This Month

+
{stats.thisMonth}
+
+
+
+
๐Ÿ“Š
+
+

Total

+
{stats.total}
+
+
+
+ +
+
+

Upcoming Appointments

+ +
+ +
+ {upcomingAppointments.length === 0 ? ( +
+

No upcoming appointments

+
+ ) : ( + upcomingAppointments.map(apt => ( +
+
+

{apt.firstName} {apt.lastName}

+

+ ๐Ÿ• {formatDateTime(apt.datetime)} + โ€ข {apt.type} +

+
+
+ + {apt.status.charAt(0).toUpperCase() + apt.status.slice(1)} + + +
+
+ )) + )} +
+
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/index.html b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/index.html new file mode 100644 index 0000000..22a2fcb --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/index.html @@ -0,0 +1,12 @@ + + + + + + Appointment Dashboard - Acuity Scheduling MCP + + +
+ + + 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 new file mode 100644 index 0000000..9707d82 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/main.tsx @@ -0,0 +1,9 @@ +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/appointment-dashboard/package.json b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/package.json new file mode 100644 index 0000000..838e5ea --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/package.json @@ -0,0 +1,21 @@ +{ + "name": "appointment-dashboard", + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "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/appointment-dashboard/styles.css b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/styles.css new file mode 100644 index 0000000..72d4da1 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/styles.css @@ -0,0 +1,246 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Helvetica', 'Arial', sans-serif; + background: #0f172a; + color: #e2e8f0; + line-height: 1.6; +} + +.loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 100vh; + gap: 1rem; +} + +.spinner { + width: 40px; + height: 40px; + border: 3px solid #1e293b; + border-top-color: #3b82f6; + border-radius: 50%; + animation: spin 0.8s linear infinite; +} + +@keyframes spin { + to { transform: rotate(360deg); } +} + +.dashboard { + max-width: 1400px; + margin: 0 auto; + padding: 2rem; +} + +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 2rem; + flex-wrap: wrap; + gap: 1rem; +} + +.dashboard-header h1 { + font-size: 2rem; + font-weight: 700; + color: #f1f5f9; +} + +.date-range-selector { + display: flex; + gap: 0.5rem; + background: #1e293b; + padding: 0.25rem; + border-radius: 0.5rem; +} + +.date-range-selector button { + padding: 0.5rem 1rem; + background: transparent; + color: #94a3b8; + border: none; + border-radius: 0.375rem; + cursor: pointer; + font-weight: 500; + transition: all 0.2s; +} + +.date-range-selector button:hover { + background: #334155; + color: #e2e8f0; +} + +.date-range-selector button.active { + background: #3b82f6; + color: white; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #1e293b; + padding: 1.5rem; + border-radius: 0.75rem; + border: 1px solid #334155; + display: flex; + align-items: center; + gap: 1rem; + transition: transform 0.2s, box-shadow 0.2s; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +.stat-icon { + font-size: 2.5rem; +} + +.stat-content h3 { + color: #94a3b8; + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.25rem; +} + +.stat-value { + font-size: 2rem; + font-weight: 700; + color: #3b82f6; +} + +.appointments-section { + background: #1e293b; + border-radius: 0.75rem; + border: 1px solid #334155; + padding: 1.5rem; +} + +.section-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.section-header h2 { + font-size: 1.5rem; + font-weight: 600; + color: #f1f5f9; +} + +.refresh-btn { + padding: 0.5rem 1rem; + background: #334155; + color: #e2e8f0; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-weight: 500; + transition: background 0.2s; +} + +.refresh-btn:hover { + background: #475569; +} + +.appointments-list { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.appointment-card { + background: #0f172a; + padding: 1.25rem; + border-radius: 0.5rem; + border: 1px solid #334155; + display: flex; + justify-content: space-between; + align-items: center; + transition: border-color 0.2s; +} + +.appointment-card:hover { + border-color: #3b82f6; +} + +.appointment-info h4 { + font-size: 1.125rem; + color: #f1f5f9; + margin-bottom: 0.5rem; +} + +.appointment-meta { + display: flex; + gap: 0.5rem; + font-size: 0.875rem; + color: #94a3b8; + flex-wrap: wrap; +} + +.appointment-actions { + display: flex; + align-items: center; + gap: 1rem; +} + +.status-badge { + padding: 0.375rem 0.75rem; + border-radius: 1rem; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; +} + +.view-btn { + padding: 0.5rem 1rem; + background: #3b82f6; + color: white; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-weight: 500; + transition: background 0.2s; +} + +.view-btn:hover { + background: #2563eb; +} + +.empty-state { + text-align: center; + padding: 3rem; + color: #64748b; +} + +@media (max-width: 768px) { + .dashboard { + padding: 1rem; + } + + .appointment-card { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .appointment-actions { + width: 100%; + justify-content: space-between; + } +} diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/tsconfig.json b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/tsconfig.json new file mode 100644 index 0000000..3934b8f --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"], + "references": [{ "path": "./tsconfig.node.json" }] +} diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/vite.config.ts new file mode 100644 index 0000000..ee0cd13 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-dashboard/vite.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + open: true, + }, + build: { + outDir: 'dist', + sourcemap: true, + }, +}); diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-detail.ts b/servers/acuity-scheduling/src/ui/react-app/appointment-detail.ts deleted file mode 100644 index 8035782..0000000 --- a/servers/acuity-scheduling/src/ui/react-app/appointment-detail.ts +++ /dev/null @@ -1,151 +0,0 @@ -export default ` - - - - - Appointment Detail - - - - - - -
- - -`; diff --git a/servers/acuity-scheduling/src/ui/react-app/appointment-detail/App.tsx b/servers/acuity-scheduling/src/ui/react-app/appointment-detail/App.tsx new file mode 100644 index 0000000..16f9819 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/appointment-detail/App.tsx @@ -0,0 +1,282 @@ +import React, { useState, useEffect } from 'react'; +import './styles.css'; + +interface AppointmentDetail { + id: number; + firstName: string; + lastName: string; + email: string; + phone: string; + datetime: string; + endTime: string; + type: string; + appointmentTypeID: number; + calendarID: number; + calendar: string; + status: 'confirmed' | 'pending' | 'canceled'; + notes: string; + price: string; + paid: string; + amountPaid: string; + certificate: string; + confirmationPage: string; + formsText: string; + labels: Array<{ id: number; name: string; color: string }>; +} + +export default function AppointmentDetail() { + const [appointment, setAppointment] = useState(null); + const [loading, setLoading] = useState(true); + const [editing, setEditing] = useState(false); + const [formData, setFormData] = useState({ firstName: '', lastName: '', email: '', phone: '', notes: '' }); + + useEffect(() => { + fetchAppointmentDetail(); + }, []); + + const fetchAppointmentDetail = async () => { + setLoading(true); + try { + // In production: const appointment = await window.mcp.call('acuity_get_appointment', { id: appointmentId }); + + // Mock data + const mockAppointment: AppointmentDetail = { + id: 12345, + firstName: 'John', + lastName: 'Doe', + email: 'john.doe@example.com', + phone: '(555) 123-4567', + datetime: '2024-02-15T10:00:00', + endTime: '2024-02-15T11:00:00', + type: 'Initial Consultation', + appointmentTypeID: 1, + calendarID: 1, + calendar: 'Main Calendar', + status: 'confirmed', + notes: 'Client prefers morning appointments. First-time visitor.', + price: '$150.00', + paid: 'yes', + amountPaid: '$150.00', + certificate: '', + confirmationPage: 'https://acuityscheduling.com/confirmation', + formsText: 'Intake form completed', + labels: [ + { id: 1, name: 'VIP', color: '#fbbf24' }, + { id: 2, name: 'New Client', color: '#3b82f6' } + ] + }; + + setAppointment(mockAppointment); + setFormData({ + firstName: mockAppointment.firstName, + lastName: mockAppointment.lastName, + email: mockAppointment.email, + phone: mockAppointment.phone, + notes: mockAppointment.notes + }); + } catch (error) { + console.error('Error fetching appointment:', error); + } finally { + setLoading(false); + } + }; + + const handleUpdate = async () => { + if (!appointment) return; + + try { + // In production: await window.mcp.call('acuity_update_appointment', { id: appointment.id, ...formData }); + console.log('Updating appointment:', formData); + setEditing(false); + await fetchAppointmentDetail(); + } catch (error) { + console.error('Error updating appointment:', error); + } + }; + + const handleCancel = async () => { + if (!appointment || !confirm('Are you sure you want to cancel this appointment?')) return; + + try { + // In production: await window.mcp.call('acuity_cancel_appointment', { id: appointment.id }); + console.log('Canceling appointment:', appointment.id); + await fetchAppointmentDetail(); + } catch (error) { + console.error('Error canceling appointment:', error); + } + }; + + const handleReschedule = async () => { + if (!appointment) return; + const newDatetime = prompt('Enter new datetime (ISO 8601 format):'); + if (!newDatetime) return; + + try { + // In production: await window.mcp.call('acuity_reschedule_appointment', { id: appointment.id, datetime: newDatetime }); + console.log('Rescheduling to:', newDatetime); + await fetchAppointmentDetail(); + } catch (error) { + console.error('Error rescheduling appointment:', error); + } + }; + + if (loading) { + return ( +
+
+

Loading appointment details...

+
+ ); + } + + if (!appointment) { + return ( +
+

Appointment not found

+
+ ); + } + + return ( +
+
+
+

Appointment #{appointment.id}

+ + {appointment.status.charAt(0).toUpperCase() + appointment.status.slice(1)} + +
+
+ {!editing && } + + +
+
+ +
+
+

Client Information

+ {editing ? ( +
+
+ + setFormData({...formData, firstName: e.target.value})} + /> +
+
+ + setFormData({...formData, lastName: e.target.value})} + /> +
+
+ + setFormData({...formData, email: e.target.value})} + /> +
+
+ + setFormData({...formData, phone: e.target.value})} + /> +
+
+ + +
+
+ ) : ( +
+
+ Name: + {appointment.firstName} {appointment.lastName} +
+
+ Email: + {appointment.email} +
+
+ Phone: + {appointment.phone} +
+
+ )} +
+ +
+

Appointment Details

+
+
+ Type: + {appointment.type} +
+
+ Calendar: + {appointment.calendar} +
+
+ Date & Time: + {new Date(appointment.datetime).toLocaleString()} +
+
+ Duration: + 60 minutes +
+
+
+ +
+

Payment

+
+
+ Price: + {appointment.price} +
+
+ Paid: + {appointment.paid === 'yes' ? 'โœ… Yes' : 'โŒ No'} +
+
+ Amount Paid: + {appointment.amountPaid} +
+
+
+ +
+

Labels

+
+ {appointment.labels.map(label => ( + + {label.name} + + ))} +
+
+ +
+

Notes

+ {editing ? ( +