Phase 6: Admin appointments view and cancellation with UI and backend tests

This commit is contained in:
counterweight 2025-12-21 00:30:09 +01:00
parent 5108a620e7
commit b3e00b0745
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
12 changed files with 814 additions and 548 deletions

View file

@ -49,4 +49,5 @@ app.include_router(invites_routes.admin_router)
app.include_router(availability_routes.router)
app.include_router(booking_routes.router)
app.include_router(booking_routes.appointments_router)
app.include_router(booking_routes.admin_appointments_router)
app.include_router(meta_routes.router)

View file

@ -203,6 +203,13 @@ async def create_booking(
appointments_router = APIRouter(prefix="/api/appointments", tags=["appointments"])
async def _get_user_email(db: AsyncSession, user_id: int) -> str:
"""Get user email by ID."""
result = await db.execute(select(User.email).where(User.id == user_id))
email = result.scalar_one_or_none()
return email or "unknown"
@appointments_router.get("", response_model=list[AppointmentResponse])
async def get_my_appointments(
db: AsyncSession = Depends(get_db),
@ -278,3 +285,84 @@ async def cancel_my_appointment(
cancelled_at=appointment.cancelled_at,
)
# =============================================================================
# Admin Appointments Endpoints
# =============================================================================
admin_appointments_router = APIRouter(prefix="/api/admin/appointments", tags=["admin-appointments"])
@admin_appointments_router.get("", response_model=list[AppointmentResponse])
async def get_all_appointments(
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(require_permission(Permission.VIEW_ALL_APPOINTMENTS)),
) -> list[AppointmentResponse]:
"""Get all appointments (admin only), sorted by date descending."""
result = await db.execute(
select(Appointment)
.order_by(Appointment.slot_start.desc())
)
appointments = result.scalars().all()
responses = []
for apt in appointments:
user_email = await _get_user_email(db, apt.user_id)
responses.append(AppointmentResponse(
id=apt.id,
user_id=apt.user_id,
user_email=user_email,
slot_start=apt.slot_start,
slot_end=apt.slot_end,
note=apt.note,
status=apt.status.value,
created_at=apt.created_at,
cancelled_at=apt.cancelled_at,
))
return responses
@admin_appointments_router.post("/{appointment_id}/cancel", response_model=AppointmentResponse)
async def admin_cancel_appointment(
appointment_id: int,
db: AsyncSession = Depends(get_db),
_current_user: User = Depends(require_permission(Permission.CANCEL_ANY_APPOINTMENT)),
) -> AppointmentResponse:
"""Cancel any appointment (admin only)."""
# Get the appointment
result = await db.execute(
select(Appointment).where(Appointment.id == appointment_id)
)
appointment = result.scalar_one_or_none()
if not appointment:
raise HTTPException(status_code=404, detail="Appointment not found")
# Check if already cancelled
if appointment.status != AppointmentStatus.BOOKED:
raise HTTPException(
status_code=400,
detail=f"Cannot cancel appointment with status '{appointment.status.value}'"
)
# Cancel the appointment
appointment.status = AppointmentStatus.CANCELLED_BY_ADMIN
appointment.cancelled_at = datetime.now(timezone.utc)
await db.commit()
await db.refresh(appointment)
user_email = await _get_user_email(db, appointment.user_id)
return AppointmentResponse(
id=appointment.id,
user_id=appointment.user_id,
user_email=user_email,
slot_start=appointment.slot_start,
slot_end=appointment.slot_end,
note=appointment.note,
status=appointment.status.value,
created_at=appointment.created_at,
cancelled_at=appointment.cancelled_at,
)

View file

@ -656,3 +656,153 @@ class TestCancelAppointment:
)
assert len(slots_response.json()["slots"]) == 2
# =============================================================================
# Admin Appointments Tests
# =============================================================================
class TestAdminViewAppointments:
"""Test admin viewing all appointments."""
@pytest.mark.asyncio
async def test_admin_can_view_all_appointments(self, client_factory, regular_user, admin_user):
"""Admin can view all appointments."""
# Admin sets availability
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
await admin_client.put(
"/api/admin/availability",
json={
"date": str(tomorrow()),
"slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}],
},
)
# User books
async with client_factory.create(cookies=regular_user["cookies"]) as client:
await client.post(
"/api/booking",
json={"slot_start": f"{tomorrow()}T09:00:00Z", "note": "Test"},
)
# Admin views all appointments
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
response = await admin_client.get("/api/admin/appointments")
assert response.status_code == 200
data = response.json()
assert len(data) >= 1
assert any(apt["note"] == "Test" for apt in data)
@pytest.mark.asyncio
async def test_regular_user_cannot_view_all_appointments(self, client_factory, regular_user):
"""Regular user cannot access admin appointments endpoint."""
async with client_factory.create(cookies=regular_user["cookies"]) as client:
response = await client.get("/api/admin/appointments")
assert response.status_code == 403
@pytest.mark.asyncio
async def test_unauthenticated_cannot_view_all_appointments(self, client):
"""Unauthenticated user cannot view appointments."""
response = await client.get("/api/admin/appointments")
assert response.status_code == 401
class TestAdminCancelAppointment:
"""Test admin cancelling appointments."""
@pytest.mark.asyncio
async def test_admin_can_cancel_any_appointment(self, client_factory, regular_user, admin_user):
"""Admin can cancel any user's appointment."""
# Admin sets availability
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
await admin_client.put(
"/api/admin/availability",
json={
"date": str(tomorrow()),
"slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}],
},
)
# User books
async with client_factory.create(cookies=regular_user["cookies"]) as client:
book_response = await client.post(
"/api/booking",
json={"slot_start": f"{tomorrow()}T09:00:00Z"},
)
apt_id = book_response.json()["id"]
# Admin cancels
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel")
assert response.status_code == 200
data = response.json()
assert data["status"] == "cancelled_by_admin"
assert data["cancelled_at"] is not None
@pytest.mark.asyncio
async def test_regular_user_cannot_use_admin_cancel(self, client_factory, regular_user, admin_user):
"""Regular user cannot use admin cancel endpoint."""
# Admin sets availability
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
await admin_client.put(
"/api/admin/availability",
json={
"date": str(tomorrow()),
"slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}],
},
)
# User books
async with client_factory.create(cookies=regular_user["cookies"]) as client:
book_response = await client.post(
"/api/booking",
json={"slot_start": f"{tomorrow()}T09:00:00Z"},
)
apt_id = book_response.json()["id"]
# User tries to use admin cancel endpoint
response = await client.post(f"/api/admin/appointments/{apt_id}/cancel")
assert response.status_code == 403
@pytest.mark.asyncio
async def test_admin_cancel_nonexistent_appointment(self, client_factory, admin_user):
"""Returns 404 for non-existent appointment."""
async with client_factory.create(cookies=admin_user["cookies"]) as client:
response = await client.post("/api/admin/appointments/99999/cancel")
assert response.status_code == 404
@pytest.mark.asyncio
async def test_admin_cannot_cancel_already_cancelled(self, client_factory, regular_user, admin_user):
"""Admin cannot cancel an already cancelled appointment."""
# Admin sets availability
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
await admin_client.put(
"/api/admin/availability",
json={
"date": str(tomorrow()),
"slots": [{"start_time": "09:00:00", "end_time": "12:00:00"}],
},
)
# User books
async with client_factory.create(cookies=regular_user["cookies"]) as client:
book_response = await client.post(
"/api/booking",
json={"slot_start": f"{tomorrow()}T09:00:00Z"},
)
apt_id = book_response.json()["id"]
# User cancels their own appointment
await client.post(f"/api/appointments/{apt_id}/cancel")
# Admin tries to cancel again
async with client_factory.create(cookies=admin_user["cookies"]) as admin_client:
response = await admin_client.post(f"/api/admin/appointments/{apt_id}/cancel")
assert response.status_code == 400
assert "cancelled_by_user" in response.json()["detail"]

View file

@ -0,0 +1,259 @@
"use client";
import { useEffect, useState, useCallback } from "react";
import { Permission } from "../../auth-context";
import { api } from "../../api";
import { sharedStyles } from "../../styles/shared";
import { Header } from "../../components/Header";
import { useRequireAuth } from "../../hooks/useRequireAuth";
import { components } from "../../generated/api";
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
// Helper to format datetime
function formatDateTime(isoString: string): string {
const d = new Date(isoString);
return d.toLocaleString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
// Helper to get status display
function getStatusDisplay(status: string): { text: string; color: string } {
switch (status) {
case "booked":
return { text: "Booked", color: "#28a745" };
case "cancelled_by_user":
return { text: "Cancelled by user", color: "#dc3545" };
case "cancelled_by_admin":
return { text: "Cancelled by admin", color: "#dc3545" };
default:
return { text: status, color: "#666" };
}
}
export default function AdminAppointmentsPage() {
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredPermission: Permission.VIEW_ALL_APPOINTMENTS,
fallbackRedirect: "/",
});
const [appointments, setAppointments] = useState<AppointmentResponse[]>([]);
const [isLoadingAppointments, setIsLoadingAppointments] = useState(true);
const [cancellingId, setCancellingId] = useState<number | null>(null);
const [confirmCancelId, setConfirmCancelId] = useState<number | null>(null);
const [error, setError] = useState<string | null>(null);
const [statusFilter, setStatusFilter] = useState<string>("all");
const fetchAppointments = useCallback(async () => {
try {
const data = await api.get<AppointmentResponse[]>("/api/admin/appointments");
setAppointments(data);
} catch (err) {
console.error("Failed to fetch appointments:", err);
setError("Failed to load appointments");
} finally {
setIsLoadingAppointments(false);
}
}, []);
useEffect(() => {
if (user && isAuthorized) {
fetchAppointments();
}
}, [user, isAuthorized, fetchAppointments]);
const handleCancel = async (appointmentId: number) => {
setCancellingId(appointmentId);
setError(null);
try {
await api.post<AppointmentResponse>(`/api/admin/appointments/${appointmentId}/cancel`, {});
await fetchAppointments();
setConfirmCancelId(null);
} catch (err) {
if (err instanceof Error) {
setError(err.message);
} else {
setError("Failed to cancel appointment");
}
} finally {
setCancellingId(null);
}
};
if (isLoading) {
return (
<div style={sharedStyles.pageContainer}>
<Header currentPage="admin-appointments" />
<main style={sharedStyles.mainContent}>
<p>Loading...</p>
</main>
</div>
);
}
if (!isAuthorized) {
return null;
}
const filteredAppointments = appointments.filter((apt) => {
if (statusFilter === "all") return true;
return apt.status === statusFilter;
});
const bookedCount = appointments.filter((a) => a.status === "booked").length;
const cancelledCount = appointments.filter((a) => a.status !== "booked").length;
return (
<div style={sharedStyles.pageContainer}>
<Header currentPage="admin-appointments" />
<main style={sharedStyles.mainContent}>
<h1 style={{ marginBottom: "0.5rem" }}>All Appointments</h1>
<p style={{ color: "#666", marginBottom: "1.5rem" }}>
View and manage all user appointments
</p>
{error && (
<div style={{
background: "#f8d7da",
border: "1px solid #f5c6cb",
color: "#721c24",
padding: "1rem",
borderRadius: "8px",
marginBottom: "1rem",
}}>
{error}
</div>
)}
{/* Status Filter */}
<div style={{ marginBottom: "1rem", display: "flex", gap: "0.5rem", alignItems: "center" }}>
<label style={{ fontWeight: 500 }}>Filter:</label>
<select
value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value)}
style={{
padding: "0.5rem",
border: "1px solid #ddd",
borderRadius: "4px",
}}
>
<option value="all">All ({appointments.length})</option>
<option value="booked">Booked ({bookedCount})</option>
<option value="cancelled_by_user">Cancelled by User</option>
<option value="cancelled_by_admin">Cancelled by Admin</option>
</select>
</div>
{isLoadingAppointments ? (
<p>Loading appointments...</p>
) : appointments.length === 0 ? (
<p style={{ color: "#666" }}>No appointments yet.</p>
) : filteredAppointments.length === 0 ? (
<p style={{ color: "#666" }}>No appointments match the filter.</p>
) : (
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem" }}>
{filteredAppointments.map((apt) => {
const status = getStatusDisplay(apt.status);
const isPast = new Date(apt.slot_start) <= new Date();
return (
<div
key={apt.id}
style={{
border: "1px solid #ddd",
borderRadius: "8px",
padding: "1rem",
background: isPast ? "#fafafa" : "#fff",
opacity: isPast && apt.status !== "booked" ? 0.7 : 1,
}}
>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
<div>
<div style={{ fontWeight: 500, marginBottom: "0.25rem" }}>
{formatDateTime(apt.slot_start)}
</div>
<div style={{ color: "#666", fontSize: "0.875rem", marginBottom: "0.25rem" }}>
User: {apt.user_email}
</div>
{apt.note && (
<div style={{ color: "#888", fontSize: "0.875rem", marginBottom: "0.5rem", fontStyle: "italic" }}>
&quot;{apt.note}&quot;
</div>
)}
<span style={{
color: status.color,
fontSize: "0.75rem",
fontWeight: 500,
}}>
{status.text}
</span>
</div>
{apt.status === "booked" && (
<div>
{confirmCancelId === apt.id ? (
<div style={{ display: "flex", gap: "0.5rem" }}>
<button
onClick={() => handleCancel(apt.id)}
disabled={cancellingId === apt.id}
style={{
padding: "0.25rem 0.75rem",
background: "#dc3545",
color: "#fff",
border: "none",
borderRadius: "4px",
cursor: cancellingId === apt.id ? "not-allowed" : "pointer",
fontSize: "0.75rem",
}}
>
{cancellingId === apt.id ? "..." : "Confirm"}
</button>
<button
onClick={() => setConfirmCancelId(null)}
style={{
padding: "0.25rem 0.75rem",
background: "#fff",
border: "1px solid #ddd",
borderRadius: "4px",
cursor: "pointer",
fontSize: "0.75rem",
}}
>
No
</button>
</div>
) : (
<button
onClick={() => setConfirmCancelId(apt.id)}
style={{
padding: "0.25rem 0.75rem",
background: "#fff",
border: "1px solid #ddd",
borderRadius: "4px",
cursor: "pointer",
fontSize: "0.75rem",
color: "#666",
}}
>
Cancel
</button>
)}
</div>
)}
</div>
</div>
);
})}
</div>
)}
</main>
</div>
);
}

View file

@ -7,7 +7,7 @@ import constants from "../../../shared/constants.json";
const { ADMIN, REGULAR } = constants.roles;
type PageId = "counter" | "sum" | "profile" | "invites" | "booking" | "appointments" | "audit" | "admin-invites" | "admin-availability";
type PageId = "counter" | "sum" | "profile" | "invites" | "booking" | "appointments" | "audit" | "admin-invites" | "admin-availability" | "admin-appointments";
interface HeaderProps {
currentPage: PageId;
@ -34,6 +34,7 @@ const ADMIN_NAV_ITEMS: NavItem[] = [
{ id: "audit", label: "Audit", href: "/audit", adminOnly: true },
{ id: "admin-invites", label: "Invites", href: "/admin/invites", adminOnly: true },
{ id: "admin-availability", label: "Availability", href: "/admin/availability", adminOnly: true },
{ id: "admin-appointments", label: "Appointments", href: "/admin/appointments", adminOnly: true },
];
export function Header({ currentPage }: HeaderProps) {

View file

@ -436,6 +436,46 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/appointments": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* Get All Appointments
* @description Get all appointments (admin only), sorted by date descending.
*/
get: operations["get_all_appointments_api_admin_appointments_get"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/admin/appointments/{appointment_id}/cancel": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Admin Cancel Appointment
* @description Cancel any appointment (admin only).
*/
post: operations["admin_cancel_appointment_api_admin_appointments__appointment_id__cancel_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/meta/constants": {
parameters: {
query?: never;
@ -1575,6 +1615,57 @@ export interface operations {
};
};
};
get_all_appointments_api_admin_appointments_get: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AppointmentResponse"][];
};
};
};
};
admin_cancel_appointment_api_admin_appointments__appointment_id__cancel_post: {
parameters: {
query?: never;
header?: never;
path: {
appointment_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AppointmentResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
get_constants_api_meta_constants_get: {
parameters: {
query?: never;

View file

@ -2,9 +2,6 @@
"status": "failed",
"failedTests": [
"647d672ac99574a52088-7123e0baf27b194c0b82",
"647d672ac99574a52088-a2e9f69e9c9ef92dc2bb",
"647d672ac99574a52088-f95d3555fba5395917f0",
"a3a10f2351dcc49e1cb9-ac24d656b0f11d39342d",
"50879ed375f8988ef978-7be9cdef76510785fb98"
"647d672ac99574a52088-a2e9f69e9c9ef92dc2bb"
]
}

View file

@ -12,112 +12,116 @@
- link "Invites" [ref=e8] [cursor=pointer]:
- /url: /admin/invites
- generic [ref=e9]: •Availability
- generic [ref=e10]:
- generic [ref=e11]: admin@example.com
- button "Sign out" [ref=e12] [cursor=pointer]
- generic [ref=e14]:
- generic [ref=e16]:
- heading "Availability" [level=1] [ref=e17]
- paragraph [ref=e18]: Configure your available time slots for the next 30 days
- generic [ref=e19]:
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- generic [ref=e22]: Mon, Dec 22
- button "⎘" [ref=e23]
- generic [ref=e25]: 09:00 - 12:00
- generic [ref=e26] [cursor=pointer]:
- generic [ref=e28]: Tue, Dec 23
- generic [ref=e30]: No availability
- generic [ref=e31] [cursor=pointer]:
- generic [ref=e33]: Wed, Dec 24
- generic [ref=e35]: No availability
- generic [ref=e36] [cursor=pointer]:
- generic [ref=e38]: Thu, Dec 25
- generic [ref=e40]: No availability
- generic [ref=e41] [cursor=pointer]:
- generic [ref=e43]: Fri, Dec 26
- generic [ref=e45]: No availability
- generic [ref=e46] [cursor=pointer]:
- generic [ref=e48]: Sat, Dec 27
- generic [ref=e50]: No availability
- generic [ref=e51] [cursor=pointer]:
- generic [ref=e53]: Sun, Dec 28
- generic [ref=e55]: No availability
- generic [ref=e56] [cursor=pointer]:
- generic [ref=e58]: Mon, Dec 29
- generic [ref=e60]: No availability
- generic [ref=e61] [cursor=pointer]:
- generic [ref=e63]: Tue, Dec 30
- generic [ref=e65]: No availability
- generic [ref=e66] [cursor=pointer]:
- generic [ref=e68]: Wed, Dec 31
- generic [ref=e70]: No availability
- generic [ref=e71] [cursor=pointer]:
- generic [ref=e73]: Thu, Jan 1
- generic [ref=e75]: No availability
- generic [ref=e76] [cursor=pointer]:
- generic [ref=e78]: Fri, Jan 2
- generic [ref=e80]: No availability
- generic [ref=e81] [cursor=pointer]:
- generic [ref=e83]: Sat, Jan 3
- generic [ref=e85]: No availability
- generic [ref=e86] [cursor=pointer]:
- generic [ref=e88]: Sun, Jan 4
- generic [ref=e90]: No availability
- generic [ref=e91] [cursor=pointer]:
- generic [ref=e93]: Mon, Jan 5
- generic [ref=e95]: No availability
- generic [ref=e96] [cursor=pointer]:
- generic [ref=e98]: Tue, Jan 6
- generic [ref=e100]: No availability
- generic [ref=e101] [cursor=pointer]:
- generic [ref=e103]: Wed, Jan 7
- generic [ref=e105]: No availability
- generic [ref=e106] [cursor=pointer]:
- generic [ref=e108]: Thu, Jan 8
- generic [ref=e110]: No availability
- generic [ref=e111] [cursor=pointer]:
- generic [ref=e113]: Fri, Jan 9
- generic [ref=e115]: No availability
- generic [ref=e116] [cursor=pointer]:
- generic [ref=e118]: Sat, Jan 10
- generic [ref=e120]: No availability
- generic [ref=e121] [cursor=pointer]:
- generic [ref=e123]: Sun, Jan 11
- generic [ref=e125]: No availability
- generic [ref=e126] [cursor=pointer]:
- generic [ref=e128]: Mon, Jan 12
- generic [ref=e130]: No availability
- generic [ref=e131] [cursor=pointer]:
- generic [ref=e133]: Tue, Jan 13
- generic [ref=e135]: No availability
- generic [ref=e136] [cursor=pointer]:
- generic [ref=e138]: Wed, Jan 14
- generic [ref=e140]: No availability
- generic [ref=e141] [cursor=pointer]:
- generic [ref=e143]: Thu, Jan 15
- generic [ref=e145]: No availability
- generic [ref=e146] [cursor=pointer]:
- generic [ref=e148]: Fri, Jan 16
- generic [ref=e150]: No availability
- generic [ref=e151] [cursor=pointer]:
- generic [ref=e153]: Sat, Jan 17
- generic [ref=e155]: No availability
- generic [ref=e156] [cursor=pointer]:
- generic [ref=e158]: Sun, Jan 18
- generic [ref=e160]: No availability
- generic [ref=e161] [cursor=pointer]:
- generic [ref=e163]: Mon, Jan 19
- generic [ref=e165]: No availability
- generic [ref=e166] [cursor=pointer]:
- generic [ref=e168]: Tue, Jan 20
- generic [ref=e170]: No availability
- status [ref=e171]:
- generic [ref=e172]:
- img [ref=e174]
- generic [ref=e176]:
- generic [ref=e10]:
- text: •
- link "Appointments" [ref=e11] [cursor=pointer]:
- /url: /admin/appointments
- generic [ref=e12]:
- generic [ref=e13]: admin@example.com
- button "Sign out" [ref=e14] [cursor=pointer]
- generic [ref=e16]:
- generic [ref=e18]:
- heading "Availability" [level=1] [ref=e19]
- paragraph [ref=e20]: Configure your available time slots for the next 30 days
- generic [ref=e21]:
- generic [ref=e22] [cursor=pointer]:
- generic [ref=e23]:
- generic [ref=e24]: Mon, Dec 22
- button "⎘" [ref=e25]
- generic [ref=e27]: 09:00 - 12:00
- generic [ref=e28] [cursor=pointer]:
- generic [ref=e30]: Tue, Dec 23
- generic [ref=e32]: No availability
- generic [ref=e33] [cursor=pointer]:
- generic [ref=e35]: Wed, Dec 24
- generic [ref=e37]: No availability
- generic [ref=e38] [cursor=pointer]:
- generic [ref=e40]: Thu, Dec 25
- generic [ref=e42]: No availability
- generic [ref=e43] [cursor=pointer]:
- generic [ref=e45]: Fri, Dec 26
- generic [ref=e47]: No availability
- generic [ref=e48] [cursor=pointer]:
- generic [ref=e50]: Sat, Dec 27
- generic [ref=e52]: No availability
- generic [ref=e53] [cursor=pointer]:
- generic [ref=e55]: Sun, Dec 28
- generic [ref=e57]: No availability
- generic [ref=e58] [cursor=pointer]:
- generic [ref=e60]: Mon, Dec 29
- generic [ref=e62]: No availability
- generic [ref=e63] [cursor=pointer]:
- generic [ref=e65]: Tue, Dec 30
- generic [ref=e67]: No availability
- generic [ref=e68] [cursor=pointer]:
- generic [ref=e70]: Wed, Dec 31
- generic [ref=e72]: No availability
- generic [ref=e73] [cursor=pointer]:
- generic [ref=e75]: Thu, Jan 1
- generic [ref=e77]: No availability
- generic [ref=e78] [cursor=pointer]:
- generic [ref=e80]: Fri, Jan 2
- generic [ref=e82]: No availability
- generic [ref=e83] [cursor=pointer]:
- generic [ref=e85]: Sat, Jan 3
- generic [ref=e87]: No availability
- generic [ref=e88] [cursor=pointer]:
- generic [ref=e90]: Sun, Jan 4
- generic [ref=e92]: No availability
- generic [ref=e93] [cursor=pointer]:
- generic [ref=e95]: Mon, Jan 5
- generic [ref=e97]: No availability
- generic [ref=e98] [cursor=pointer]:
- generic [ref=e100]: Tue, Jan 6
- generic [ref=e102]: No availability
- generic [ref=e103] [cursor=pointer]:
- generic [ref=e105]: Wed, Jan 7
- generic [ref=e107]: No availability
- generic [ref=e108] [cursor=pointer]:
- generic [ref=e110]: Thu, Jan 8
- generic [ref=e112]: No availability
- generic [ref=e113] [cursor=pointer]:
- generic [ref=e115]: Fri, Jan 9
- generic [ref=e117]: No availability
- generic [ref=e118] [cursor=pointer]:
- generic [ref=e120]: Sat, Jan 10
- generic [ref=e122]: No availability
- generic [ref=e123] [cursor=pointer]:
- generic [ref=e125]: Sun, Jan 11
- generic [ref=e127]: No availability
- generic [ref=e128] [cursor=pointer]:
- generic [ref=e130]: Mon, Jan 12
- generic [ref=e132]: No availability
- generic [ref=e133] [cursor=pointer]:
- generic [ref=e135]: Tue, Jan 13
- generic [ref=e137]: No availability
- generic [ref=e138] [cursor=pointer]:
- generic [ref=e140]: Wed, Jan 14
- generic [ref=e142]: No availability
- generic [ref=e143] [cursor=pointer]:
- generic [ref=e145]: Thu, Jan 15
- generic [ref=e147]: No availability
- generic [ref=e148] [cursor=pointer]:
- generic [ref=e150]: Fri, Jan 16
- generic [ref=e152]: No availability
- generic [ref=e153] [cursor=pointer]:
- generic [ref=e155]: Sat, Jan 17
- generic [ref=e157]: No availability
- generic [ref=e158] [cursor=pointer]:
- generic [ref=e160]: Sun, Jan 18
- generic [ref=e162]: No availability
- generic [ref=e163] [cursor=pointer]:
- generic [ref=e165]: Mon, Jan 19
- generic [ref=e167]: No availability
- generic [ref=e168] [cursor=pointer]:
- generic [ref=e170]: Tue, Jan 20
- generic [ref=e172]: No availability
- status [ref=e173]:
- generic [ref=e174]:
- img [ref=e176]
- generic [ref=e178]:
- text: Static route
- button "Hide static indicator" [ref=e177] [cursor=pointer]:
- img [ref=e178]
- alert [ref=e181]
- button "Hide static indicator" [ref=e179] [cursor=pointer]:
- img [ref=e180]
- alert [ref=e183]
```

View file

@ -12,112 +12,116 @@
- link "Invites" [ref=e8] [cursor=pointer]:
- /url: /admin/invites
- generic [ref=e9]: •Availability
- generic [ref=e10]:
- generic [ref=e11]: admin@example.com
- button "Sign out" [ref=e12] [cursor=pointer]
- generic [ref=e14]:
- generic [ref=e16]:
- heading "Availability" [level=1] [ref=e17]
- paragraph [ref=e18]: Configure your available time slots for the next 30 days
- generic [ref=e19]:
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- generic [ref=e22]: Mon, Dec 22
- button "⎘" [ref=e23]
- generic [ref=e25]: 09:00 - 12:00
- generic [ref=e26] [cursor=pointer]:
- generic [ref=e28]: Tue, Dec 23
- generic [ref=e30]: No availability
- generic [ref=e31] [cursor=pointer]:
- generic [ref=e33]: Wed, Dec 24
- generic [ref=e35]: No availability
- generic [ref=e36] [cursor=pointer]:
- generic [ref=e38]: Thu, Dec 25
- generic [ref=e40]: No availability
- generic [ref=e41] [cursor=pointer]:
- generic [ref=e43]: Fri, Dec 26
- generic [ref=e45]: No availability
- generic [ref=e46] [cursor=pointer]:
- generic [ref=e48]: Sat, Dec 27
- generic [ref=e50]: No availability
- generic [ref=e51] [cursor=pointer]:
- generic [ref=e53]: Sun, Dec 28
- generic [ref=e55]: No availability
- generic [ref=e56] [cursor=pointer]:
- generic [ref=e58]: Mon, Dec 29
- generic [ref=e60]: No availability
- generic [ref=e61] [cursor=pointer]:
- generic [ref=e63]: Tue, Dec 30
- generic [ref=e65]: No availability
- generic [ref=e66] [cursor=pointer]:
- generic [ref=e68]: Wed, Dec 31
- generic [ref=e70]: No availability
- generic [ref=e71] [cursor=pointer]:
- generic [ref=e73]: Thu, Jan 1
- generic [ref=e75]: No availability
- generic [ref=e76] [cursor=pointer]:
- generic [ref=e78]: Fri, Jan 2
- generic [ref=e80]: No availability
- generic [ref=e81] [cursor=pointer]:
- generic [ref=e83]: Sat, Jan 3
- generic [ref=e85]: No availability
- generic [ref=e86] [cursor=pointer]:
- generic [ref=e88]: Sun, Jan 4
- generic [ref=e90]: No availability
- generic [ref=e91] [cursor=pointer]:
- generic [ref=e93]: Mon, Jan 5
- generic [ref=e95]: No availability
- generic [ref=e96] [cursor=pointer]:
- generic [ref=e98]: Tue, Jan 6
- generic [ref=e100]: No availability
- generic [ref=e101] [cursor=pointer]:
- generic [ref=e103]: Wed, Jan 7
- generic [ref=e105]: No availability
- generic [ref=e106] [cursor=pointer]:
- generic [ref=e108]: Thu, Jan 8
- generic [ref=e110]: No availability
- generic [ref=e111] [cursor=pointer]:
- generic [ref=e113]: Fri, Jan 9
- generic [ref=e115]: No availability
- generic [ref=e116] [cursor=pointer]:
- generic [ref=e118]: Sat, Jan 10
- generic [ref=e120]: No availability
- generic [ref=e121] [cursor=pointer]:
- generic [ref=e123]: Sun, Jan 11
- generic [ref=e125]: No availability
- generic [ref=e126] [cursor=pointer]:
- generic [ref=e128]: Mon, Jan 12
- generic [ref=e130]: No availability
- generic [ref=e131] [cursor=pointer]:
- generic [ref=e133]: Tue, Jan 13
- generic [ref=e135]: No availability
- generic [ref=e136] [cursor=pointer]:
- generic [ref=e138]: Wed, Jan 14
- generic [ref=e140]: No availability
- generic [ref=e141] [cursor=pointer]:
- generic [ref=e143]: Thu, Jan 15
- generic [ref=e145]: No availability
- generic [ref=e146] [cursor=pointer]:
- generic [ref=e148]: Fri, Jan 16
- generic [ref=e150]: No availability
- generic [ref=e151] [cursor=pointer]:
- generic [ref=e153]: Sat, Jan 17
- generic [ref=e155]: No availability
- generic [ref=e156] [cursor=pointer]:
- generic [ref=e158]: Sun, Jan 18
- generic [ref=e160]: No availability
- generic [ref=e161] [cursor=pointer]:
- generic [ref=e163]: Mon, Jan 19
- generic [ref=e165]: No availability
- generic [ref=e166] [cursor=pointer]:
- generic [ref=e168]: Tue, Jan 20
- generic [ref=e170]: No availability
- status [ref=e171]:
- generic [ref=e172]:
- img [ref=e174]
- generic [ref=e176]:
- generic [ref=e10]:
- text: •
- link "Appointments" [ref=e11] [cursor=pointer]:
- /url: /admin/appointments
- generic [ref=e12]:
- generic [ref=e13]: admin@example.com
- button "Sign out" [ref=e14] [cursor=pointer]
- generic [ref=e16]:
- generic [ref=e18]:
- heading "Availability" [level=1] [ref=e19]
- paragraph [ref=e20]: Configure your available time slots for the next 30 days
- generic [ref=e21]:
- generic [ref=e22] [cursor=pointer]:
- generic [ref=e23]:
- generic [ref=e24]: Mon, Dec 22
- button "⎘" [ref=e25]
- generic [ref=e27]: 09:00 - 12:00
- generic [ref=e28] [cursor=pointer]:
- generic [ref=e30]: Tue, Dec 23
- generic [ref=e32]: No availability
- generic [ref=e33] [cursor=pointer]:
- generic [ref=e35]: Wed, Dec 24
- generic [ref=e37]: No availability
- generic [ref=e38] [cursor=pointer]:
- generic [ref=e40]: Thu, Dec 25
- generic [ref=e42]: No availability
- generic [ref=e43] [cursor=pointer]:
- generic [ref=e45]: Fri, Dec 26
- generic [ref=e47]: No availability
- generic [ref=e48] [cursor=pointer]:
- generic [ref=e50]: Sat, Dec 27
- generic [ref=e52]: No availability
- generic [ref=e53] [cursor=pointer]:
- generic [ref=e55]: Sun, Dec 28
- generic [ref=e57]: No availability
- generic [ref=e58] [cursor=pointer]:
- generic [ref=e60]: Mon, Dec 29
- generic [ref=e62]: No availability
- generic [ref=e63] [cursor=pointer]:
- generic [ref=e65]: Tue, Dec 30
- generic [ref=e67]: No availability
- generic [ref=e68] [cursor=pointer]:
- generic [ref=e70]: Wed, Dec 31
- generic [ref=e72]: No availability
- generic [ref=e73] [cursor=pointer]:
- generic [ref=e75]: Thu, Jan 1
- generic [ref=e77]: No availability
- generic [ref=e78] [cursor=pointer]:
- generic [ref=e80]: Fri, Jan 2
- generic [ref=e82]: No availability
- generic [ref=e83] [cursor=pointer]:
- generic [ref=e85]: Sat, Jan 3
- generic [ref=e87]: No availability
- generic [ref=e88] [cursor=pointer]:
- generic [ref=e90]: Sun, Jan 4
- generic [ref=e92]: No availability
- generic [ref=e93] [cursor=pointer]:
- generic [ref=e95]: Mon, Jan 5
- generic [ref=e97]: No availability
- generic [ref=e98] [cursor=pointer]:
- generic [ref=e100]: Tue, Jan 6
- generic [ref=e102]: No availability
- generic [ref=e103] [cursor=pointer]:
- generic [ref=e105]: Wed, Jan 7
- generic [ref=e107]: No availability
- generic [ref=e108] [cursor=pointer]:
- generic [ref=e110]: Thu, Jan 8
- generic [ref=e112]: No availability
- generic [ref=e113] [cursor=pointer]:
- generic [ref=e115]: Fri, Jan 9
- generic [ref=e117]: No availability
- generic [ref=e118] [cursor=pointer]:
- generic [ref=e120]: Sat, Jan 10
- generic [ref=e122]: No availability
- generic [ref=e123] [cursor=pointer]:
- generic [ref=e125]: Sun, Jan 11
- generic [ref=e127]: No availability
- generic [ref=e128] [cursor=pointer]:
- generic [ref=e130]: Mon, Jan 12
- generic [ref=e132]: No availability
- generic [ref=e133] [cursor=pointer]:
- generic [ref=e135]: Tue, Jan 13
- generic [ref=e137]: No availability
- generic [ref=e138] [cursor=pointer]:
- generic [ref=e140]: Wed, Jan 14
- generic [ref=e142]: No availability
- generic [ref=e143] [cursor=pointer]:
- generic [ref=e145]: Thu, Jan 15
- generic [ref=e147]: No availability
- generic [ref=e148] [cursor=pointer]:
- generic [ref=e150]: Fri, Jan 16
- generic [ref=e152]: No availability
- generic [ref=e153] [cursor=pointer]:
- generic [ref=e155]: Sat, Jan 17
- generic [ref=e157]: No availability
- generic [ref=e158] [cursor=pointer]:
- generic [ref=e160]: Sun, Jan 18
- generic [ref=e162]: No availability
- generic [ref=e163] [cursor=pointer]:
- generic [ref=e165]: Mon, Jan 19
- generic [ref=e167]: No availability
- generic [ref=e168] [cursor=pointer]:
- generic [ref=e170]: Tue, Jan 20
- generic [ref=e172]: No availability
- status [ref=e173]:
- generic [ref=e174]:
- img [ref=e176]
- generic [ref=e178]:
- text: Static route
- button "Hide static indicator" [ref=e177] [cursor=pointer]:
- img [ref=e178]
- alert [ref=e181]
- button "Hide static indicator" [ref=e179] [cursor=pointer]:
- img [ref=e180]
- alert [ref=e183]
```

View file

@ -1,123 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- main [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- link "Audit" [ref=e6] [cursor=pointer]:
- /url: /audit
- generic [ref=e7]:
- text: •
- link "Invites" [ref=e8] [cursor=pointer]:
- /url: /admin/invites
- generic [ref=e9]: •Availability
- generic [ref=e10]:
- generic [ref=e11]: admin@example.com
- button "Sign out" [ref=e12] [cursor=pointer]
- generic [ref=e14]:
- generic [ref=e16]:
- heading "Availability" [level=1] [ref=e17]
- paragraph [ref=e18]: Configure your available time slots for the next 30 days
- generic [ref=e19]:
- generic [ref=e20] [cursor=pointer]:
- generic [ref=e21]:
- generic [ref=e22]: Mon, Dec 22
- button "⎘" [ref=e23]
- generic [ref=e25]: 09:00 - 12:00
- generic [ref=e26] [cursor=pointer]:
- generic [ref=e28]: Tue, Dec 23
- generic [ref=e30]: No availability
- generic [ref=e31] [cursor=pointer]:
- generic [ref=e33]: Wed, Dec 24
- generic [ref=e35]: No availability
- generic [ref=e36] [cursor=pointer]:
- generic [ref=e38]: Thu, Dec 25
- generic [ref=e40]: No availability
- generic [ref=e41] [cursor=pointer]:
- generic [ref=e43]: Fri, Dec 26
- generic [ref=e45]: No availability
- generic [ref=e46] [cursor=pointer]:
- generic [ref=e48]: Sat, Dec 27
- generic [ref=e50]: No availability
- generic [ref=e51] [cursor=pointer]:
- generic [ref=e53]: Sun, Dec 28
- generic [ref=e55]: No availability
- generic [ref=e56] [cursor=pointer]:
- generic [ref=e58]: Mon, Dec 29
- generic [ref=e60]: No availability
- generic [ref=e61] [cursor=pointer]:
- generic [ref=e63]: Tue, Dec 30
- generic [ref=e65]: No availability
- generic [ref=e66] [cursor=pointer]:
- generic [ref=e68]: Wed, Dec 31
- generic [ref=e70]: No availability
- generic [ref=e71] [cursor=pointer]:
- generic [ref=e73]: Thu, Jan 1
- generic [ref=e75]: No availability
- generic [ref=e76] [cursor=pointer]:
- generic [ref=e78]: Fri, Jan 2
- generic [ref=e80]: No availability
- generic [ref=e81] [cursor=pointer]:
- generic [ref=e83]: Sat, Jan 3
- generic [ref=e85]: No availability
- generic [ref=e86] [cursor=pointer]:
- generic [ref=e88]: Sun, Jan 4
- generic [ref=e90]: No availability
- generic [ref=e91] [cursor=pointer]:
- generic [ref=e93]: Mon, Jan 5
- generic [ref=e95]: No availability
- generic [ref=e96] [cursor=pointer]:
- generic [ref=e98]: Tue, Jan 6
- generic [ref=e100]: No availability
- generic [ref=e101] [cursor=pointer]:
- generic [ref=e103]: Wed, Jan 7
- generic [ref=e105]: No availability
- generic [ref=e106] [cursor=pointer]:
- generic [ref=e108]: Thu, Jan 8
- generic [ref=e110]: No availability
- generic [ref=e111] [cursor=pointer]:
- generic [ref=e113]: Fri, Jan 9
- generic [ref=e115]: No availability
- generic [ref=e116] [cursor=pointer]:
- generic [ref=e118]: Sat, Jan 10
- generic [ref=e120]: No availability
- generic [ref=e121] [cursor=pointer]:
- generic [ref=e123]: Sun, Jan 11
- generic [ref=e125]: No availability
- generic [ref=e126] [cursor=pointer]:
- generic [ref=e128]: Mon, Jan 12
- generic [ref=e130]: No availability
- generic [ref=e131] [cursor=pointer]:
- generic [ref=e133]: Tue, Jan 13
- generic [ref=e135]: No availability
- generic [ref=e136] [cursor=pointer]:
- generic [ref=e138]: Wed, Jan 14
- generic [ref=e140]: No availability
- generic [ref=e141] [cursor=pointer]:
- generic [ref=e143]: Thu, Jan 15
- generic [ref=e145]: No availability
- generic [ref=e146] [cursor=pointer]:
- generic [ref=e148]: Fri, Jan 16
- generic [ref=e150]: No availability
- generic [ref=e151] [cursor=pointer]:
- generic [ref=e153]: Sat, Jan 17
- generic [ref=e155]: No availability
- generic [ref=e156] [cursor=pointer]:
- generic [ref=e158]: Sun, Jan 18
- generic [ref=e160]: No availability
- generic [ref=e161] [cursor=pointer]:
- generic [ref=e163]: Mon, Jan 19
- generic [ref=e165]: No availability
- generic [ref=e166] [cursor=pointer]:
- generic [ref=e168]: Tue, Jan 20
- generic [ref=e170]: No availability
- status [ref=e171]:
- generic [ref=e172]:
- img [ref=e174]
- generic [ref=e176]:
- text: Static route
- button "Hide static indicator" [ref=e177] [cursor=pointer]:
- img [ref=e178]
- alert [ref=e181]
```

View file

@ -1,160 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e4]:
- link "Counter" [ref=e6] [cursor=pointer]:
- /url: /
- generic [ref=e7]:
- text: •
- link "Sum" [ref=e8] [cursor=pointer]:
- /url: /sum
- generic [ref=e9]: •Book
- generic [ref=e10]:
- text: •
- link "Appointments" [ref=e11] [cursor=pointer]:
- /url: /appointments
- generic [ref=e12]:
- text: •
- link "My Invites" [ref=e13] [cursor=pointer]:
- /url: /invites
- generic [ref=e14]:
- text: •
- link "My Profile" [ref=e15] [cursor=pointer]:
- /url: /profile
- generic [ref=e16]:
- generic [ref=e17]: user@example.com
- button "Sign out" [ref=e18] [cursor=pointer]
- main [ref=e19]:
- heading "Book an Appointment" [level=1] [ref=e20]
- paragraph [ref=e21]: Select a date to see available 15-minute slots
- generic [ref=e22]: "Request failed: 409"
- generic [ref=e23]:
- heading "Select a Date" [level=2] [ref=e24]
- generic [ref=e25]:
- button "Mon Dec 22" [ref=e26] [cursor=pointer]:
- generic [ref=e27]: Mon
- generic [ref=e28]: Dec 22
- button "Tue Dec 23" [ref=e29] [cursor=pointer]:
- generic [ref=e30]: Tue
- generic [ref=e31]: Dec 23
- button "Wed Dec 24" [ref=e32] [cursor=pointer]:
- generic [ref=e33]: Wed
- generic [ref=e34]: Dec 24
- button "Thu Dec 25" [ref=e35] [cursor=pointer]:
- generic [ref=e36]: Thu
- generic [ref=e37]: Dec 25
- button "Fri Dec 26" [ref=e38] [cursor=pointer]:
- generic [ref=e39]: Fri
- generic [ref=e40]: Dec 26
- button "Sat Dec 27" [ref=e41] [cursor=pointer]:
- generic [ref=e42]: Sat
- generic [ref=e43]: Dec 27
- button "Sun Dec 28" [ref=e44] [cursor=pointer]:
- generic [ref=e45]: Sun
- generic [ref=e46]: Dec 28
- button "Mon Dec 29" [ref=e47] [cursor=pointer]:
- generic [ref=e48]: Mon
- generic [ref=e49]: Dec 29
- button "Tue Dec 30" [ref=e50] [cursor=pointer]:
- generic [ref=e51]: Tue
- generic [ref=e52]: Dec 30
- button "Wed Dec 31" [ref=e53] [cursor=pointer]:
- generic [ref=e54]: Wed
- generic [ref=e55]: Dec 31
- button "Thu Jan 1" [ref=e56] [cursor=pointer]:
- generic [ref=e57]: Thu
- generic [ref=e58]: Jan 1
- button "Fri Jan 2" [ref=e59] [cursor=pointer]:
- generic [ref=e60]: Fri
- generic [ref=e61]: Jan 2
- button "Sat Jan 3" [ref=e62] [cursor=pointer]:
- generic [ref=e63]: Sat
- generic [ref=e64]: Jan 3
- button "Sun Jan 4" [ref=e65] [cursor=pointer]:
- generic [ref=e66]: Sun
- generic [ref=e67]: Jan 4
- button "Mon Jan 5" [ref=e68] [cursor=pointer]:
- generic [ref=e69]: Mon
- generic [ref=e70]: Jan 5
- button "Tue Jan 6" [ref=e71] [cursor=pointer]:
- generic [ref=e72]: Tue
- generic [ref=e73]: Jan 6
- button "Wed Jan 7" [ref=e74] [cursor=pointer]:
- generic [ref=e75]: Wed
- generic [ref=e76]: Jan 7
- button "Thu Jan 8" [ref=e77] [cursor=pointer]:
- generic [ref=e78]: Thu
- generic [ref=e79]: Jan 8
- button "Fri Jan 9" [ref=e80] [cursor=pointer]:
- generic [ref=e81]: Fri
- generic [ref=e82]: Jan 9
- button "Sat Jan 10" [ref=e83] [cursor=pointer]:
- generic [ref=e84]: Sat
- generic [ref=e85]: Jan 10
- button "Sun Jan 11" [ref=e86] [cursor=pointer]:
- generic [ref=e87]: Sun
- generic [ref=e88]: Jan 11
- button "Mon Jan 12" [ref=e89] [cursor=pointer]:
- generic [ref=e90]: Mon
- generic [ref=e91]: Jan 12
- button "Tue Jan 13" [ref=e92] [cursor=pointer]:
- generic [ref=e93]: Tue
- generic [ref=e94]: Jan 13
- button "Wed Jan 14" [ref=e95] [cursor=pointer]:
- generic [ref=e96]: Wed
- generic [ref=e97]: Jan 14
- button "Thu Jan 15" [ref=e98] [cursor=pointer]:
- generic [ref=e99]: Thu
- generic [ref=e100]: Jan 15
- button "Fri Jan 16" [ref=e101] [cursor=pointer]:
- generic [ref=e102]: Fri
- generic [ref=e103]: Jan 16
- button "Sat Jan 17" [ref=e104] [cursor=pointer]:
- generic [ref=e105]: Sat
- generic [ref=e106]: Jan 17
- button "Sun Jan 18" [ref=e107] [cursor=pointer]:
- generic [ref=e108]: Sun
- generic [ref=e109]: Jan 18
- button "Mon Jan 19" [ref=e110] [cursor=pointer]:
- generic [ref=e111]: Mon
- generic [ref=e112]: Jan 19
- button "Tue Jan 20" [ref=e113] [cursor=pointer]:
- generic [ref=e114]: Tue
- generic [ref=e115]: Jan 20
- generic [ref=e116]:
- heading "Available Slots for Monday, December 22" [level=2] [ref=e117]
- generic [ref=e118]:
- button "10:00" [ref=e119] [cursor=pointer]
- button "10:45" [ref=e120] [cursor=pointer]
- button "11:00" [ref=e121] [cursor=pointer]
- button "11:15" [ref=e122] [cursor=pointer]
- button "11:30" [ref=e123] [cursor=pointer]
- button "11:45" [ref=e124] [cursor=pointer]
- button "12:00" [ref=e125] [cursor=pointer]
- button "12:15" [ref=e126] [cursor=pointer]
- button "12:30" [ref=e127] [cursor=pointer]
- button "12:45" [ref=e128] [cursor=pointer]
- generic [ref=e129]:
- heading "Confirm Booking" [level=3] [ref=e130]
- paragraph [ref=e131]:
- strong [ref=e132]: "Time:"
- text: 11:00 - 11:15
- generic [ref=e133]:
- generic [ref=e134]: Note (optional, max 144 chars)
- textbox "Add a note about your appointment..." [ref=e135]
- generic [ref=e136]: 0/144
- generic [ref=e137]:
- button "Book Appointment" [ref=e138] [cursor=pointer]
- button "Cancel" [ref=e139] [cursor=pointer]
- status [ref=e140]:
- generic [ref=e141]:
- img [ref=e143]
- generic [ref=e145]:
- text: Static route
- button "Hide static indicator" [ref=e146] [cursor=pointer]:
- img [ref=e147]
- alert [ref=e150]
```

View file

@ -1,46 +0,0 @@
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- status [ref=e2]:
- generic [ref=e3]:
- img [ref=e5]
- generic [ref=e7]:
- text: Static route
- button "Hide static indicator" [ref=e8] [cursor=pointer]:
- img [ref=e9]
- alert [ref=e12]: ...
- main [ref=e13]:
- generic [ref=e14]:
- generic [ref=e15]:
- generic [ref=e16]: Counter
- generic [ref=e17]:
- text: •
- link "Sum" [ref=e18] [cursor=pointer]:
- /url: /sum
- generic [ref=e19]:
- text: •
- link "Book" [ref=e20] [cursor=pointer]:
- /url: /booking
- generic [ref=e21]:
- text: •
- link "Appointments" [ref=e22] [cursor=pointer]:
- /url: /appointments
- generic [ref=e23]:
- text: •
- link "My Invites" [ref=e24] [cursor=pointer]:
- /url: /invites
- generic [ref=e25]:
- text: •
- link "My Profile" [ref=e26] [cursor=pointer]:
- /url: /profile
- generic [ref=e27]:
- generic [ref=e28]: counter-1766272960115-d79hw5@example.com
- button "Sign out" [ref=e29] [cursor=pointer]
- generic [ref=e31]:
- generic [ref=e32]: Current Count
- heading "0" [level=1] [ref=e33]
- button "+ Increment" [ref=e34] [cursor=pointer]:
- generic [ref=e35]: +
- text: Increment
```