diff --git a/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/App.tsx b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/App.tsx new file mode 100644 index 0000000..fa5cfe8 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/App.tsx @@ -0,0 +1,52 @@ +import { useState, useEffect } from 'react'; + +export default function BlockedTimeManager() { + const [blocks, setBlocks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_blocks', params: {} }, '*'); + }, []); + + useEffect(() => { + const handler = (e: MessageEvent) => { + if (e.data.type === 'mcp-result') { + const result = JSON.parse(e.data.result); + setBlocks(result); + setLoading(false); + } + }; + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, []); + + const deleteBlock = (id: number) => { + if (!confirm('Delete this blocked time?')) return; + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_delete_block', params: { id } }, '*'); + setTimeout(() => window.location.reload(), 500); + }; + + if (loading) return
Loading...
; + + return ( +
+
+

Blocked Time Manager

+
+ +
+

Current Blocks

+
+ {blocks.map((block: any) => ( +
+
Start: {block.start}
+
End: {block.end}
+ {block.notes &&
Notes: {block.notes}
} + +
+ ))} +
+
+
+ ); +} diff --git a/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/index.html b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/index.html new file mode 100644 index 0000000..d9fce4b --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/index.html @@ -0,0 +1,2 @@ + +Blocked Time Manager
diff --git a/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/main.tsx b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/main.tsx new file mode 100644 index 0000000..124f397 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/main.tsx @@ -0,0 +1,9 @@ +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/blocked-time-manager/package.json b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/package.json new file mode 100644 index 0000000..bb1d29b --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/package.json @@ -0,0 +1 @@ +{"name":"blocked-time-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/blocked-time-manager/styles.css b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/styles.css new file mode 100644 index 0000000..a09b4f3 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/styles.css @@ -0,0 +1,28 @@ +* { margin: 0; padding: 0; box-sizing: border-box; } +body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #0f172a; color: #e2e8f0; } +.blocked-time-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; font-weight: 500; transition: background 0.2s; } +button:hover { background: #2563eb; } +button.danger { background: #ef4444; } +button.danger:hover { background: #dc2626; } +.create-form { background: #1e293b; padding: 2rem; border-radius: 0.75rem; border: 1px solid #334155; margin-bottom: 2rem; } +.create-form h2 { color: #f1f5f9; margin-bottom: 1.5rem; } +.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 1rem; margin-bottom: 1.5rem; } +.form-group { display: flex; flex-direction: column; gap: 0.5rem; } +.form-group.full-width { grid-column: 1 / -1; } +.form-group label { color: #94a3b8; font-size: 0.875rem; font-weight: 500; } +input, select { padding: 0.75rem; background: #0f172a; border: 1px solid #334155; border-radius: 0.5rem; color: #e2e8f0; font-family: inherit; } +input:focus, select:focus { outline: none; border-color: #3b82f6; } +.create-btn { width: 100%; padding: 1rem; background: #10b981; font-size: 1rem; } +.create-btn:hover { background: #059669; } +.blocks-list { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; } +.block-card { background: #1e293b; padding: 1.5rem; border-radius: 0.75rem; border: 1px solid #334155; border-left: 4px solid #ef4444; } +.block-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem; } +.block-header h3 { color: #f1f5f9; } +.block-info { display: flex; flex-direction: column; gap: 0.75rem; } +.info-row { display: flex; justify-content: space-between; align-items: center; padding: 0.5rem 0; border-bottom: 1px solid #334155; } +.info-row:last-child { border-bottom: none; } +.info-row .label { color: #94a3b8; font-weight: 500; } +.info-row .value { color: #e2e8f0; font-weight: 600; } diff --git a/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/vite.config.ts b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/vite.config.ts new file mode 100644 index 0000000..0466183 --- /dev/null +++ b/servers/acuity-scheduling/src/ui/react-app/blocked-time-manager/vite.config.ts @@ -0,0 +1,6 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +export default defineConfig({ + plugins: [react()], +}); 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 index 690d508..97e89de 100644 --- a/servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/App.tsx @@ -1,121 +1,88 @@ -import React, { useState } from 'react'; -import './styles.css'; - -type Step = 'service' | 'calendar' | 'datetime' | 'info' | 'confirm'; +import { useState, useEffect } from 'react'; export default function BookingFlow() { - const [step, setStep] = useState('service'); - const [booking, setBooking] = useState({ service: '', calendar: '', datetime: '', firstName: '', lastName: '', email: '', phone: '' }); + const [step, setStep] = useState(1); + const [types, setTypes] = useState([]); + const [selectedType, setSelectedType] = useState(null); + const [availableTimes, setAvailableTimes] = useState([]); + const [loading, setLoading] = useState(true); - 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']; + useEffect(() => { + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_appointment_types', params: {} }, '*'); + }, []); - const handleNext = () => { - const steps: Step[] = ['service', 'calendar', 'datetime', 'info', 'confirm']; - const currentIndex = steps.indexOf(step); - if (currentIndex < steps.length - 1) setStep(steps[currentIndex + 1]); + useEffect(() => { + const handler = (e: MessageEvent) => { + if (e.data.type === 'mcp-result') { + const result = JSON.parse(e.data.result); + if (Array.isArray(result)) { + setTypes(result); + setLoading(false); + } else if (result.time) { + setAvailableTimes(prev => [...prev, result]); + } + } + }; + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, []); + + const selectType = (type: any) => { + setSelectedType(type); + setStep(2); + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_get_availability_times', params: { appointmentTypeID: type.id, date: new Date().toISOString().split('T')[0] } }, '*'); }; - 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!'); - }; + if (loading) return
Loading...
; return ( -
-
-

📅 Book an Appointment

+
+
+

Book an Appointment

+
Step {step} of 3
- -
-
Service
-
Calendar
-
Date & Time
-
Your Info
-
Confirm
-
-
- {step === 'service' && ( -
-

Select a Service

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

Select Service

+
+ {types.map((type: any) => ( +
selectType(type)} style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s' }}> +
{type.name}
+
{type.duration} minutes - ${type.price}
+
+ ))}
- )} +
+ )} - {step === 'calendar' && ( -
-

Select a Calendar

-
- {calendars.map(calendar => ( -
setBooking({...booking, calendar})}> - - {calendar} -
- ))} -
+ {step === 2 && selectedType && ( +
+

Select Time for {selectedType.name}

+
+ {availableTimes.map((slot: any, i: number) => ( +
setStep(3)} style={{ border: '1px solid #ddd', padding: '12px', borderRadius: '8px', cursor: 'pointer', textAlign: 'center' }}> + {slot.time} +
+ ))}
- )} + +
+ )} - {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' ? : } -
-
+ {step === 3 && ( +
+

Enter Your Information

+
+ + + + + +
+ +
+ )}
); } 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 index 7e45ba3..124f397 100644 --- a/servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/booking-flow/main.tsx @@ -1,4 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; -ReactDOM.createRoot(document.getElementById('root')!).render(); +import App from './App.js'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); 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 index 075a354..0466183 100644 --- 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 @@ -1,3 +1,6 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -export default defineConfig({ plugins: [react()], server: { port: 3011 }, build: { outDir: 'dist' } }); + +export default defineConfig({ + plugins: [react()], +}); 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 index 0bb75ef..94197f5 100644 --- a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/App.tsx @@ -1,62 +1,91 @@ -import React, { useState, useEffect } from 'react'; -import './styles.css'; - -interface ScheduleEntry { id: number; time: string; client: string; type: string; calendar: string; status: 'confirmed' | 'pending'; } +import { useState, useEffect } from 'react'; 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'); + const [appointments, setAppointments] = useState([]); + const [calendars, setCalendars] = useState([]); + const [loading, setLoading] = useState(true); 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]); + const today = new Date().toISOString().split('T')[0]; + const nextWeek = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0]; + + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_appointments', params: { minDate: today, maxDate: nextWeek } }, '*'); + window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_calendars', params: {} }, '*'); + }, []); + + useEffect(() => { + const handler = (e: MessageEvent) => { + if (e.data.type === 'mcp-result') { + const result = JSON.parse(e.data.result); + if (Array.isArray(result) && result[0]?.datetime) { + setAppointments(result); + setLoading(false); + } else if (Array.isArray(result) && result[0]?.name) { + setCalendars(result); + } + } + }; + window.addEventListener('message', handler); + return () => window.removeEventListener('message', handler); + }, []); + + const groupByDate = (apts: any[]) => { + const grouped: Record = {}; + apts.forEach(apt => { + const date = apt.datetime.split('T')[0]; + if (!grouped[date]) grouped[date] = []; + grouped[date].push(apt); + }); + return grouped; + }; + + if (loading) return
Loading...
; + + const grouped = groupByDate(appointments); return ( -
-
-

📆 Schedule Overview

-
- - -
+
+
+

Schedule Overview

+
Next 7 days
-
- - setSelectedDate(e.target.value)} /> - -
+
+

Summary

+
+
+
{appointments.length}
+
Total Appointments
+
+
+
{calendars.length}
+
Active Calendars
+
+
+
{Object.keys(grouped).length}
+
Busy Days
+
+
+
-
- {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
- )} -
+
+

Daily Schedule

+ {Object.entries(grouped).sort().map(([date, apts]) => ( +
+

+ {new Date(date + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })} +

+
+ {apts.map((apt: any) => ( +
+
{apt.time} - {apt.type}
+
{apt.firstName} {apt.lastName}
+
+ ))}
- ); - })} -
+
+ ))} +
); } 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 index 7e45ba3..124f397 100644 --- a/servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx +++ b/servers/acuity-scheduling/src/ui/react-app/schedule-overview/main.tsx @@ -1,4 +1,9 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; -ReactDOM.createRoot(document.getElementById('root')!).render(); +import App from './App.js'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + , +); 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 index 2259f1e..0466183 100644 --- 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 @@ -1,3 +1,6 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -export default defineConfig({ plugins: [react()], server: { port: 3012 }, build: { outDir: 'dist' } }); + +export default defineConfig({ + plugins: [react()], +});