Files
NextChair/src/features/calendar/components/calendar-connection-panel.tsx
T
2026-08-04 09:57:58 +01:00

129 lines
4.4 KiB
TypeScript

"use client";
import { useState, useTransition } from "react";
import { Button } from "@/components/ui/button";
import {
disconnectCalendarAction,
listAvailableCalendarsAction,
selectCalendarAction,
syncNowAction,
} from "../actions/calendar";
import type { RemoteCalendarInfo } from "@/services/calendar/types";
export interface ConnectionState {
connected: boolean;
accountEmail?: string;
selectedCalendarName?: string;
lastSyncAt?: string;
lastSyncError?: string | null;
}
export function CalendarConnectionPanel({ initial, configured }: { initial: ConnectionState; configured: boolean }) {
const [state, setState] = useState(initial);
const [isPending, startTransition] = useTransition();
const [message, setMessage] = useState<string | null>(null);
const [calendars, setCalendars] = useState<RemoteCalendarInfo[] | null>(null);
const sync = () => {
setMessage(null);
startTransition(async () => {
const result = await syncNowAction();
setMessage(result.success ? "Sync complete." : (result.error ?? "Sync failed."));
});
};
const disconnect = () => {
setMessage(null);
startTransition(async () => {
await disconnectCalendarAction();
setState({ connected: false });
setMessage("Disconnected. Existing calendar events were left as they are.");
});
};
const loadCalendars = () => {
setMessage(null);
startTransition(async () => {
const result = await listAvailableCalendarsAction();
if (!result.success) {
setMessage(result.error ?? "Couldn't load calendars.");
return;
}
setCalendars(result.calendars ?? []);
});
};
const chooseCalendar = (calendar: RemoteCalendarInfo) => {
startTransition(async () => {
const result = await selectCalendarAction(calendar.id, calendar.name);
if (result.success) {
setState((prev) => ({ ...prev, selectedCalendarName: calendar.name }));
setCalendars(null);
setMessage(`Now syncing to "${calendar.name}".`);
}
});
};
if (!state.connected) {
return (
<div className="border-line rounded-jumbo border bg-white p-5">
<p className="mb-1 text-sm font-bold">Google Calendar</p>
<p className="text-muted mb-4 text-xs">
Sync your confirmed appointments to your own Google Calendar. This connects a Google account separately
from sign-in — you can use a different one if you like.
</p>
{message && <p className="text-danger mb-3 text-xs font-medium">{message}</p>}
{configured ? (
<Button asChild size="sm">
<a href="/api/staff/calendar/google/authorize">Connect Google Calendar</a>
</Button>
) : (
<p className="text-muted text-xs">Google Calendar sync is not configured on this deployment yet.</p>
)}
</div>
);
}
return (
<div className="border-line rounded-jumbo border bg-white p-5">
<p className="mb-1 text-sm font-bold">Google Calendar</p>
<div className="text-muted mb-4 space-y-1 text-xs">
<p>Connected as {state.accountEmail}</p>
<p>Syncing to &ldquo;{state.selectedCalendarName}&rdquo;</p>
{state.lastSyncAt && <p>Last synced {new Date(state.lastSyncAt).toLocaleString()}</p>}
{state.lastSyncError && <p className="text-danger">Last sync error: {state.lastSyncError}</p>}
</div>
{message && <p className="text-muted mb-3 text-xs font-medium">{message}</p>}
{calendars ? (
<ul className="mb-4 space-y-2">
{calendars.map((calendar) => (
<li key={calendar.id}>
<button
type="button"
onClick={() => chooseCalendar(calendar)}
className="border-line w-full rounded-lg border px-3 py-2 text-left text-xs font-bold hover:bg-stone"
>
{calendar.name} {calendar.primary && "(primary)"}
</button>
</li>
))}
</ul>
) : null}
<div className="flex flex-wrap gap-2">
<Button size="sm" isLoading={isPending} onClick={sync}>
Sync Now
</Button>
<Button size="sm" variant="secondary" isLoading={isPending} onClick={loadCalendars}>
Change Calendar
</Button>
<Button size="sm" variant="destructive" isLoading={isPending} onClick={disconnect}>
Disconnect
</Button>
</div>
</div>
);
}