intermittent commit

This commit is contained in:
2026-07-14 13:22:12 +02:00
parent c1afce7bdb
commit 2f1b7fb6b8
9 changed files with 364 additions and 9 deletions

View File

@@ -161,8 +161,33 @@ export type SchedulingSummaryResponse = {
};
};
export type SchedulingAddressLookupCandidate = {
contact_id: string;
address_book_id: string;
display_name: string;
email?: string | null;
email_label?: string | null;
organization?: string | null;
role_title?: string | null;
tags: string[];
source_kind: string;
source_ref?: string | null;
source_revision?: string | null;
provenance: Record<string, unknown>;
};
export type SchedulingAddressLookupResponse = {
available: boolean;
candidates: SchedulingAddressLookupCandidate[];
};
const json = (payload: unknown) => ({ method: "POST", body: JSON.stringify(payload ?? {}) });
export function lookupSchedulingAddresses(settings: ApiSettings, query: string, limit = 25): Promise<SchedulingAddressLookupResponse> {
const params = new URLSearchParams({ query, limit: String(limit) });
return apiFetch<SchedulingAddressLookupResponse>(settings, `/api/v1/scheduling/address-lookup?${params.toString()}`);
}
export function listSchedulingRequests(settings: ApiSettings, status?: string): Promise<SchedulingRequestListResponse> {
const query = status ? `?status=${encodeURIComponent(status)}` : "";
return apiFetch<SchedulingRequestListResponse>(settings, `/api/v1/scheduling/requests${query}`);

View File

@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState, type FormEvent } from "react";
import { Bell, CalendarCheck, Check, Clock, Plus, RefreshCw, Send, XCircle } from "lucide-react";
import { Button, hasScope, type ApiSettings, type AuthInfo } from "@govoplan/core-webui";
import { Button, EmailAddressInput, hasScope, type ApiSettings, type AuthInfo, type MailboxAddress } from "@govoplan/core-webui";
import {
closeSchedulingRequest,
createSchedulingCalendarEvent,
@@ -11,6 +11,7 @@ import {
evaluateSchedulingFreeBusy,
listSchedulingNotifications,
listSchedulingRequests,
lookupSchedulingAddresses,
openSchedulingRequest,
schedulingSummary,
type SchedulingNotification,
@@ -51,6 +52,8 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
{ label: "Option 1", start_at: localValue(defaultStart), end_at: localValue(defaultEnd) }
]);
const [participants, setParticipants] = useState<ParticipantDraft[]>([{ display_name: "", email: "", required: true }]);
const [addressLookupQuery, setAddressLookupQuery] = useState("");
const [addressSuggestions, setAddressSuggestions] = useState<MailboxAddress[]>([]);
const canWrite = hasScope(auth, "scheduling:schedule:write");
const selected = useMemo(() => requests.find((item) => item.id === selectedId) || requests[0] || null, [requests, selectedId]);
@@ -73,6 +76,37 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
void loadDetails(selected.id);
}, [selected?.id]);
useEffect(() => {
const query = addressLookupQuery.trim();
if (!canWrite || query.length < 2) {
setAddressSuggestions([]);
return;
}
let cancelled = false;
const timer = window.setTimeout(() => {
void lookupSchedulingAddresses(settings, query).
then((response) => {
if (cancelled) return;
if (!response.available) {
setAddressSuggestions([]);
return;
}
setAddressSuggestions(
response.candidates
.map((candidate) => candidate.email ? { name: candidate.display_name || undefined, email: candidate.email } : null)
.filter((address): address is MailboxAddress => Boolean(address?.email))
);
}).
catch(() => {
if (!cancelled) setAddressSuggestions([]);
});
}, 200);
return () => {
cancelled = true;
window.clearTimeout(timer);
};
}, [addressLookupQuery, canWrite, settings.accessToken, settings.apiBaseUrl, settings.apiKey]);
async function loadRequests() {
setLoading(true);
setError("");
@@ -258,7 +292,22 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
{participants.map((participant, index) => (
<div className="scheduling-participant-draft" key={index}>
<input placeholder="Name" value={participant.display_name} onChange={(event) => updateParticipant(index, "display_name", event.target.value)} />
<input placeholder="Email" value={participant.email} onChange={(event) => updateParticipant(index, "email", event.target.value)} />
<EmailAddressInput
value={participant.email ? [{ name: participant.display_name || undefined, email: participant.email }] : []}
suggestions={addressSuggestions}
onSuggestionQueryChange={setAddressLookupQuery}
allowMultiple={false}
disabled={!canWrite}
compact
addLabel="Add participant"
emptyText="Participant email"
onChange={(addresses) => {
const address = addresses[0];
updateParticipantDraft(index, {
display_name: address?.name ?? participant.display_name,
email: address?.email ?? ""
});
}} />
</div>
))}
</div>
@@ -333,6 +382,10 @@ export default function SchedulingPage({ settings, auth }: { settings: ApiSettin
function updateParticipant(index: number, field: keyof ParticipantDraft, value: string) {
setParticipants((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, [field]: value } : item)));
}
function updateParticipantDraft(index: number, patch: Partial<ParticipantDraft>) {
setParticipants((items) => items.map((item, itemIndex) => (itemIndex === index ? { ...item, ...patch } : item)));
}
}
function isRequestEnvelope(value: unknown): value is { request: SchedulingRequest } {

View File

@@ -12,9 +12,9 @@ export const schedulingModule: PlatformWebModule = {
label: "Scheduling",
version: "1.0.0",
dependencies: ["poll"],
optionalDependencies: ["calendar", "mail", "notifications", "workflow", "appointments"],
optionalDependencies: ["calendar", "mail", "notifications", "workflow", "appointments", "addresses"],
translations: generatedTranslations,
navItems: [{ to: "/scheduling", label: "Scheduling", iconName: "calendar", anyOf: scheduleRead, order: 56 }],
navItems: [{ to: "/scheduling", label: "Scheduling", iconName: "calendar-clock", anyOf: scheduleRead, order: 56 }],
routes: [
{ path: "/scheduling", anyOf: scheduleRead, order: 56, render: ({ settings, auth }) => createElement(SchedulingPage, { settings, auth }) }
]