Phase 8
This commit is contained in:
82 files changed
+4903
-287
No files matched your search
@@ -0,0 +1,128 @@
|
||||
"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 “{state.selectedCalendarName}”</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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user