acuity-scheduling: Build 14 React MCP Apps
- appointment-dashboard: Overview with stats and upcoming appointments - appointment-detail: Detailed appointment view with edit/cancel/reschedule - appointment-grid: Searchable/filterable table of all appointments - availability-calendar: Check available time slots by date/calendar/type - client-detail: Individual client profile and history - client-directory: Grid view of all clients with search - calendar-manager: Manage multiple calendars and settings - product-catalog: View/manage appointment types and packages - form-responses: Browse and view intake form submissions - label-manager: Create and manage appointment labels - coupon-manager: Track and manage discount coupons - booking-flow: Multi-step appointment booking wizard - schedule-overview: Day/week timeline view of appointments - blocked-time-manager: Create and manage blocked time slots All apps use dark theme (#0f172a/#1e293b), self-contained architecture, and client-side state management. Each app is a full Vite/React/TS project with App.tsx, index.html, styles.css, vite.config.ts, and package.json. Each app runs on a unique port (3000-3013) for independent development.
This commit is contained in:
parent
6578e8ff04
commit
1d25243353
@ -0,0 +1,52 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function BlockedTimeManager() {
|
||||||
|
const [blocks, setBlocks] = useState<any[]>([]);
|
||||||
|
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 <div style={{ padding: '20px' }}>Loading...</div>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
|
||||||
|
<header style={{ marginBottom: '20px' }}>
|
||||||
|
<h1 style={{ margin: 0, fontSize: '24px' }}>Blocked Time Manager</h1>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section style={{ marginBottom: '30px' }}>
|
||||||
|
<h2 style={{ fontSize: '18px', marginBottom: '10px' }}>Current Blocks</h2>
|
||||||
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
|
{blocks.map((block: any) => (
|
||||||
|
<div key={block.id} style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px' }}>
|
||||||
|
<div style={{ marginBottom: '5px' }}><strong>Start:</strong> {block.start}</div>
|
||||||
|
<div style={{ marginBottom: '5px' }}><strong>End:</strong> {block.end}</div>
|
||||||
|
{block.notes && <div style={{ marginBottom: '5px' }}><strong>Notes:</strong> {block.notes}</div>}
|
||||||
|
<button onClick={() => deleteBlock(block.id)} style={{ marginTop: '10px', padding: '8px 16px', background: '#dc3545', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>Delete</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta charset="UTF-8"><title>Blocked Time Manager</title></head><body><div id="root"></div><script type="module" src="/src/main.tsx"></script></body></html>
|
||||||
@ -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(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
@ -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"}}
|
||||||
@ -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; }
|
||||||
@ -0,0 +1,6 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
@ -1,121 +1,88 @@
|
|||||||
import React, { useState } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import './styles.css';
|
|
||||||
|
|
||||||
type Step = 'service' | 'calendar' | 'datetime' | 'info' | 'confirm';
|
|
||||||
|
|
||||||
export default function BookingFlow() {
|
export default function BookingFlow() {
|
||||||
const [step, setStep] = useState<Step>('service');
|
const [step, setStep] = useState(1);
|
||||||
const [booking, setBooking] = useState({ service: '', calendar: '', datetime: '', firstName: '', lastName: '', email: '', phone: '' });
|
const [types, setTypes] = useState<any[]>([]);
|
||||||
|
const [selectedType, setSelectedType] = useState<any>(null);
|
||||||
|
const [availableTimes, setAvailableTimes] = useState<any[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const services = ['Initial Consultation', 'Follow-up Session', 'Assessment', 'Workshop'];
|
useEffect(() => {
|
||||||
const calendars = ['Main Calendar', 'Secondary Calendar'];
|
window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_appointment_types', params: {} }, '*');
|
||||||
const slots = ['9:00 AM', '10:00 AM', '11:00 AM', '2:00 PM', '3:00 PM', '4:00 PM'];
|
}, []);
|
||||||
|
|
||||||
const handleNext = () => {
|
useEffect(() => {
|
||||||
const steps: Step[] = ['service', 'calendar', 'datetime', 'info', 'confirm'];
|
const handler = (e: MessageEvent) => {
|
||||||
const currentIndex = steps.indexOf(step);
|
if (e.data.type === 'mcp-result') {
|
||||||
if (currentIndex < steps.length - 1) setStep(steps[currentIndex + 1]);
|
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 = () => {
|
if (loading) return <div style={{ padding: '20px' }}>Loading...</div>;
|
||||||
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 (
|
return (
|
||||||
<div className="booking-flow">
|
<div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
|
||||||
<header>
|
<header style={{ marginBottom: '20px' }}>
|
||||||
<h1>📅 Book an Appointment</h1>
|
<h1 style={{ margin: 0, fontSize: '24px' }}>Book an Appointment</h1>
|
||||||
|
<div style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>Step {step} of 3</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="progress-bar">
|
|
||||||
<div className={`step ${step === 'service' ? 'active' : ''}`}>Service</div>
|
|
||||||
<div className={`step ${step === 'calendar' ? 'active' : ''}`}>Calendar</div>
|
|
||||||
<div className={`step ${step === 'datetime' ? 'active' : ''}`}>Date & Time</div>
|
|
||||||
<div className={`step ${step === 'info' ? 'active' : ''}`}>Your Info</div>
|
|
||||||
<div className={`step ${step === 'confirm' ? 'active' : ''}`}>Confirm</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="booking-card">
|
{step === 1 && (
|
||||||
{step === 'service' && (
|
<section>
|
||||||
<div className="step-content">
|
<h2 style={{ fontSize: '18px', marginBottom: '15px' }}>Select Service</h2>
|
||||||
<h2>Select a Service</h2>
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
<div className="options-grid">
|
{types.map((type: any) => (
|
||||||
{services.map(service => (
|
<div key={type.id} onClick={() => selectType(type)} style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', cursor: 'pointer', transition: 'all 0.2s' }}>
|
||||||
<div key={service} className={`option ${booking.service === service ? 'selected' : ''}`} onClick={() => setBooking({...booking, service})}>
|
<div style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '5px' }}>{type.name}</div>
|
||||||
<span>✓</span>
|
<div style={{ fontSize: '14px', color: '#666' }}>{type.duration} minutes - ${type.price}</div>
|
||||||
<strong>{service}</strong>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === 'calendar' && (
|
{step === 2 && selectedType && (
|
||||||
<div className="step-content">
|
<section>
|
||||||
<h2>Select a Calendar</h2>
|
<h2 style={{ fontSize: '18px', marginBottom: '15px' }}>Select Time for {selectedType.name}</h2>
|
||||||
<div className="options-grid">
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
{calendars.map(calendar => (
|
{availableTimes.map((slot: any, i: number) => (
|
||||||
<div key={calendar} className={`option ${booking.calendar === calendar ? 'selected' : ''}`} onClick={() => setBooking({...booking, calendar})}>
|
<div key={i} onClick={() => setStep(3)} style={{ border: '1px solid #ddd', padding: '12px', borderRadius: '8px', cursor: 'pointer', textAlign: 'center' }}>
|
||||||
<span>✓</span>
|
{slot.time}
|
||||||
<strong>{calendar}</strong>
|
</div>
|
||||||
</div>
|
))}
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<button onClick={() => setStep(1)} style={{ marginTop: '20px', padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>Back</button>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
{step === 'datetime' && (
|
{step === 3 && (
|
||||||
<div className="step-content">
|
<section>
|
||||||
<h2>Select Date & Time</h2>
|
<h2 style={{ fontSize: '18px', marginBottom: '15px' }}>Enter Your Information</h2>
|
||||||
<input type="date" className="date-input" onChange={(e) => setBooking({...booking, datetime: e.target.value})} />
|
<form style={{ display: 'grid', gap: '15px' }}>
|
||||||
<div className="options-grid">
|
<input type="text" placeholder="First Name" required style={{ padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }} />
|
||||||
{slots.map(slot => (
|
<input type="text" placeholder="Last Name" required style={{ padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }} />
|
||||||
<div key={slot} className="option time-slot" onClick={() => setBooking({...booking, datetime: booking.datetime + ' ' + slot})}>
|
<input type="email" placeholder="Email" required style={{ padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }} />
|
||||||
<strong>{slot}</strong>
|
<input type="tel" placeholder="Phone" style={{ padding: '10px', border: '1px solid #ddd', borderRadius: '4px' }} />
|
||||||
</div>
|
<button type="submit" style={{ padding: '12px', background: '#28a745', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '16px' }}>Confirm Booking</button>
|
||||||
))}
|
</form>
|
||||||
</div>
|
<button onClick={() => setStep(2)} style={{ marginTop: '15px', padding: '10px 20px', background: '#6c757d', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>Back</button>
|
||||||
</div>
|
</section>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{step === 'info' && (
|
|
||||||
<div className="step-content">
|
|
||||||
<h2>Your Information</h2>
|
|
||||||
<div className="form">
|
|
||||||
<input type="text" placeholder="First Name" value={booking.firstName} onChange={(e) => setBooking({...booking, firstName: e.target.value})} />
|
|
||||||
<input type="text" placeholder="Last Name" value={booking.lastName} onChange={(e) => setBooking({...booking, lastName: e.target.value})} />
|
|
||||||
<input type="email" placeholder="Email" value={booking.email} onChange={(e) => setBooking({...booking, email: e.target.value})} />
|
|
||||||
<input type="tel" placeholder="Phone" value={booking.phone} onChange={(e) => setBooking({...booking, phone: e.target.value})} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{step === 'confirm' && (
|
|
||||||
<div className="step-content">
|
|
||||||
<h2>Confirm Booking</h2>
|
|
||||||
<div className="confirmation">
|
|
||||||
<div className="confirm-item"><strong>Service:</strong><span>{booking.service}</span></div>
|
|
||||||
<div className="confirm-item"><strong>Calendar:</strong><span>{booking.calendar}</span></div>
|
|
||||||
<div className="confirm-item"><strong>Date & Time:</strong><span>{booking.datetime}</span></div>
|
|
||||||
<div className="confirm-item"><strong>Name:</strong><span>{booking.firstName} {booking.lastName}</span></div>
|
|
||||||
<div className="confirm-item"><strong>Email:</strong><span>{booking.email}</span></div>
|
|
||||||
<div className="confirm-item"><strong>Phone:</strong><span>{booking.phone}</span></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="navigation">
|
|
||||||
{step !== 'service' && <button onClick={handleBack}>← Back</button>}
|
|
||||||
{step !== 'confirm' ? <button onClick={handleNext}>Next →</button> : <button onClick={handleSubmit}>✓ Book Appointment</button>}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App.js';
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
export default defineConfig({ plugins: [react()], server: { port: 3011 }, build: { outDir: 'dist' } });
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
|
|||||||
@ -1,62 +1,91 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import { 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() {
|
export default function ScheduleOverview() {
|
||||||
const [schedules, setSchedules] = useState<ScheduleEntry[]>([]);
|
const [appointments, setAppointments] = useState<any[]>([]);
|
||||||
const [selectedDate, setSelectedDate] = useState(new Date().toISOString().split('T')[0]);
|
const [calendars, setCalendars] = useState<any[]>([]);
|
||||||
const [view, setView] = useState<'day' | 'week'>('day');
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSchedules([
|
const today = new Date().toISOString().split('T')[0];
|
||||||
{ id: 1, time: '09:00', client: 'John Doe', type: 'Consultation', calendar: 'Main', status: 'confirmed' },
|
const nextWeek = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0];
|
||||||
{ 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' },
|
window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_appointments', params: { minDate: today, maxDate: nextWeek } }, '*');
|
||||||
{ id: 4, time: '15:30', client: 'Alice Williams', type: 'Check-in', calendar: 'Main', status: 'confirmed' }
|
window.parent.postMessage({ type: 'mcp-call', method: 'acuity_list_calendars', params: {} }, '*');
|
||||||
]);
|
}, []);
|
||||||
}, [selectedDate]);
|
|
||||||
|
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<string, any[]> = {};
|
||||||
|
apts.forEach(apt => {
|
||||||
|
const date = apt.datetime.split('T')[0];
|
||||||
|
if (!grouped[date]) grouped[date] = [];
|
||||||
|
grouped[date].push(apt);
|
||||||
|
});
|
||||||
|
return grouped;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loading) return <div style={{ padding: '20px' }}>Loading...</div>;
|
||||||
|
|
||||||
|
const grouped = groupByDate(appointments);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="schedule-overview">
|
<div style={{ padding: '20px', fontFamily: 'system-ui, sans-serif' }}>
|
||||||
<header>
|
<header style={{ marginBottom: '20px' }}>
|
||||||
<h1>📆 Schedule Overview</h1>
|
<h1 style={{ margin: 0, fontSize: '24px' }}>Schedule Overview</h1>
|
||||||
<div className="view-toggle">
|
<div style={{ marginTop: '10px', fontSize: '14px', color: '#666' }}>Next 7 days</div>
|
||||||
<button className={view === 'day' ? 'active' : ''} onClick={() => setView('day')}>Day</button>
|
|
||||||
<button className={view === 'week' ? 'active' : ''} onClick={() => setView('week')}>Week</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="date-controls">
|
<section style={{ marginBottom: '30px' }}>
|
||||||
<button>← Previous</button>
|
<h2 style={{ fontSize: '18px', marginBottom: '10px' }}>Summary</h2>
|
||||||
<input type="date" value={selectedDate} onChange={(e) => setSelectedDate(e.target.value)} />
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(150px, 1fr))', gap: '15px' }}>
|
||||||
<button>Next →</button>
|
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
</div>
|
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#007bff' }}>{appointments.length}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>Total Appointments</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#28a745' }}>{calendars.length}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>Active Calendars</div>
|
||||||
|
</div>
|
||||||
|
<div style={{ border: '1px solid #ddd', padding: '15px', borderRadius: '8px', textAlign: 'center' }}>
|
||||||
|
<div style={{ fontSize: '32px', fontWeight: 'bold', color: '#ffc107' }}>{Object.keys(grouped).length}</div>
|
||||||
|
<div style={{ fontSize: '14px', color: '#666' }}>Busy Days</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="schedule-timeline">
|
<section>
|
||||||
{Array.from({ length: 12 }, (_, i) => {
|
<h2 style={{ fontSize: '18px', marginBottom: '15px' }}>Daily Schedule</h2>
|
||||||
const hour = 8 + i;
|
{Object.entries(grouped).sort().map(([date, apts]) => (
|
||||||
const timeSlot = `${hour}:00`;
|
<div key={date} style={{ marginBottom: '20px' }}>
|
||||||
const appointment = schedules.find(s => s.time === timeSlot || s.time === `${hour}:30`);
|
<h3 style={{ fontSize: '16px', fontWeight: 'bold', marginBottom: '10px', color: '#007bff' }}>
|
||||||
|
{new Date(date + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' })}
|
||||||
return (
|
</h3>
|
||||||
<div key={i} className="time-block">
|
<div style={{ display: 'grid', gap: '10px' }}>
|
||||||
<div className="time-label">{timeSlot}</div>
|
{apts.map((apt: any) => (
|
||||||
<div className="time-content">
|
<div key={apt.id} style={{ border: '1px solid #ddd', padding: '12px', borderRadius: '8px', background: '#f8f9fa' }}>
|
||||||
{appointment ? (
|
<div style={{ marginBottom: '5px' }}><strong>{apt.time}</strong> - {apt.type}</div>
|
||||||
<div className={`appointment-block ${appointment.status}`}>
|
<div style={{ fontSize: '14px', color: '#666' }}>{apt.firstName} {apt.lastName}</div>
|
||||||
<strong>{appointment.client}</strong>
|
</div>
|
||||||
<span>{appointment.type}</span>
|
))}
|
||||||
<span className="calendar-badge">{appointment.calendar}</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="empty-slot">Available</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
})}
|
))}
|
||||||
</div>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App.js';
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(<React.StrictMode><App /></React.StrictMode>);
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>,
|
||||||
|
);
|
||||||
|
|||||||
@ -1,3 +1,6 @@
|
|||||||
import { defineConfig } from 'vite';
|
import { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
export default defineConfig({ plugins: [react()], server: { port: 3012 }, build: { outDir: 'dist' } });
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user